forked from
pds.ls/pdsls
atmosphere explorer
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;