atmosphere explorer
at main 119 lines 3.5 kB view raw
1import { Client, simpleFetchHandler } from "@atcute/client"; 2import { ActorIdentifier } from "@atcute/lexicons"; 3import { createSignal, Show } from "solid-js"; 4import { getPDS } from "../../utils/api"; 5import { JSONValue } from "../json"; 6import HoverCard from "./base"; 7 8interface RecordHoverCardProps { 9 uri: string; 10 newTab?: boolean; 11 class?: string; 12 labelClass?: string; 13 trigger?: any; 14 hoverDelay?: number; 15} 16 17const recordCache = new Map<string, { value: unknown; loading: boolean; error?: string }>(); 18 19const parseAtUri = (uri: string) => { 20 const match = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/); 21 if (!match) return null; 22 return { repo: match[1], collection: match[2], rkey: match[3] }; 23}; 24 25const prefetchRecord = async (uri: string) => { 26 if (recordCache.has(uri)) return; 27 28 const parsed = parseAtUri(uri); 29 if (!parsed) return; 30 31 recordCache.set(uri, { value: null, loading: true }); 32 33 try { 34 const pds = await getPDS(parsed.repo); 35 const rpc = new Client({ handler: simpleFetchHandler({ service: pds }) }); 36 const res = await rpc.get("com.atproto.repo.getRecord", { 37 params: { 38 repo: parsed.repo as ActorIdentifier, 39 collection: parsed.collection as `${string}.${string}.${string}`, 40 rkey: parsed.rkey, 41 }, 42 }); 43 44 if (!res.ok) { 45 recordCache.set(uri, { value: null, loading: false, error: res.data.error }); 46 return; 47 } 48 49 recordCache.set(uri, { value: res.data.value, loading: false }); 50 } catch (err: any) { 51 recordCache.set(uri, { value: null, loading: false, error: err.message || "Failed to fetch" }); 52 } 53}; 54 55const RecordHoverCard = (props: RecordHoverCardProps) => { 56 const [record, setRecord] = createSignal<{ 57 value: unknown; 58 loading: boolean; 59 error?: string; 60 } | null>(null); 61 62 const parsed = () => parseAtUri(props.uri); 63 64 const handlePrefetch = () => { 65 prefetchRecord(props.uri); 66 67 // Start polling for cache updates 68 const cached = recordCache.get(props.uri); 69 setRecord(cached || { value: null, loading: true }); 70 71 if (!cached || cached.loading) { 72 const pollInterval = setInterval(() => { 73 const updated = recordCache.get(props.uri); 74 if (updated && !updated.loading) { 75 setRecord(updated); 76 clearInterval(pollInterval); 77 } 78 }, 100); 79 80 setTimeout(() => clearInterval(pollInterval), 10000); 81 } 82 }; 83 84 return ( 85 <HoverCard 86 href={`/${props.uri}`} 87 label={props.uri} 88 newTab={props.newTab} 89 onHover={handlePrefetch} 90 hoverDelay={props.hoverDelay ?? 300} 91 trigger={props.trigger} 92 class={props.class} 93 labelClass={props.labelClass} 94 > 95 <Show when={record()?.loading}> 96 <div class="flex items-center gap-2 font-sans text-sm text-neutral-500 dark:text-neutral-400"> 97 <span class="iconify lucide--loader-circle animate-spin" /> 98 Loading... 99 </div> 100 </Show> 101 <Show when={record()?.error}> 102 <div class="font-sans text-sm text-red-500 dark:text-red-400">{record()?.error}</div> 103 </Show> 104 <Show when={record()?.value && !record()?.loading}> 105 <div class="font-mono text-xs wrap-break-word"> 106 <JSONValue 107 data={record()?.value as any} 108 repo={parsed()?.repo || ""} 109 truncate 110 newTab 111 hideBlobs 112 /> 113 </div> 114 </Show> 115 </HoverCard> 116 ); 117}; 118 119export default RecordHoverCard;