forked from pdsls.dev/pdsls
atproto explorer

Compare changes

Choose any two refs to compare.

Changed files
+274 -121
src
+71 -76
src/components/backlinks.tsx
··· 24 24 const Backlinks = (props: { target: string }) => { 25 25 const fetchBacklinks = async () => { 26 26 const res = await getAllBacklinks(props.target); 27 - setBacklinks(linksBySource(res.links)); 28 - return res; 27 + return linksBySource(res.links); 29 28 }; 30 29 31 30 const [response] = createResource(fetchBacklinks); 32 - const [backlinks, setBacklinks] = createSignal<any>(); 33 31 34 32 const [show, setShow] = createSignal<{ 35 33 collection: string; ··· 38 36 } | null>(); 39 37 40 38 return ( 41 - <Show when={response()}> 42 - <div class="flex w-full flex-col gap-1 text-sm wrap-anywhere"> 43 - <For each={backlinks()}> 44 - {({ collection, path, counts }) => ( 39 + <div class="flex w-full flex-col gap-1 text-sm wrap-anywhere"> 40 + <Show when={response()?.length === 0}> 41 + <p>No backlinks found.</p> 42 + </Show> 43 + <For each={response()}> 44 + {({ collection, path, counts }) => ( 45 + <div> 45 46 <div> 46 - <div> 47 - <div title="Collection containing linking records" class="flex items-center gap-1"> 48 - <span class="iconify lucide--book-text shrink-0"></span> 49 - {collection} 50 - </div> 51 - <div title="Record path where the link is found" class="flex items-center gap-1"> 52 - <span class="iconify lucide--route shrink-0"></span> 53 - {path.slice(1)} 54 - </div> 47 + <div title="Collection containing linking records" class="flex items-center gap-1"> 48 + <span class="iconify lucide--book-text shrink-0"></span> 49 + {collection} 50 + </div> 51 + <div title="Record path where the link is found" class="flex items-center gap-1"> 52 + <span class="iconify lucide--route shrink-0"></span> 53 + {path.slice(1)} 55 54 </div> 56 - <div class="ml-4.5"> 57 - <p> 58 - <button 59 - class="text-blue-400 hover:underline active:underline" 60 - title="Show linking records" 61 - onclick={() => 62 - ( 63 - show()?.collection === collection && 64 - show()?.path === path && 65 - !show()?.showDids 66 - ) ? 67 - setShow(null) 68 - : setShow({ collection, path, showDids: false }) 69 - } 70 - > 71 - {counts.records} record{counts.records < 2 ? "" : "s"} 72 - </button> 73 - {" from "} 74 - <button 75 - class="text-blue-400 hover:underline active:underline" 76 - title="Show linking DIDs" 77 - onclick={() => 78 - ( 79 - show()?.collection === collection && 80 - show()?.path === path && 81 - show()?.showDids 82 - ) ? 83 - setShow(null) 84 - : setShow({ collection, path, showDids: true }) 85 - } 86 - > 87 - {counts.distinct_dids} DID 88 - {counts.distinct_dids < 2 ? "" : "s"} 89 - </button> 90 - </p> 91 - <Show when={show()?.collection === collection && show()?.path === path}> 92 - <Show when={show()?.showDids}> 93 - {/* putting this in the `dids` prop directly failed to re-render. idk how to solidjs. */} 94 - <p class="w-full font-semibold">Distinct identities</p> 95 - <BacklinkItems 96 - target={props.target} 97 - collection={collection} 98 - path={path} 99 - dids={true} 100 - /> 101 - </Show> 102 - <Show when={!show()?.showDids}> 103 - <p class="w-full font-semibold">Records</p> 104 - <BacklinkItems 105 - target={props.target} 106 - collection={collection} 107 - path={path} 108 - dids={false} 109 - /> 110 - </Show> 55 + </div> 56 + <div class="ml-4.5"> 57 + <p> 58 + <button 59 + class="text-blue-400 hover:underline active:underline" 60 + title="Show linking records" 61 + onclick={() => 62 + ( 63 + show()?.collection === collection && 64 + show()?.path === path && 65 + !show()?.showDids 66 + ) ? 67 + setShow(null) 68 + : setShow({ collection, path, showDids: false }) 69 + } 70 + > 71 + {counts.records} record{counts.records < 2 ? "" : "s"} 72 + </button> 73 + {" from "} 74 + <button 75 + class="text-blue-400 hover:underline active:underline" 76 + title="Show linking DIDs" 77 + onclick={() => 78 + show()?.collection === collection && show()?.path === path && show()?.showDids ? 79 + setShow(null) 80 + : setShow({ collection, path, showDids: true }) 81 + } 82 + > 83 + {counts.distinct_dids} DID 84 + {counts.distinct_dids < 2 ? "" : "s"} 85 + </button> 86 + </p> 87 + <Show when={show()?.collection === collection && show()?.path === path}> 88 + <Show when={show()?.showDids}> 89 + {/* putting this in the `dids` prop directly failed to re-render. idk how to solidjs. */} 90 + <p class="w-full font-semibold">Distinct identities</p> 91 + <BacklinkItems 92 + target={props.target} 93 + collection={collection} 94 + path={path} 95 + dids={true} 96 + /> 97 + </Show> 98 + <Show when={!show()?.showDids}> 99 + <p class="w-full font-semibold">Records</p> 100 + <BacklinkItems 101 + target={props.target} 102 + collection={collection} 103 + path={path} 104 + dids={false} 105 + /> 111 106 </Show> 112 - </div> 107 + </Show> 113 108 </div> 114 - )} 115 - </For> 116 - </div> 117 - </Show> 109 + </div> 110 + )} 111 + </For> 112 + </div> 118 113 ); 119 114 }; 120 115
+6 -3
src/components/create.tsx
··· 172 172 ></span> 173 173 <span>{props.create ? "Creating" : "Editing"} record</span> 174 174 </div> 175 - <button onclick={() => setOpenDialog(false)} class="flex items-center"> 176 - <span class="iconify lucide--x text-lg hover:text-neutral-500 dark:hover:text-neutral-400"></span> 175 + <button 176 + onclick={() => setOpenDialog(false)} 177 + class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 178 + > 179 + <span class="iconify lucide--x"></span> 177 180 </button> 178 181 </div> 179 182 <form ref={formRef} class="flex flex-col gap-y-2"> ··· 186 189 <TextInput 187 190 id="collection" 188 191 name="collection" 189 - placeholder="Optional (default: record type)" 192 + placeholder="Optional (default: $type)" 190 193 class="w-[15rem]" 191 194 /> 192 195 </div>
-4
src/components/navbar.tsx
··· 5 5 import Tooltip from "./tooltip"; 6 6 7 7 export const [pds, setPDS] = createSignal<string>(); 8 - export const [cid, setCID] = createSignal<string>(); 9 8 export const [isLabeler, setIsLabeler] = createSignal(false); 10 9 11 10 const NavBar = (props: { params: Params }) => { ··· 60 59 copyContent={`at://${props.params.repo}${props.params.collection ? `/${props.params.collection}` : ""}${props.params.rkey ? `/${props.params.rkey}` : ""}`} 61 60 label="Copy AT URI" 62 61 /> 63 - </Show> 64 - <Show when={props.params.rkey && cid()}> 65 - <CopyMenu copyContent={cid()!} label="Copy CID" /> 66 62 </Show> 67 63 </DropdownMenu> 68 64 </MenuProvider>
+66 -18
src/components/search.tsx
··· 2 2 import { A, useLocation, useNavigate } from "@solidjs/router"; 3 3 import { createResource, createSignal, For, onCleanup, onMount, Show } from "solid-js"; 4 4 import { isTouchDevice } from "../layout"; 5 + import { appHandleLink, appList, appName, AppUrl } from "../utils/app-urls"; 5 6 import { createDebouncedValue } from "../utils/hooks/debounced"; 7 + import { Modal } from "./modal"; 6 8 7 9 export const [showSearch, setShowSearch] = createSignal(false); 8 10 ··· 66 68 input = input.trim().replace(/^@/, ""); 67 69 if (!input.length) return; 68 70 setShowSearch(false); 69 - if (input === "me" && localStorage.getItem("lastSignedIn") !== null) { 70 - navigate(`/at://${localStorage.getItem("lastSignedIn")}`); 71 - } else if ( 72 - !input.startsWith("https://bsky.app/") && 73 - (input.startsWith("https://") || input.startsWith("http://")) 74 - ) { 75 - navigate(`/${input.replace("https://", "").replace("http://", "").replace("/", "")}`); 76 - } else if (search()?.length) { 71 + if (search()?.length) { 77 72 navigate(`/at://${search()![0].did}`); 73 + } else if (input.startsWith("https://") || input.startsWith("http://")) { 74 + const hostLength = input.indexOf("/", 8); 75 + const host = input.slice(0, hostLength).replace("https://", "").replace("http://", ""); 76 + 77 + if (!(host in appList)) { 78 + navigate(`/${input.replace("https://", "").replace("http://", "").replace("/", "")}`); 79 + } else { 80 + const app = appList[host as AppUrl]; 81 + const path = input.slice(hostLength + 1).split("/"); 82 + 83 + const uri = appHandleLink[app](path); 84 + navigate(`/${uri}`); 85 + } 78 86 } else { 79 - const uri = input 80 - .replace("at://", "") 81 - .replace("https://bsky.app/profile/", "") 82 - .replace("/post/", "/app.bsky.feed.post/"); 83 - const uriParts = uri.split("/"); 84 - navigate( 85 - `/at://${uriParts[0]}${uriParts.length > 1 ? `/${uriParts.slice(1).join("/")}` : ""}`, 86 - ); 87 + navigate(`/at://${input.replace("at://", "")}`); 87 88 } 88 - setShowSearch(false); 89 89 }; 90 90 91 91 return ( ··· 114 114 value={input() ?? ""} 115 115 onInput={(e) => setInput(e.currentTarget.value)} 116 116 /> 117 - <Show when={input()}> 117 + <Show when={input()} fallback={ListUrlsTooltip()}> 118 118 <button 119 119 type="button" 120 120 class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-600 dark:active:bg-neutral-500" ··· 144 144 </div> 145 145 </Show> 146 146 </form> 147 + ); 148 + }; 149 + 150 + const ListUrlsTooltip = () => { 151 + const [openList, setOpenList] = createSignal(false); 152 + 153 + let urls: Record<string, AppUrl[]> = {}; 154 + for (const [appUrl, appView] of Object.entries(appList)) { 155 + if (!urls[appView]) urls[appView] = [appUrl as AppUrl]; 156 + else urls[appView].push(appUrl as AppUrl); 157 + } 158 + 159 + return ( 160 + <> 161 + <Modal open={openList()} onClose={() => setOpenList(false)}> 162 + <div class="dark:bg-dark-300 dark:shadow-dark-800 absolute top-16 left-[50%] w-[26rem] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0"> 163 + <div class="mb-2 flex items-center gap-1 font-semibold"> 164 + <span class="iconify lucide--link"></span> 165 + <span>Supported URLs</span> 166 + </div> 167 + <div class="mb-2 text-sm text-neutral-500 dark:text-neutral-400"> 168 + Links that will be parsed automatically, as long as all the data necessary is on the 169 + URL. 170 + </div> 171 + <div class="flex flex-col gap-2 text-sm"> 172 + <For each={Object.entries(appName)}> 173 + {([appView, name]) => { 174 + return ( 175 + <div> 176 + <p class="font-semibold">{name}</p> 177 + <div class="grid grid-cols-2 gap-x-4 text-neutral-600 dark:text-neutral-400"> 178 + <For each={urls[appView]}>{(url) => <span>{url}</span>}</For> 179 + </div> 180 + </div> 181 + ); 182 + }} 183 + </For> 184 + </div> 185 + </div> 186 + </Modal> 187 + <button 188 + type="button" 189 + class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-600 dark:active:bg-neutral-500" 190 + onClick={() => setOpenList(true)} 191 + > 192 + <span class="iconify lucide--help-circle"></span> 193 + </button> 194 + </> 147 195 ); 148 196 }; 149 197
+119
src/utils/app-urls.ts
··· 1 + export type AppUrl = `${string}.${string}` | `localhost:${number}`; 2 + 3 + export enum App { 4 + Bluesky, 5 + Tangled, 6 + Whitewind, 7 + Frontpage, 8 + Pinksea, 9 + Linkat, 10 + } 11 + 12 + export const appName = { 13 + [App.Bluesky]: "Bluesky", 14 + [App.Tangled]: "Tangled", 15 + [App.Whitewind]: "Whitewind", 16 + [App.Frontpage]: "Frontpage", 17 + [App.Pinksea]: "Pinksea", 18 + [App.Linkat]: "Linkat", 19 + }; 20 + 21 + export const appList: Record<AppUrl, App> = { 22 + "localhost:19006": App.Bluesky, 23 + "blacksky.community": App.Bluesky, 24 + "bsky.app": App.Bluesky, 25 + "catsky.social": App.Bluesky, 26 + "deer.aylac.top": App.Bluesky, 27 + "deer-social-ayla.pages.dev": App.Bluesky, 28 + "deer.social": App.Bluesky, 29 + "main.bsky.dev": App.Bluesky, 30 + "social.daniela.lol": App.Bluesky, 31 + "tangled.org": App.Tangled, 32 + "whtwnd.com": App.Whitewind, 33 + "frontpage.fyi": App.Frontpage, 34 + "pinksea.art": App.Pinksea, 35 + "linkat.blue": App.Linkat, 36 + }; 37 + 38 + export const appHandleLink: Record<App, (url: string[]) => string> = { 39 + [App.Bluesky]: (path) => { 40 + const baseType = path[0]; 41 + const user = path[1]; 42 + 43 + if (baseType === "profile") { 44 + if (path[2]) { 45 + const type = path[2]; 46 + const rkey = path[3]; 47 + 48 + if (type === "post") { 49 + return `at://${user}/app.bsky.feed.post/${rkey}`; 50 + } else if (type === "list") { 51 + return `at://${user}/app.bsky.graph.list/${rkey}`; 52 + } else if (type === "feed") { 53 + return `at://${user}/app.bsky.feed.generator/${rkey}`; 54 + } else if (type === "follows") { 55 + return `at://${user}/app.bsky.graph.follow/${rkey}`; 56 + } 57 + } else { 58 + return `at://${user}`; 59 + } 60 + } else if (baseType === "starter-pack") { 61 + return `at://${user}/app.bsky.graph.starterpack/${path[2]}`; 62 + } 63 + return `at://${user}`; 64 + }, 65 + [App.Tangled]: (path) => { 66 + if (path[0] === "strings") { 67 + return `at://${path[1]}/sh.tangled.string/${path[2]}`; 68 + } 69 + 70 + let query: string | undefined; 71 + if (path[path.length - 1].includes("?")) { 72 + const split = path[path.length - 1].split("?"); 73 + query = split[1]; 74 + path[path.length - 1] = split[0]; 75 + } 76 + 77 + const user = path[0].replace("@", ""); 78 + 79 + if (path.length === 1) { 80 + if (query === "tab=repos") { 81 + return `at://${user}/sh.tangled.repo`; 82 + } else if (query === "tab=starred") { 83 + return `at://${user}/sh.tangled.feed.star`; 84 + } else if (query === "tab=strings") { 85 + return `at://${user}/sh.tangled.string`; 86 + } 87 + } else if (path.length === 2) { 88 + // no way to convert the repo name to an rkey afaik 89 + // same reason why there's nothing related to issues in here 90 + return `at://${user}/sh.tangled.repo`; 91 + } 92 + 93 + return `at://${user}`; 94 + }, 95 + [App.Whitewind]: (path) => { 96 + if (path.length === 2) { 97 + return `at://${path[0]}/com.whtwnd.blog.entry/${path[1]}`; 98 + } 99 + 100 + return `at://${path[0]}/com.whtwnd.blog.entry`; 101 + }, 102 + [App.Frontpage]: (path) => { 103 + if (path.length === 3) { 104 + return `at://${path[1]}/fyi.unravel.frontpage.post/${path[2]}`; 105 + } else if (path.length === 5) { 106 + return `at://${path[3]}/fyi.unravel.frontpage.comment/${path[4]}`; 107 + } 108 + 109 + return `at://${path[0]}`; 110 + }, 111 + [App.Pinksea]: (path) => { 112 + if (path.length === 2) { 113 + return `at://${path[0]}/com.shinolabs.pinksea.oekaki/${path[1]}`; 114 + } 115 + 116 + return `at://${path[0]}`; 117 + }, 118 + [App.Linkat]: (path) => `at://${path[0]}/blue.linkat.board/self`, 119 + };
+1 -1
src/views/collection.tsx
··· 41 41 42 42 return ( 43 43 <span 44 - class="relative flex w-full items-baseline rounded px-0.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 44 + class="relative flex w-full min-w-0 items-baseline rounded px-0.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 45 45 ref={rkeyRef} 46 46 onmouseover={() => setHover(true)} 47 47 onmouseleave={() => setHover(false)}
+11 -19
src/views/record.tsx
··· 10 10 import { JSONValue } from "../components/json.jsx"; 11 11 import { agent } from "../components/login.jsx"; 12 12 import { Modal } from "../components/modal.jsx"; 13 - import { pds, setCID } from "../components/navbar.jsx"; 13 + import { pds } from "../components/navbar.jsx"; 14 14 import Tooltip from "../components/tooltip.jsx"; 15 15 import { setNotif } from "../layout.jsx"; 16 16 import { didDocCache, resolveLexiconAuthority, resolvePDS } from "../utils/api.js"; ··· 34 34 let rpc: Client; 35 35 36 36 const fetchRecord = async () => { 37 - setCID(undefined); 38 37 setValidRecord(undefined); 39 38 setValidSchema(undefined); 40 39 setLexiconUri(undefined); ··· 52 51 setNotice(res.data.error); 53 52 throw new Error(res.data.error); 54 53 } 55 - setCID(res.data.cid); 56 54 setExternalLink(checkUri(res.data.uri, res.data.value)); 57 55 resolveLexicon(params.collection as Nsid); 58 56 verify(res.data); ··· 198 196 label="Copy record" 199 197 icon="lucide--copy" 200 198 /> 199 + <Show when={record()?.cid}> 200 + {(cid) => <CopyMenu copyContent={cid()} label="Copy CID" icon="lucide--copy" />} 201 + </Show> 202 + <Show when={lexiconUri()}> 203 + <NavMenu 204 + href={`/${lexiconUri()}`} 205 + icon="lucide--scroll-text" 206 + label="Lexicon schema" 207 + /> 208 + </Show> 201 209 <Show when={externalLink()}> 202 210 {(externalLink) => ( 203 211 <NavMenu ··· 280 288 <span 281 289 class={`iconify ${validSchema() ? "lucide--check text-green-500 dark:text-green-400" : "lucide--x text-red-500 dark:text-red-400"}`} 282 290 ></span> 283 - </div> 284 - </Show> 285 - <Show when={lexiconUri()}> 286 - <div> 287 - <div class="flex items-center gap-1"> 288 - <span class="iconify lucide--scroll-text"></span> 289 - <p class="font-semibold">Lexicon document</p> 290 - </div> 291 - <div class="truncate text-xs"> 292 - <A 293 - href={`/${lexiconUri()}`} 294 - class="text-blue-400 hover:underline active:underline" 295 - > 296 - {lexiconUri()} 297 - </A> 298 - </div> 299 291 </div> 300 292 </Show> 301 293 </div>