forked from pdsls.dev/pdsls
this repo has no description

increase printWidth to 100

+1
.prettierrc
··· 1 { 2 "experimentalTernaries": true, 3 "plugins": ["prettier-plugin-tailwindcss"] 4 }
··· 1 { 2 + "printWidth": 100, 3 "experimentalTernaries": true, 4 "plugins": ["prettier-plugin-tailwindcss"] 5 }
+2 -6
index.html
··· 7 <meta property="og:title" content="PDSls" /> 8 <meta property="og:type" content="website" /> 9 <meta property="og:url" content="https://pdsls.dev" /> 10 - <meta 11 - property="og:description" 12 - content="Browse and manage atproto repositories" 13 - /> 14 <title>PDSls</title> 15 <script> 16 if ( 17 localStorage.theme === "dark" || 18 - (!("theme" in localStorage) && 19 - window.matchMedia("(prefers-color-scheme: dark)").matches) 20 ) 21 document.documentElement.classList.add("dark"); 22 else document.documentElement.classList.remove("dark");
··· 7 <meta property="og:title" content="PDSls" /> 8 <meta property="og:type" content="website" /> 9 <meta property="og:url" content="https://pdsls.dev" /> 10 + <meta property="og:description" content="Browse and manage atproto repositories" /> 11 <title>PDSls</title> 12 <script> 13 if ( 14 localStorage.theme === "dark" || 15 + (!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches) 16 ) 17 document.documentElement.classList.add("dark"); 18 else document.documentElement.classList.remove("dark");
+3 -15
src/components/account.tsx
··· 1 - import { 2 - createSignal, 3 - onMount, 4 - Show, 5 - onCleanup, 6 - createEffect, 7 - For, 8 - } from "solid-js"; 9 import Tooltip from "./tooltip.jsx"; 10 - import { 11 - deleteStoredSession, 12 - getSession, 13 - OAuthUserAgent, 14 - } from "@atcute/oauth-browser-client"; 15 import { agent, Login, setLoginState } from "./login.jsx"; 16 import { At } from "@atcute/client/lexicons"; 17 ··· 89 classList={{ 90 "basis-full text-left font-mono max-w-[32ch] text-sm truncate group-hover/select:bg-slate-300 dark:group-hover/select:bg-neutral-700": 91 true, 92 - "text-green-500 dark:text-green-400": 93 - session === agent?.sub, 94 }} 95 onclick={() => resumeSession(session)} 96 >
··· 1 + import { createSignal, onMount, Show, onCleanup, createEffect, For } from "solid-js"; 2 import Tooltip from "./tooltip.jsx"; 3 + import { deleteStoredSession, getSession, OAuthUserAgent } from "@atcute/oauth-browser-client"; 4 import { agent, Login, setLoginState } from "./login.jsx"; 5 import { At } from "@atcute/client/lexicons"; 6 ··· 78 classList={{ 79 "basis-full text-left font-mono max-w-[32ch] text-sm truncate group-hover/select:bg-slate-300 dark:group-hover/select:bg-neutral-700": 80 true, 81 + "text-green-500 dark:text-green-400": session === agent?.sub, 82 }} 83 onclick={() => resumeSession(session)} 84 >
+7 -32
src/components/backlinks.tsx
··· 45 {({ collection, path, matchesFilter, counts }) => ( 46 <div class="mt-2 font-mono text-sm sm:text-base"> 47 <p classList={{ "text-stone-400": matchesFilter }}> 48 - <span title="Collection containing linking records"> 49 - {collection} 50 - </span> 51 <span class="text-cyan-500">@</span> 52 - <span title="Record path where the link is found"> 53 - {path.slice(1)} 54 - </span> 55 - : 56 </p> 57 <div class="pl-2.5 font-sans"> 58 <p> ··· 78 href="#" 79 title="Show linking DIDs" 80 onclick={() => 81 - ( 82 - show()?.collection === collection && 83 - show()?.path === path && 84 - show()?.showDids 85 - ) ? 86 setShow(null) 87 : setShow({ collection, path, showDids: true }) 88 } ··· 91 {counts.distinct_dids < 2 ? "" : "s"} 92 </a> 93 </p> 94 - <Show 95 - when={ 96 - show()?.collection === collection && show()?.path === path 97 - } 98 - > 99 <Show when={show()?.showDids}> 100 {/* putting this in the `dids` prop directly failed to re-render. idk how to solidjs. */} 101 <p class="w-full font-semibold text-stone-600 dark:text-stone-400"> 102 Distinct identities 103 </p> 104 - <BacklinkItems 105 - target={target} 106 - collection={collection} 107 - path={path} 108 - dids={true} 109 - /> 110 </Show> 111 <Show when={!show()?.showDids}> 112 - <p class="w-full font-semibold text-stone-600 dark:text-stone-400"> 113 - Records 114 - </p> 115 - <BacklinkItems 116 - target={target} 117 - collection={collection} 118 - path={path} 119 - dids={false} 120 - /> 121 </Show> 122 </Show> 123 </div>
··· 45 {({ collection, path, matchesFilter, counts }) => ( 46 <div class="mt-2 font-mono text-sm sm:text-base"> 47 <p classList={{ "text-stone-400": matchesFilter }}> 48 + <span title="Collection containing linking records">{collection}</span> 49 <span class="text-cyan-500">@</span> 50 + <span title="Record path where the link is found">{path.slice(1)}</span>: 51 </p> 52 <div class="pl-2.5 font-sans"> 53 <p> ··· 73 href="#" 74 title="Show linking DIDs" 75 onclick={() => 76 + show()?.collection === collection && show()?.path === path && show()?.showDids ? 77 setShow(null) 78 : setShow({ collection, path, showDids: true }) 79 } ··· 82 {counts.distinct_dids < 2 ? "" : "s"} 83 </a> 84 </p> 85 + <Show when={show()?.collection === collection && show()?.path === path}> 86 <Show when={show()?.showDids}> 87 {/* putting this in the `dids` prop directly failed to re-render. idk how to solidjs. */} 88 <p class="w-full font-semibold text-stone-600 dark:text-stone-400"> 89 Distinct identities 90 </p> 91 + <BacklinkItems target={target} collection={collection} path={path} dids={true} /> 92 </Show> 93 <Show when={!show()?.showDids}> 94 + <p class="w-full font-semibold text-stone-600 dark:text-stone-400">Records</p> 95 + <BacklinkItems target={target} collection={collection} path={path} dids={false} /> 96 </Show> 97 </Show> 98 </div>
+1 -3
src/components/create.tsx
··· 137 </div> 138 <Editor theme={theme().color} model={model!} /> 139 <div class="flex flex-col gap-x-2"> 140 - <div class="text-red-500 dark:text-red-400"> 141 - {createNotice()} 142 - </div> 143 <div class="flex items-center justify-end gap-2"> 144 <button 145 onclick={() => setOpenCreate(false)}
··· 137 </div> 138 <Editor theme={theme().color} model={model!} /> 139 <div class="flex flex-col gap-x-2"> 140 + <div class="text-red-500 dark:text-red-400">{createNotice()}</div> 141 <div class="flex items-center justify-end gap-2"> 142 <button 143 onclick={() => setOpenCreate(false)}
+5 -29
src/components/json.tsx
··· 11 } 12 13 export const syntaxHighlight = (json: string) => { 14 - json = json 15 - .replace(/&/g, "&amp;") 16 - .replace(/</g, "&lt;") 17 - .replace(/>/g, "&gt;"); 18 19 return json.replace( 20 /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, ··· 70 ["http:", "https:", "web+at:"].includes(new URL(part).protocol) && 71 part.split("\n").length === 1 72 ) ? 73 - <a 74 - class="underline" 75 - href={part} 76 - target="_blank" 77 - rel="noopener noreferer" 78 - > 79 {part} 80 </a> 81 : part} ··· 91 }; 92 93 const JSONBoolean = ({ data }: { data: boolean }) => { 94 - return ( 95 - <span class="text-[#f57d26] dark:text-orange-300"> 96 - {data ? "true" : "false"} 97 - </span> 98 - ); 99 }; 100 101 const JSONNull = () => { 102 return <span class="text-neutral-400 dark:text-neutral-500">null</span>; 103 }; 104 105 - const JSONObject = ({ 106 - data, 107 - repo, 108 - }: { 109 - data: { [x: string]: JSONType }; 110 - repo: string; 111 - }) => { 112 const [clip, setClip] = createSignal(false); 113 const rawObj = ( 114 <For each={Object.entries(data)}> ··· 214 return <JSONObject data={data} repo={repo} />; 215 }; 216 217 - export type JSONType = 218 - | string 219 - | number 220 - | boolean 221 - | null 222 - | { [x: string]: JSONType } 223 - | JSONType[];
··· 11 } 12 13 export const syntaxHighlight = (json: string) => { 14 + json = json.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); 15 16 return json.replace( 17 /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, ··· 67 ["http:", "https:", "web+at:"].includes(new URL(part).protocol) && 68 part.split("\n").length === 1 69 ) ? 70 + <a class="underline" href={part} target="_blank" rel="noopener noreferer"> 71 {part} 72 </a> 73 : part} ··· 83 }; 84 85 const JSONBoolean = ({ data }: { data: boolean }) => { 86 + return <span class="text-[#f57d26] dark:text-orange-300">{data ? "true" : "false"}</span>; 87 }; 88 89 const JSONNull = () => { 90 return <span class="text-neutral-400 dark:text-neutral-500">null</span>; 91 }; 92 93 + const JSONObject = ({ data, repo }: { data: { [x: string]: JSONType }; repo: string }) => { 94 const [clip, setClip] = createSignal(false); 95 const rawObj = ( 96 <For each={Object.entries(data)}> ··· 196 return <JSONObject data={data} repo={repo} />; 197 }; 198 199 + export type JSONType = string | number | boolean | null | { [x: string]: JSONType } | JSONType[];
+4 -14
src/components/navbar.tsx
··· 6 export const [pds, setPDS] = createSignal<string>(); 7 export const [cid, setCID] = createSignal<string>(); 8 export const [isLabeler, setIsLabeler] = createSignal(false); 9 - export const [validRecord, setValidRecord] = createSignal<boolean | undefined>( 10 - undefined, 11 - ); 12 13 const NavBar = (props: { params: Params }) => { 14 const [openMenu, setOpenMenu] = createSignal(false); ··· 29 <Tooltip text="PDS"> 30 <div class="i-tabler-server mr-1 shrink-0" /> 31 </Tooltip> 32 - <A 33 - end 34 - href={pds()!} 35 - inactiveClass="text-lightblue-500 w-full hover:underline" 36 - > 37 {pds()} 38 </A> 39 </Show> ··· 49 <Show when={props.params.repo}> 50 <button 51 class="p-0.75 flex items-center rounded hover:bg-neutral-200 dark:hover:bg-neutral-600" 52 - onclick={() => 53 - navigator.clipboard.writeText(props.params.repo) 54 - } 55 > 56 Copy DID 57 </button> ··· 121 </Show> 122 <Show 123 when={ 124 - props.params.repo in labelerCache && 125 - !props.params.collection && 126 - !props.params.rkey 127 } 128 > 129 <div class="mt-1 flex items-center">
··· 6 export const [pds, setPDS] = createSignal<string>(); 7 export const [cid, setCID] = createSignal<string>(); 8 export const [isLabeler, setIsLabeler] = createSignal(false); 9 + export const [validRecord, setValidRecord] = createSignal<boolean | undefined>(undefined); 10 11 const NavBar = (props: { params: Params }) => { 12 const [openMenu, setOpenMenu] = createSignal(false); ··· 27 <Tooltip text="PDS"> 28 <div class="i-tabler-server mr-1 shrink-0" /> 29 </Tooltip> 30 + <A end href={pds()!} inactiveClass="text-lightblue-500 w-full hover:underline"> 31 {pds()} 32 </A> 33 </Show> ··· 43 <Show when={props.params.repo}> 44 <button 45 class="p-0.75 flex items-center rounded hover:bg-neutral-200 dark:hover:bg-neutral-600" 46 + onclick={() => navigator.clipboard.writeText(props.params.repo)} 47 > 48 Copy DID 49 </button> ··· 113 </Show> 114 <Show 115 when={ 116 + props.params.repo in labelerCache && !props.params.collection && !props.params.rkey 117 } 118 > 119 <div class="mt-1 flex items-center">
+3 -9
src/components/search.tsx
··· 13 !input.startsWith("https://main.bsky.dev/") && 14 (input.startsWith("https://") || input.startsWith("http://")) 15 ) 16 - throw redirect( 17 - `/${input.replace("https://", "").replace("http://", "").replace("/", "")}`, 18 - ); 19 20 const uri = input 21 .replace("at://", "") ··· 30 } catch { 31 throw redirect(`/${actor}`); 32 } 33 - throw redirect( 34 - `/at://${did}${uriParts.length > 1 ? `/${uriParts.slice(1).join("/")}` : ""}`, 35 - ); 36 }); 37 38 const Search = () => { ··· 77 </Show> 78 </div> 79 </form> 80 - <Show when={submission.error}> 81 - {(err) => <div class="mt-3">{err().message}</div>} 82 - </Show> 83 </> 84 ); 85 };
··· 13 !input.startsWith("https://main.bsky.dev/") && 14 (input.startsWith("https://") || input.startsWith("http://")) 15 ) 16 + throw redirect(`/${input.replace("https://", "").replace("http://", "").replace("/", "")}`); 17 18 const uri = input 19 .replace("at://", "") ··· 28 } catch { 29 throw redirect(`/${actor}`); 30 } 31 + throw redirect(`/at://${did}${uriParts.length > 1 ? `/${uriParts.slice(1).join("/")}` : ""}`); 32 }); 33 34 const Search = () => { ··· 73 </Show> 74 </div> 75 </form> 76 + <Show when={submission.error}>{(err) => <div class="mt-3">{err().message}</div>}</Show> 77 </> 78 ); 79 };
+10 -33
src/components/settings.tsx
··· 4 const getInitialTheme = () => { 5 const isDarkMode = 6 localStorage.theme === "dark" || 7 - (!("theme" in localStorage) && 8 - window.matchMedia("(prefers-color-scheme: dark)").matches); 9 return { 10 color: isDarkMode ? "dark" : "light", 11 system: !("theme" in localStorage), ··· 13 }; 14 15 export const [theme, setTheme] = createSignal(getInitialTheme()); 16 - const [backlinksEnabled, setBacklinksEnabled] = createSignal( 17 - localStorage.backlinks === "true", 18 - ); 19 20 const Settings = () => { 21 const [modal, setModal] = createSignal<HTMLDialogElement>(); ··· 41 onMount(() => { 42 window.addEventListener("keydown", keyEvent); 43 window.addEventListener("click", clickEvent); 44 - window 45 - .matchMedia("(prefers-color-scheme: dark)") 46 - .addEventListener("change", themeEvent); 47 }); 48 49 onCleanup(() => { 50 window.removeEventListener("keydown", keyEvent); 51 window.removeEventListener("click", clickEvent); 52 - window 53 - .matchMedia("(prefers-color-scheme: dark)") 54 - .removeEventListener("change", themeEvent); 55 }); 56 57 createEffect(() => { ··· 61 62 const updateTheme = (newTheme: { color: string; system: boolean }) => { 63 setTheme(newTheme); 64 - document.documentElement.classList.toggle( 65 - "dark", 66 - newTheme.color === "dark", 67 - ); 68 if (newTheme.system) { 69 localStorage.removeItem("theme"); 70 } else { ··· 80 class="backdrop-brightness-60 fixed left-0 top-0 z-20 flex h-screen w-screen items-center justify-center bg-transparent" 81 > 82 <div class="dark:bg-dark-400 top-10% absolute rounded-md border border-slate-900 bg-slate-100 p-4 text-slate-900 dark:border-slate-100 dark:text-slate-100"> 83 - <h3 class="mb-2 border-b border-neutral-500 pb-2 text-xl font-bold"> 84 - Settings 85 - </h3> 86 <h4 class="mb-1 font-semibold">Theme</h4> 87 <div class="w-xs flex divide-x divide-neutral-500 overflow-hidden rounded-lg border border-neutral-500"> 88 <button ··· 94 onclick={() => 95 updateTheme({ 96 color: 97 - ( 98 - window.matchMedia("(prefers-color-scheme: dark)") 99 - .matches 100 - ) ? 101 - "dark" 102 - : "light", 103 system: true, 104 }) 105 } ··· 111 "basis-1/3 p-2": true, 112 "hover:bg-slate-200 dark:hover:bg-dark-200": 113 theme().color !== "light" || theme().system, 114 - "bg-neutral-500 text-slate-100": 115 - theme().color === "light" && !theme().system, 116 }} 117 onclick={() => updateTheme({ color: "light", system: false })} 118 > ··· 161 name="constellation" 162 type="text" 163 spellcheck={false} 164 - value={ 165 - localStorage.constellationHost || 166 - "https://constellation.microcosm.blue" 167 - } 168 disabled={!backlinksEnabled()} 169 class="dark:bg-dark-100 rounded-lg border border-gray-400 px-2 py-1 focus:outline-none focus:ring-1 focus:ring-gray-300 disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 dark:disabled:border-gray-700 dark:disabled:bg-gray-800/20" 170 - onInput={(e) => 171 - (localStorage.constellationHost = e.currentTarget.value) 172 - } 173 /> 174 </div> 175 </div>
··· 4 const getInitialTheme = () => { 5 const isDarkMode = 6 localStorage.theme === "dark" || 7 + (!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches); 8 return { 9 color: isDarkMode ? "dark" : "light", 10 system: !("theme" in localStorage), ··· 12 }; 13 14 export const [theme, setTheme] = createSignal(getInitialTheme()); 15 + const [backlinksEnabled, setBacklinksEnabled] = createSignal(localStorage.backlinks === "true"); 16 17 const Settings = () => { 18 const [modal, setModal] = createSignal<HTMLDialogElement>(); ··· 38 onMount(() => { 39 window.addEventListener("keydown", keyEvent); 40 window.addEventListener("click", clickEvent); 41 + window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", themeEvent); 42 }); 43 44 onCleanup(() => { 45 window.removeEventListener("keydown", keyEvent); 46 window.removeEventListener("click", clickEvent); 47 + window.matchMedia("(prefers-color-scheme: dark)").removeEventListener("change", themeEvent); 48 }); 49 50 createEffect(() => { ··· 54 55 const updateTheme = (newTheme: { color: string; system: boolean }) => { 56 setTheme(newTheme); 57 + document.documentElement.classList.toggle("dark", newTheme.color === "dark"); 58 if (newTheme.system) { 59 localStorage.removeItem("theme"); 60 } else { ··· 70 class="backdrop-brightness-60 fixed left-0 top-0 z-20 flex h-screen w-screen items-center justify-center bg-transparent" 71 > 72 <div class="dark:bg-dark-400 top-10% absolute rounded-md border border-slate-900 bg-slate-100 p-4 text-slate-900 dark:border-slate-100 dark:text-slate-100"> 73 + <h3 class="mb-2 border-b border-neutral-500 pb-2 text-xl font-bold">Settings</h3> 74 <h4 class="mb-1 font-semibold">Theme</h4> 75 <div class="w-xs flex divide-x divide-neutral-500 overflow-hidden rounded-lg border border-neutral-500"> 76 <button ··· 82 onclick={() => 83 updateTheme({ 84 color: 85 + window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light", 86 system: true, 87 }) 88 } ··· 94 "basis-1/3 p-2": true, 95 "hover:bg-slate-200 dark:hover:bg-dark-200": 96 theme().color !== "light" || theme().system, 97 + "bg-neutral-500 text-slate-100": theme().color === "light" && !theme().system, 98 }} 99 onclick={() => updateTheme({ color: "light", system: false })} 100 > ··· 143 name="constellation" 144 type="text" 145 spellcheck={false} 146 + value={localStorage.constellationHost || "https://constellation.microcosm.blue"} 147 disabled={!backlinksEnabled()} 148 class="dark:bg-dark-100 rounded-lg border border-gray-400 px-2 py-1 focus:outline-none focus:ring-1 focus:ring-gray-300 disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 dark:disabled:border-gray-700 dark:disabled:bg-gray-800/20" 149 + onInput={(e) => (localStorage.constellationHost = e.currentTarget.value)} 150 /> 151 </div> 152 </div>
+5 -20
src/layout.tsx
··· 28 window.location.href = location.pathname.replace(params.repo, did); 29 } 30 await retrieveSession(); 31 - if (loginState() && location.pathname === "/") 32 - window.location.href = `/at://${agent.sub}`; 33 }); 34 35 return ( 36 - <div 37 - id="main" 38 - class="m-5 flex flex-col items-center text-slate-900 dark:text-slate-100" 39 - > 40 <Show when={location.pathname !== "/"}> 41 <MetaProvider> 42 <Meta name="robots" content="noindex, nofollow" /> ··· 69 </div> 70 </div> 71 <div class="mb-5 flex max-w-full flex-col items-center text-pretty md:max-w-screen-md"> 72 - <Show 73 - when={ 74 - location.pathname !== "/jetstream" && 75 - location.pathname !== "/firehose" 76 - } 77 - > 78 <Search /> 79 </Show> 80 <Show when={params.pds}> ··· 82 </Show> 83 <Show keyed when={location.pathname}> 84 <ErrorBoundary 85 - fallback={(err) => ( 86 - <div class="mt-3 break-words">Error: {err.message}</div> 87 - )} 88 > 89 - <Suspense 90 - fallback={ 91 - <div class="i-line-md-loading-twotone-loop mt-3 text-xl" /> 92 - } 93 - > 94 {props.children} 95 </Suspense> 96 </ErrorBoundary>
··· 28 window.location.href = location.pathname.replace(params.repo, did); 29 } 30 await retrieveSession(); 31 + if (loginState() && location.pathname === "/") window.location.href = `/at://${agent.sub}`; 32 }); 33 34 return ( 35 + <div id="main" class="m-5 flex flex-col items-center text-slate-900 dark:text-slate-100"> 36 <Show when={location.pathname !== "/"}> 37 <MetaProvider> 38 <Meta name="robots" content="noindex, nofollow" /> ··· 65 </div> 66 </div> 67 <div class="mb-5 flex max-w-full flex-col items-center text-pretty md:max-w-screen-md"> 68 + <Show when={location.pathname !== "/jetstream" && location.pathname !== "/firehose"}> 69 <Search /> 70 </Show> 71 <Show when={params.pds}> ··· 73 </Show> 74 <Show keyed when={location.pathname}> 75 <ErrorBoundary 76 + fallback={(err) => <div class="mt-3 break-words">Error: {err.message}</div>} 77 > 78 + <Suspense fallback={<div class="i-line-md-loading-twotone-loop mt-3 text-xl" />}> 79 {props.children} 80 </Suspense> 81 </ErrorBoundary>
+2 -2
src/styles/index.css
··· 4 samp, 5 pre { 6 font-family: 7 - "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 8 - "Liberation Mono", "Courier New", monospace; 9 } 10 11 .string {
··· 4 samp, 5 pre { 6 font-family: 7 + "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", 8 + "Courier New", monospace; 9 } 10 11 .string {
+11 -7
src/styles/tailwind.css
··· 15 16 ::before, 17 ::after { 18 - --un-content: ''; 19 } 20 21 /* ··· 34 -webkit-text-size-adjust: 100%; /* 2 */ 35 -moz-tab-size: 4; /* 3 */ 36 tab-size: 4; /* 3 */ 37 - font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 4 */ 38 font-feature-settings: normal; /* 5 */ 39 font-variation-settings: normal; /* 6 */ 40 -webkit-tap-highlight-color: transparent; /* 7 */ ··· 113 kbd, 114 samp, 115 pre { 116 - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* 1 */ 117 font-feature-settings: normal; /* 2 */ 118 font-variation-settings: normal; /* 3 */ 119 font-size: 1em; /* 4 */ ··· 196 */ 197 198 button, 199 - [type='button'], 200 - [type='reset'], 201 - [type='submit'] { 202 -webkit-appearance: button; /* 1 */ 203 background-color: transparent; /* 2 */ 204 background-image: none; /* 2 */ ··· 242 2. Correct the outline style in Safari. 243 */ 244 245 - [type='search'] { 246 -webkit-appearance: textfield; /* 1 */ 247 outline-offset: -2px; /* 2 */ 248 }
··· 15 16 ::before, 17 ::after { 18 + --un-content: ""; 19 } 20 21 /* ··· 34 -webkit-text-size-adjust: 100%; /* 2 */ 35 -moz-tab-size: 4; /* 3 */ 36 tab-size: 4; /* 3 */ 37 + font-family: 38 + ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", 39 + "Noto Color Emoji"; /* 4 */ 40 font-feature-settings: normal; /* 5 */ 41 font-variation-settings: normal; /* 6 */ 42 -webkit-tap-highlight-color: transparent; /* 7 */ ··· 115 kbd, 116 samp, 117 pre { 118 + font-family: 119 + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", 120 + monospace; /* 1 */ 121 font-feature-settings: normal; /* 2 */ 122 font-variation-settings: normal; /* 3 */ 123 font-size: 1em; /* 4 */ ··· 200 */ 201 202 button, 203 + [type="button"], 204 + [type="reset"], 205 + [type="submit"] { 206 -webkit-appearance: button; /* 1 */ 207 background-color: transparent; /* 2 */ 208 background-image: none; /* 2 */ ··· 246 2. Correct the outline style in Safari. 247 */ 248 249 + [type="search"] { 250 -webkit-appearance: textfield; /* 1 */ 251 outline-offset: -2px; /* 2 */ 252 }
+5 -18
src/utils/api.ts
··· 90 cursor?: string, 91 limit?: number, 92 ) => { 93 - const url = new URL( 94 - localStorage.constellationHost || "https://constellation.microcosm.blue", 95 - ); 96 url.pathname = endpoint; 97 url.searchParams.set("target", target); 98 if (collection) { 99 - if (!path) 100 - throw new Error("collection and path must either both be set or neither"); 101 url.searchParams.set("collection", collection); 102 url.searchParams.set("path", path); 103 } else { 104 - if (path) 105 - throw new Error("collection and path must either both be set or neither"); 106 } 107 if (limit) url.searchParams.set("limit", `${limit}`); 108 if (cursor) url.searchParams.set("cursor", `${cursor}`); ··· 111 return await res.json(); 112 }; 113 114 - const getAllBacklinks = (target: string) => 115 - getConstellation("/links/all", target); 116 117 const getRecordBacklinks = ( 118 target: string, ··· 128 path: string, 129 cursor?: string, 130 limit?: number, 131 - ) => 132 - getConstellation( 133 - "/links/distinct-dids", 134 - target, 135 - collection, 136 - path, 137 - cursor, 138 - limit || 100, 139 - ); 140 141 export { 142 didDocCache,
··· 90 cursor?: string, 91 limit?: number, 92 ) => { 93 + const url = new URL(localStorage.constellationHost || "https://constellation.microcosm.blue"); 94 url.pathname = endpoint; 95 url.searchParams.set("target", target); 96 if (collection) { 97 + if (!path) throw new Error("collection and path must either both be set or neither"); 98 url.searchParams.set("collection", collection); 99 url.searchParams.set("path", path); 100 } else { 101 + if (path) throw new Error("collection and path must either both be set or neither"); 102 } 103 if (limit) url.searchParams.set("limit", `${limit}`); 104 if (cursor) url.searchParams.set("cursor", `${cursor}`); ··· 107 return await res.json(); 108 }; 109 110 + const getAllBacklinks = (target: string) => getConstellation("/links/all", target); 111 112 const getRecordBacklinks = ( 113 target: string, ··· 123 path: string, 124 cursor?: string, 125 limit?: number, 126 + ) => getConstellation("/links/distinct-dids", target, collection, path, cursor, limit || 100); 127 128 export { 129 didDocCache,
+1 -3
src/utils/firehose.ts
··· 196 return this; 197 } 198 199 - private parseMessage( 200 - data: ArrayBuffer, 201 - ): ParsedCommit | { $type: string; seq?: number } { 202 const [header, remainder] = decodeFirst(new Uint8Array(data)); 203 const [body, remainder2] = decodeFirst(remainder); 204 if (remainder2.length > 0) {
··· 196 return this; 197 } 198 199 + private parseMessage(data: ArrayBuffer): ParsedCommit | { $type: string; seq?: number } { 200 const [header, remainder] = decodeFirst(new Uint8Array(data)); 201 const [body, remainder2] = decodeFirst(remainder); 202 if (remainder2.length > 0) {
+2 -8
src/utils/types/at-uri.ts
··· 16 17 const isDid = (input: unknown): input is Did => { 18 return ( 19 - typeof input === "string" && 20 - input.length >= 7 && 21 - input.length <= 2048 && 22 - DID_RE.test(input) 23 ); 24 }; 25 26 const isNsid = (input: unknown): input is Nsid => { 27 return ( 28 - typeof input === "string" && 29 - input.length >= 5 && 30 - input.length <= 317 && 31 - NSID_RE.test(input) 32 ); 33 }; 34
··· 16 17 const isDid = (input: unknown): input is Did => { 18 return ( 19 + typeof input === "string" && input.length >= 7 && input.length <= 2048 && DID_RE.test(input) 20 ); 21 }; 22 23 const isNsid = (input: unknown): input is Nsid => { 24 return ( 25 + typeof input === "string" && input.length >= 5 && input.length <= 317 && NSID_RE.test(input) 26 ); 27 }; 28
+4 -15
src/utils/verify.ts
··· 4 import * as CAR from "@atcute/car"; 5 import * as CBOR from "@atcute/cbor"; 6 import * as CID from "@atcute/cid"; 7 - import { 8 - type FoundPublicKey, 9 - getPublicKeyFromDidController, 10 - verifySig, 11 - } from "@atcute/crypto"; 12 - import { 13 - type DidDocument, 14 - getAtprotoVerificationMaterial, 15 - } from "@atcute/identity"; 16 import { toSha256 } from "@atcute/uint8array"; 17 18 import { type AddressedAtUri, parseAddressedAtUri } from "./types/at-uri"; ··· 34 didDoc: DidDocument; 35 } 36 37 - export const verifyRecord = async ( 38 - opts: VerifyOptions, 39 - ): Promise<VerifyResult> => { 40 const errors: VerifyError[] = []; 41 42 // verify cid can be parsed ··· 125 const cidString = CID.toString(entry.cid); 126 127 // Verify that `bytes` matches its associated CID 128 - const expectedCid = CID.toString( 129 - await CID.create(entry.cid.codec as 85 | 113, entry.bytes), 130 - ); 131 if (cidString !== expectedCid) { 132 errors.push({ 133 message: `cid does not match bytes`,
··· 4 import * as CAR from "@atcute/car"; 5 import * as CBOR from "@atcute/cbor"; 6 import * as CID from "@atcute/cid"; 7 + import { type FoundPublicKey, getPublicKeyFromDidController, verifySig } from "@atcute/crypto"; 8 + import { type DidDocument, getAtprotoVerificationMaterial } from "@atcute/identity"; 9 import { toSha256 } from "@atcute/uint8array"; 10 11 import { type AddressedAtUri, parseAddressedAtUri } from "./types/at-uri"; ··· 27 didDoc: DidDocument; 28 } 29 30 + export const verifyRecord = async (opts: VerifyOptions): Promise<VerifyResult> => { 31 const errors: VerifyError[] = []; 32 33 // verify cid can be parsed ··· 116 const cidString = CID.toString(entry.cid); 117 118 // Verify that `bytes` matches its associated CID 119 + const expectedCid = CID.toString(await CID.create(entry.cid.codec as 85 | 113, entry.bytes)); 120 if (cidString !== expectedCid) { 121 errors.push({ 122 message: `cid does not match bytes`,
+1 -2
src/views/blob.tsx
··· 25 const fetchBlobs = async (): Promise<string[]> => { 26 if (!did.startsWith("did:")) did = await resolveHandle(params.repo); 27 if (!pds) pds = await resolvePDS(did); 28 - if (!rpc) 29 - rpc = new XRPC({ handler: new CredentialManager({ service: pds }) }); 30 const res = await listBlobs(did, cursor()); 31 setCursor(res.data.cids.length < 1000 ? undefined : res.data.cursor); 32 setBlobs(blobs()?.concat(res.data.cids) ?? res.data.cids);
··· 25 const fetchBlobs = async (): Promise<string[]> => { 26 if (!did.startsWith("did:")) did = await resolveHandle(params.repo); 27 if (!pds) pds = await resolvePDS(did); 28 + if (!rpc) rpc = new XRPC({ handler: new CredentialManager({ service: pds }) }); 29 const res = await listBlobs(did, cursor()); 30 setCursor(res.data.cids.length < 1000 ? undefined : res.data.cursor); 31 setBlobs(blobs()?.concat(res.data.cids) ?? res.data.cids);
+7 -18
src/views/collection.tsx
··· 50 onmouseleave={() => setHoverRk(undefined)} 51 > 52 <span class="text-lightblue-500">{props.record.rkey}</span> 53 - <Show 54 - when={props.record.timestamp && props.record.timestamp <= Date.now()} 55 - > 56 <span class="ml-2 text-xs text-neutral-500 dark:text-neutral-400"> 57 {localDateFromTimestamp(props.record.timestamp!)} 58 </span> ··· 121 const fetchRecords = async () => { 122 if (!did.startsWith("did:")) did = await resolveHandle(params.repo); 123 if (!pds) pds = await resolvePDS(did); 124 - if (!rpc) 125 - rpc = new XRPC({ handler: new CredentialManager({ service: pds }) }); 126 const res = await listRecords(did, params.collection, cursor()); 127 setCursor(res.data.records.length < 100 ? undefined : res.data.cursor); 128 const tmpRecords: AtprotoRecord[] = []; ··· 131 tmpRecords.push({ 132 rkey: rkey, 133 record: record, 134 - timestamp: 135 - TID.validate(rkey) ? TID.parse(rkey).timestamp / 1000 : undefined, 136 toDelete: false, 137 }); 138 }); ··· 183 setRecords( 184 records 185 .map((record, index) => 186 - JSON.stringify(record.record.value).includes(filter() ?? "") ? 187 - index 188 - : undefined, 189 ) 190 .filter((i) => i !== undefined), 191 "toDelete", ··· 271 > 272 <div class="dark:bg-dark-400 rounded-md border border-neutral-500 bg-slate-100 p-3 text-slate-900 dark:text-slate-100"> 273 <h3 class="text-lg font-bold"> 274 - Delete {records.filter((rec) => rec.toDelete).length}{" "} 275 - records? 276 </h3> 277 <div class="mt-2 inline-flex gap-2"> 278 <button ··· 334 <div class="flex flex-col font-mono"> 335 <For 336 each={records.filter((rec) => 337 - filter() ? 338 - JSON.stringify(rec.record.value).includes(filter()!) 339 - : true, 340 )} 341 > 342 {(record, index) => ( ··· 349 <input 350 type="checkbox" 351 checked={record.toDelete} 352 - onchange={(e) => 353 - setRecords(index(), "toDelete", e.currentTarget.checked) 354 - } 355 /> 356 <RecordLink record={record} index={index()} /> 357 </label>
··· 50 onmouseleave={() => setHoverRk(undefined)} 51 > 52 <span class="text-lightblue-500">{props.record.rkey}</span> 53 + <Show when={props.record.timestamp && props.record.timestamp <= Date.now()}> 54 <span class="ml-2 text-xs text-neutral-500 dark:text-neutral-400"> 55 {localDateFromTimestamp(props.record.timestamp!)} 56 </span> ··· 119 const fetchRecords = async () => { 120 if (!did.startsWith("did:")) did = await resolveHandle(params.repo); 121 if (!pds) pds = await resolvePDS(did); 122 + if (!rpc) rpc = new XRPC({ handler: new CredentialManager({ service: pds }) }); 123 const res = await listRecords(did, params.collection, cursor()); 124 setCursor(res.data.records.length < 100 ? undefined : res.data.cursor); 125 const tmpRecords: AtprotoRecord[] = []; ··· 128 tmpRecords.push({ 129 rkey: rkey, 130 record: record, 131 + timestamp: TID.validate(rkey) ? TID.parse(rkey).timestamp / 1000 : undefined, 132 toDelete: false, 133 }); 134 }); ··· 179 setRecords( 180 records 181 .map((record, index) => 182 + JSON.stringify(record.record.value).includes(filter() ?? "") ? index : undefined, 183 ) 184 .filter((i) => i !== undefined), 185 "toDelete", ··· 265 > 266 <div class="dark:bg-dark-400 rounded-md border border-neutral-500 bg-slate-100 p-3 text-slate-900 dark:text-slate-100"> 267 <h3 class="text-lg font-bold"> 268 + Delete {records.filter((rec) => rec.toDelete).length} records? 269 </h3> 270 <div class="mt-2 inline-flex gap-2"> 271 <button ··· 327 <div class="flex flex-col font-mono"> 328 <For 329 each={records.filter((rec) => 330 + filter() ? JSON.stringify(rec.record.value).includes(filter()!) : true, 331 )} 332 > 333 {(record, index) => ( ··· 340 <input 341 type="checkbox" 342 checked={record.toDelete} 343 + onchange={(e) => setRecords(index(), "toDelete", e.currentTarget.checked)} 344 /> 345 <RecordLink record={record} index={index()} /> 346 </label>
+2 -9
src/views/home.tsx
··· 6 <div class="mb-2"> 7 <p> 8 Browse the public data on{" "} 9 - <a 10 - class="text-lightblue-500 hover:underline" 11 - href="https://atproto.com" 12 - target="_blank" 13 - > 14 AT Protocol 15 </a> 16 . ··· 41 <div> 42 <span class="font-semibold text-orange-400">PDS</span> 43 <div> 44 - <A 45 - href="/pyramid-activation.today" 46 - class="text-lightblue-500 hover:underline" 47 - > 48 https://pyramid-activation.today 49 </A> 50 </div>
··· 6 <div class="mb-2"> 7 <p> 8 Browse the public data on{" "} 9 + <a class="text-lightblue-500 hover:underline" href="https://atproto.com" target="_blank"> 10 AT Protocol 11 </a> 12 . ··· 37 <div> 38 <span class="font-semibold text-orange-400">PDS</span> 39 <div> 40 + <A href="/pyramid-activation.today" class="text-lightblue-500 hover:underline"> 41 https://pyramid-activation.today 42 </A> 43 </div>
+5 -15
src/views/labels.tsx
··· 24 }); 25 26 const fetchLabels = async () => { 27 - const uriPatterns = ( 28 - document.getElementById("patterns") as HTMLInputElement 29 - ).value; 30 if (!uriPatterns) return; 31 const res = await rpc.get("com.atproto.label.queryLabels", { 32 params: { ··· 46 setLabels([]); 47 setCursor(""); 48 setSearchParams({ 49 - uriPatterns: (document.getElementById("patterns") as HTMLInputElement) 50 - .value, 51 }); 52 refetch(); 53 }; 54 55 const filterLabels = () => { 56 - const newFilter = labels().filter((label) => 57 - filter() ? filter() === label.val : true, 58 - ); 59 setLabelCount(newFilter.length); 60 return newFilter; 61 }; 62 63 return ( 64 <> 65 - <form 66 - class="mt-3 flex flex-col items-center gap-y-1" 67 - onsubmit={(e) => e.preventDefault()} 68 - > 69 <div class="w-full"> 70 <label for="patterns" class="ml-0.5 text-sm"> 71 URI Patterns (comma-separated) ··· 191 </For> 192 </div> 193 </Show> 194 - <Show 195 - when={!labels().length && !response.loading && searchParams.uriPatterns} 196 - > 197 <div class="mt-2">No results</div> 198 </Show> 199 </>
··· 24 }); 25 26 const fetchLabels = async () => { 27 + const uriPatterns = (document.getElementById("patterns") as HTMLInputElement).value; 28 if (!uriPatterns) return; 29 const res = await rpc.get("com.atproto.label.queryLabels", { 30 params: { ··· 44 setLabels([]); 45 setCursor(""); 46 setSearchParams({ 47 + uriPatterns: (document.getElementById("patterns") as HTMLInputElement).value, 48 }); 49 refetch(); 50 }; 51 52 const filterLabels = () => { 53 + const newFilter = labels().filter((label) => (filter() ? filter() === label.val : true)); 54 setLabelCount(newFilter.length); 55 return newFilter; 56 }; 57 58 return ( 59 <> 60 + <form class="mt-3 flex flex-col items-center gap-y-1" onsubmit={(e) => e.preventDefault()}> 61 <div class="w-full"> 62 <label for="patterns" class="ml-0.5 text-sm"> 63 URI Patterns (comma-separated) ··· 183 </For> 184 </div> 185 </Show> 186 + <Show when={!labels().length && !response.loading && searchParams.uriPatterns}> 187 <div class="mt-2">No results</div> 188 </Show> 189 </>
+4 -12
src/views/pds.tsx
··· 11 const [version, setVersion] = createSignal<string>(); 12 const [cursor, setCursor] = createSignal<string>(); 13 setPDS(params.pds); 14 - const pds = 15 - params.pds.startsWith("localhost") ? 16 - `http://${params.pds}` 17 - : `https://${params.pds}`; 18 const rpc = new XRPC({ handler: new CredentialManager({ service: pds }) }); 19 20 const listRepos = async (cursor: string | undefined) => ··· 44 <div class="mt-3 flex flex-col"> 45 <Show when={version()}> 46 <div class="flex max-w-[21rem] gap-1"> 47 - <span class="font-semibold text-stone-600 dark:text-stone-400"> 48 - Version 49 - </span> 50 <span class="break-anywhere">{version()}</span> 51 </div> 52 </Show> 53 - <p class="w-full font-semibold text-stone-600 dark:text-stone-400"> 54 - Repositories 55 - </p> 56 <For each={repos()}> 57 {(repo) => ( 58 <A ··· 60 classList={{ 61 "w-full flex font-mono relative": true, 62 "text-lightblue-500": repo.active, 63 - "text-gray-300 absolute -left-5 dark:text-gray-600": 64 - !repo.active, 65 }} 66 > 67 <Show when={!repo.active}>
··· 11 const [version, setVersion] = createSignal<string>(); 12 const [cursor, setCursor] = createSignal<string>(); 13 setPDS(params.pds); 14 + const pds = params.pds.startsWith("localhost") ? `http://${params.pds}` : `https://${params.pds}`; 15 const rpc = new XRPC({ handler: new CredentialManager({ service: pds }) }); 16 17 const listRepos = async (cursor: string | undefined) => ··· 41 <div class="mt-3 flex flex-col"> 42 <Show when={version()}> 43 <div class="flex max-w-[21rem] gap-1"> 44 + <span class="font-semibold text-stone-600 dark:text-stone-400">Version</span> 45 <span class="break-anywhere">{version()}</span> 46 </div> 47 </Show> 48 + <p class="w-full font-semibold text-stone-600 dark:text-stone-400">Repositories</p> 49 <For each={repos()}> 50 {(repo) => ( 51 <A ··· 53 classList={{ 54 "w-full flex font-mono relative": true, 55 "text-lightblue-500": repo.active, 56 + "text-gray-300 absolute -left-5 dark:text-gray-600": !repo.active, 57 }} 58 > 59 <Show when={!repo.active}>
+12 -42
src/views/record.tsx
··· 13 import { setCID, setValidRecord, validRecord } from "../components/navbar.jsx"; 14 import { theme } from "../components/settings.jsx"; 15 16 - import { 17 - didDocCache, 18 - getAllBacklinks, 19 - LinkData, 20 - resolveHandle, 21 - resolvePDS, 22 - } from "../utils/api.js"; 23 import { AtUri, uriTemplates } from "../utils/templates.js"; 24 import { verifyRecord } from "../utils/verify.js"; 25 ··· 79 80 if (errors.length > 0) { 81 console.warn(errors); 82 - setNotice( 83 - `Invalid record: ${errors.map((e) => e.message).join("\n")}`, 84 - ); 85 } 86 setValidRecord(errors.length === 0); 87 } catch (err) { ··· 203 <div class="i-line-md-loading-twotone-loop mt-3 text-xl" /> 204 </Show> 205 <Show when={validRecord() === false}> 206 - <div class="w-20rem mb-2 mt-3 break-words text-red-500 dark:text-red-400"> 207 - {notice()} 208 - </div> 209 </Show> 210 <Show when={record()}> 211 <div class="my-4 flex w-full justify-center gap-x-2"> ··· 221 target="_blank" 222 href={externalLink()?.link} 223 > 224 - {externalLink()?.label}{" "} 225 - <div class="i-tabler-external-link text-sm" /> 226 </a> 227 </Show> 228 - <Show 229 - when={loginState() && agent.sub === record()?.uri.split("/")[2]} 230 - > 231 <Show when={openEdit()}> 232 <dialog 233 ref={setModal} ··· 252 </div> 253 <Editor theme={theme().color} model={model!} /> 254 <div class="mt-2 flex flex-col gap-2"> 255 - <div class="text-red-500 dark:text-red-400"> 256 - {editNotice()} 257 - </div> 258 <div class="flex items-center justify-end gap-2"> 259 <div class="flex items-center gap-1"> 260 - <input 261 - id="recreate" 262 - class="size-4" 263 - name="recreate" 264 - type="checkbox" 265 - /> 266 <label for="recreate" class="select-none"> 267 Recreate record 268 </label> ··· 288 </Show> 289 <button 290 onclick={() => { 291 - model = editor.createModel( 292 - JSON.stringify(record()?.value, null, 2), 293 - "json", 294 - ); 295 setOpenEdit(true); 296 }} 297 class="dark:bg-dark-700 dark:hover:bg-dark-800 rounded-lg border border-slate-400 bg-white px-2.5 py-1.5 text-sm font-bold hover:bg-slate-100 focus:outline-none focus:ring-1 focus:ring-slate-700 dark:focus:ring-slate-300" ··· 335 </div> 336 <div 337 classList={{ 338 - "break-anywhere mb-2 whitespace-pre-wrap pb-3 font-mono text-sm sm:text-base": 339 - true, 340 "border-b border-neutral-500": !!backlinks(), 341 }} 342 > 343 <Show when={!JSONSyntax()}> 344 - <JSONValue 345 - data={record()?.value as any} 346 - repo={record()!.uri.split("/")[2]} 347 - /> 348 </Show> 349 <Show when={JSONSyntax()}> 350 <span 351 innerHTML={syntaxHighlight( 352 JSON.stringify(record()?.value, null, 2).replace( 353 /[\u007F-\uFFFF]/g, 354 - (chr) => 355 - "\\u" + ("0000" + chr.charCodeAt(0).toString(16)).slice(-4), 356 ), 357 )} 358 ></span> 359 </Show> 360 </div> 361 <Show when={backlinks()}> 362 - {(backlinks) => ( 363 - <Backlinks links={backlinks().links} target={backlinks().target} /> 364 - )} 365 </Show> 366 </Show> 367 </>
··· 13 import { setCID, setValidRecord, validRecord } from "../components/navbar.jsx"; 14 import { theme } from "../components/settings.jsx"; 15 16 + import { didDocCache, getAllBacklinks, LinkData, resolveHandle, resolvePDS } from "../utils/api.js"; 17 import { AtUri, uriTemplates } from "../utils/templates.js"; 18 import { verifyRecord } from "../utils/verify.js"; 19 ··· 73 74 if (errors.length > 0) { 75 console.warn(errors); 76 + setNotice(`Invalid record: ${errors.map((e) => e.message).join("\n")}`); 77 } 78 setValidRecord(errors.length === 0); 79 } catch (err) { ··· 195 <div class="i-line-md-loading-twotone-loop mt-3 text-xl" /> 196 </Show> 197 <Show when={validRecord() === false}> 198 + <div class="w-20rem mb-2 mt-3 break-words text-red-500 dark:text-red-400">{notice()}</div> 199 </Show> 200 <Show when={record()}> 201 <div class="my-4 flex w-full justify-center gap-x-2"> ··· 211 target="_blank" 212 href={externalLink()?.link} 213 > 214 + {externalLink()?.label} <div class="i-tabler-external-link text-sm" /> 215 </a> 216 </Show> 217 + <Show when={loginState() && agent.sub === record()?.uri.split("/")[2]}> 218 <Show when={openEdit()}> 219 <dialog 220 ref={setModal} ··· 239 </div> 240 <Editor theme={theme().color} model={model!} /> 241 <div class="mt-2 flex flex-col gap-2"> 242 + <div class="text-red-500 dark:text-red-400">{editNotice()}</div> 243 <div class="flex items-center justify-end gap-2"> 244 <div class="flex items-center gap-1"> 245 + <input id="recreate" class="size-4" name="recreate" type="checkbox" /> 246 <label for="recreate" class="select-none"> 247 Recreate record 248 </label> ··· 268 </Show> 269 <button 270 onclick={() => { 271 + model = editor.createModel(JSON.stringify(record()?.value, null, 2), "json"); 272 setOpenEdit(true); 273 }} 274 class="dark:bg-dark-700 dark:hover:bg-dark-800 rounded-lg border border-slate-400 bg-white px-2.5 py-1.5 text-sm font-bold hover:bg-slate-100 focus:outline-none focus:ring-1 focus:ring-slate-700 dark:focus:ring-slate-300" ··· 312 </div> 313 <div 314 classList={{ 315 + "break-anywhere mb-2 whitespace-pre-wrap pb-3 font-mono text-sm sm:text-base": true, 316 "border-b border-neutral-500": !!backlinks(), 317 }} 318 > 319 <Show when={!JSONSyntax()}> 320 + <JSONValue data={record()?.value as any} repo={record()!.uri.split("/")[2]} /> 321 </Show> 322 <Show when={JSONSyntax()}> 323 <span 324 innerHTML={syntaxHighlight( 325 JSON.stringify(record()?.value, null, 2).replace( 326 /[\u007F-\uFFFF]/g, 327 + (chr) => "\\u" + ("0000" + chr.charCodeAt(0).toString(16)).slice(-4), 328 ), 329 )} 330 ></span> 331 </Show> 332 </div> 333 <Show when={backlinks()}> 334 + {(backlinks) => <Backlinks links={backlinks().links} target={backlinks().target} />} 335 </Show> 336 </Show> 337 </>
+12 -38
src/views/repo.tsx
··· 1 import { createSignal, For, Show, createResource } from "solid-js"; 2 import { CredentialManager, XRPC } from "@atcute/client"; 3 import { A, query, useParams } from "@solidjs/router"; 4 - import { 5 - didDocCache, 6 - getAllBacklinks, 7 - LinkData, 8 - resolveHandle, 9 - resolvePDS, 10 - } from "../utils/api.js"; 11 import { DidDocument } from "@atcute/client/utils/did"; 12 import { Backlinks } from "../components/backlinks.jsx"; 13 ··· 24 let did = params.repo; 25 26 const describeRepo = query( 27 - (repo: string) => 28 - rpc.get("com.atproto.repo.describeRepo", { params: { repo: repo } }), 29 "describeRepo", 30 ); 31 ··· 51 const downloadRepo = async () => { 52 try { 53 setDownloading(true); 54 - const response = await fetch( 55 - `${pds}/xrpc/com.atproto.sync.getRepo?did=${did}`, 56 - ); 57 if (!response.ok) { 58 throw new Error(`HTTP error status: ${response.status}`); 59 } ··· 78 <Show when={repo()}> 79 <div class="mt-3 flex w-[21rem] flex-col gap-2 break-words"> 80 <div class="flex flex-col border-b border-neutral-500 pb-2 font-mono"> 81 - <p class="font-sans font-semibold text-stone-600 dark:text-stone-400"> 82 - Collections 83 - </p> 84 <For each={repo()?.collections}> 85 {(collection) => ( 86 <A ··· 96 {(didDocument) => ( 97 <div class="flex flex-col gap-y-1"> 98 <div> 99 - <span class="font-semibold text-stone-600 dark:text-stone-400"> 100 - ID{" "} 101 - </span> 102 <span>{didDocument().id}</span> 103 </div> 104 <div> 105 - <p class="font-semibold text-stone-600 dark:text-stone-400"> 106 - Identities 107 - </p> 108 <ul class="ml-3"> 109 - <For each={didDocument().alsoKnownAs}> 110 - {(alias) => <li>{alias}</li>} 111 - </For> 112 </ul> 113 </div> 114 <div> 115 - <p class="font-semibold text-stone-600 dark:text-stone-400"> 116 - Services 117 - </p> 118 <ul class="ml-3"> 119 <For each={didDocument().service}> 120 {(service) => ( ··· 133 </ul> 134 </div> 135 <div> 136 - <p class="font-semibold text-stone-600 dark:text-stone-400"> 137 - Verification methods 138 - </p> 139 <ul class="ml-3"> 140 <For each={didDocument().verificationMethod}> 141 {(verif) => ( ··· 156 } 157 target="_blank" 158 > 159 - DID document{" "} 160 - <div class="i-tabler-external-link ml-0.5 text-xs" /> 161 </a> 162 <Show when={repo()?.did.startsWith("did:plc")}> 163 <a ··· 165 href={`https://boat.kelinci.net/plc-oplogs?q=${repo()?.did}`} 166 target="_blank" 167 > 168 - PLC operation logs{" "} 169 - <div class="i-tabler-external-link ml-0.5 text-xs" /> 170 </a> 171 </Show> 172 <div class="flex items-center gap-1"> ··· 183 <Show when={backlinks()}> 184 {(backlinks) => ( 185 <div class="mt-2 border-t border-neutral-500 pt-2"> 186 - <Backlinks 187 - links={backlinks().links} 188 - target={backlinks().target} 189 - /> 190 </div> 191 )} 192 </Show>
··· 1 import { createSignal, For, Show, createResource } from "solid-js"; 2 import { CredentialManager, XRPC } from "@atcute/client"; 3 import { A, query, useParams } from "@solidjs/router"; 4 + import { didDocCache, getAllBacklinks, LinkData, resolveHandle, resolvePDS } from "../utils/api.js"; 5 import { DidDocument } from "@atcute/client/utils/did"; 6 import { Backlinks } from "../components/backlinks.jsx"; 7 ··· 18 let did = params.repo; 19 20 const describeRepo = query( 21 + (repo: string) => rpc.get("com.atproto.repo.describeRepo", { params: { repo: repo } }), 22 "describeRepo", 23 ); 24 ··· 44 const downloadRepo = async () => { 45 try { 46 setDownloading(true); 47 + const response = await fetch(`${pds}/xrpc/com.atproto.sync.getRepo?did=${did}`); 48 if (!response.ok) { 49 throw new Error(`HTTP error status: ${response.status}`); 50 } ··· 69 <Show when={repo()}> 70 <div class="mt-3 flex w-[21rem] flex-col gap-2 break-words"> 71 <div class="flex flex-col border-b border-neutral-500 pb-2 font-mono"> 72 + <p class="font-sans font-semibold text-stone-600 dark:text-stone-400">Collections</p> 73 <For each={repo()?.collections}> 74 {(collection) => ( 75 <A ··· 85 {(didDocument) => ( 86 <div class="flex flex-col gap-y-1"> 87 <div> 88 + <span class="font-semibold text-stone-600 dark:text-stone-400">ID </span> 89 <span>{didDocument().id}</span> 90 </div> 91 <div> 92 + <p class="font-semibold text-stone-600 dark:text-stone-400">Identities</p> 93 <ul class="ml-3"> 94 + <For each={didDocument().alsoKnownAs}>{(alias) => <li>{alias}</li>}</For> 95 </ul> 96 </div> 97 <div> 98 + <p class="font-semibold text-stone-600 dark:text-stone-400">Services</p> 99 <ul class="ml-3"> 100 <For each={didDocument().service}> 101 {(service) => ( ··· 114 </ul> 115 </div> 116 <div> 117 + <p class="font-semibold text-stone-600 dark:text-stone-400">Verification methods</p> 118 <ul class="ml-3"> 119 <For each={didDocument().verificationMethod}> 120 {(verif) => ( ··· 135 } 136 target="_blank" 137 > 138 + DID document <div class="i-tabler-external-link ml-0.5 text-xs" /> 139 </a> 140 <Show when={repo()?.did.startsWith("did:plc")}> 141 <a ··· 143 href={`https://boat.kelinci.net/plc-oplogs?q=${repo()?.did}`} 144 target="_blank" 145 > 146 + PLC operation logs <div class="i-tabler-external-link ml-0.5 text-xs" /> 147 </a> 148 </Show> 149 <div class="flex items-center gap-1"> ··· 160 <Show when={backlinks()}> 161 {(backlinks) => ( 162 <div class="mt-2 border-t border-neutral-500 pt-2"> 163 + <Backlinks links={backlinks().links} target={backlinks().target} /> 164 </div> 165 )} 166 </Show>
+9 -25
src/views/stream.tsx
··· 14 const [searchParams, setSearchParams] = useSearchParams(); 15 const [parameters, setParameters] = createSignal<Parameter[]>([]); 16 const streamType = 17 - useLocation().pathname === "/firehose" ? 18 - StreamType.FIREHOSE 19 - : StreamType.JETSTREAM; 20 21 const [records, setRecords] = createSignal<Array<any>>([]); 22 const [connected, setConnected] = createSignal(false); ··· 37 let url = ""; 38 if (streamType === StreamType.JETSTREAM) { 39 url = 40 - formData.get("instance")?.toString() ?? 41 - "wss://jetstream1.us-east.bsky.network/subscribe"; 42 url = url.concat("?"); 43 } else { 44 url = formData.get("instance")?.toString() ?? "wss://bsky.network"; ··· 46 47 const collections = formData.get("collections")?.toString().split(","); 48 collections?.forEach((collection) => { 49 - if (collection.length) 50 - url = url.concat(`wantedCollections=${collection}&`); 51 }); 52 53 const dids = formData.get("dids")?.toString().split(","); ··· 131 132 onMount(async () => { 133 const formData = new FormData(); 134 - if (searchParams.instance) 135 - formData.append("instance", searchParams.instance.toString()); 136 if (searchParams.collections) 137 formData.append("collections", searchParams.collections.toString()); 138 - if (searchParams.dids) 139 - formData.append("dids", searchParams.dids.toString()); 140 - if (searchParams.cursor) 141 - formData.append("cursor", searchParams.cursor.toString()); 142 - if (searchParams.allEvents) 143 - formData.append("allEvents", searchParams.allEvents.toString()); 144 if (searchParams.instance) connectSocket(formData); 145 }); 146 ··· 149 return ( 150 <div class="mt-4 flex flex-col items-center gap-y-3"> 151 <div class="flex divide-x-2 text-lg font-bold"> 152 - <A 153 - class="pr-2" 154 - inactiveClass="text-lightblue-500 hover:underline" 155 - href="/jetstream" 156 - > 157 Jetstream 158 </A> 159 - <A 160 - class="pl-2" 161 - inactiveClass="text-lightblue-500 hover:underline" 162 - href="/firehose" 163 - > 164 Firehose 165 </A> 166 </div>
··· 14 const [searchParams, setSearchParams] = useSearchParams(); 15 const [parameters, setParameters] = createSignal<Parameter[]>([]); 16 const streamType = 17 + useLocation().pathname === "/firehose" ? StreamType.FIREHOSE : StreamType.JETSTREAM; 18 19 const [records, setRecords] = createSignal<Array<any>>([]); 20 const [connected, setConnected] = createSignal(false); ··· 35 let url = ""; 36 if (streamType === StreamType.JETSTREAM) { 37 url = 38 + formData.get("instance")?.toString() ?? "wss://jetstream1.us-east.bsky.network/subscribe"; 39 url = url.concat("?"); 40 } else { 41 url = formData.get("instance")?.toString() ?? "wss://bsky.network"; ··· 43 44 const collections = formData.get("collections")?.toString().split(","); 45 collections?.forEach((collection) => { 46 + if (collection.length) url = url.concat(`wantedCollections=${collection}&`); 47 }); 48 49 const dids = formData.get("dids")?.toString().split(","); ··· 127 128 onMount(async () => { 129 const formData = new FormData(); 130 + if (searchParams.instance) formData.append("instance", searchParams.instance.toString()); 131 if (searchParams.collections) 132 formData.append("collections", searchParams.collections.toString()); 133 + if (searchParams.dids) formData.append("dids", searchParams.dids.toString()); 134 + if (searchParams.cursor) formData.append("cursor", searchParams.cursor.toString()); 135 + if (searchParams.allEvents) formData.append("allEvents", searchParams.allEvents.toString()); 136 if (searchParams.instance) connectSocket(formData); 137 }); 138 ··· 141 return ( 142 <div class="mt-4 flex flex-col items-center gap-y-3"> 143 <div class="flex divide-x-2 text-lg font-bold"> 144 + <A class="pr-2" inactiveClass="text-lightblue-500 hover:underline" href="/jetstream"> 145 Jetstream 146 </A> 147 + <A class="pl-2" inactiveClass="text-lightblue-500 hover:underline" href="/firehose"> 148 Firehose 149 </A> 150 </div>