forked from pdsls.dev/pdsls
atproto explorer
at main 6.4 kB view raw
1import * as TID from "@atcute/tid"; 2import { createResource, createSignal, For, onMount, Show } from "solid-js"; 3import { getAllBacklinks, getDidBacklinks, getRecordBacklinks } from "../utils/api.js"; 4import { localDateFromTimestamp } from "../utils/date.js"; 5import { Button } from "./button.jsx"; 6 7// the actual backlink api will probably become closer to this 8const linksBySource = (links: Record<string, any>) => { 9 let out: any[] = []; 10 Object.keys(links) 11 .toSorted() 12 .forEach((collection) => { 13 const paths = links[collection]; 14 Object.keys(paths) 15 .toSorted() 16 .forEach((path) => { 17 if (paths[path].records === 0) return; 18 out.push({ collection, path, counts: paths[path] }); 19 }); 20 }); 21 return out; 22}; 23 24const Backlinks = (props: { target: string }) => { 25 const fetchBacklinks = async () => { 26 const res = await getAllBacklinks(props.target); 27 return linksBySource(res.links); 28 }; 29 30 const [response] = createResource(fetchBacklinks); 31 32 const [show, setShow] = createSignal<{ 33 collection: string; 34 path: string; 35 showDids: boolean; 36 } | null>(); 37 38 return ( 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> 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> 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 /> 106 </Show> 107 </Show> 108 </div> 109 </div> 110 )} 111 </For> 112 </div> 113 ); 114}; 115 116// switching on !!did everywhere is pretty annoying, this could probably be two components 117// but i don't want to duplicate or think about how to extract the paging logic 118const BacklinkItems = ({ 119 target, 120 collection, 121 path, 122 dids, 123 cursor, 124}: { 125 target: string; 126 collection: string; 127 path: string; 128 dids: boolean; 129 cursor?: string; 130}) => { 131 const [links, setLinks] = createSignal<any>(); 132 const [more, setMore] = createSignal<boolean>(false); 133 134 onMount(async () => { 135 const links = await (dids ? getDidBacklinks : getRecordBacklinks)( 136 target, 137 collection, 138 path, 139 cursor, 140 ); 141 setLinks(links); 142 }); 143 144 // TODO: could pass the `total` into this component, which can be checked against each call to this endpoint to find if it's stale. 145 // also hmm 'total' is misleading/wrong on that api 146 147 return ( 148 <Show when={links()} fallback={<p>Loading&hellip;</p>}> 149 <Show when={dids}> 150 <For each={links().linking_dids}> 151 {(did) => ( 152 <a 153 href={`/at://${did}`} 154 class="relative flex w-full font-mono text-blue-400 hover:underline active:underline" 155 > 156 {did} 157 </a> 158 )} 159 </For> 160 </Show> 161 <Show when={!dids}> 162 <For each={links().linking_records}> 163 {({ did, collection, rkey }) => ( 164 <p class="relative flex w-full items-center gap-1 font-mono"> 165 <a 166 href={`/at://${did}/${collection}/${rkey}`} 167 class="text-blue-400 hover:underline active:underline" 168 > 169 {rkey} 170 </a> 171 <span class="text-xs text-neutral-500 dark:text-neutral-400"> 172 {TID.validate(rkey) ? 173 localDateFromTimestamp(TID.parse(rkey).timestamp / 1000) 174 : undefined} 175 </span> 176 </p> 177 )} 178 </For> 179 </Show> 180 <Show when={links().cursor}> 181 <Show when={more()} fallback={<Button onClick={() => setMore(true)}>Load More</Button>}> 182 <BacklinkItems 183 target={target} 184 collection={collection} 185 path={path} 186 dids={dids} 187 cursor={links().cursor} 188 /> 189 </Show> 190 </Show> 191 </Show> 192 ); 193}; 194 195export { Backlinks };