forked from
pds.ls/pdsls
this repo has no description
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 setBacklinks(linksBySource(res.links));
28 return res;
29 };
30
31 const [response] = createResource(fetchBacklinks);
32 const [backlinks, setBacklinks] = createSignal<any>();
33
34 const [show, setShow] = createSignal<{
35 collection: string;
36 path: string;
37 showDids: boolean;
38 } | null>();
39
40 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 }) => (
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 (
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>
111 </Show>
112 </div>
113 </div>
114 )}
115 </For>
116 </div>
117 </Show>
118 );
119};
120
121// switching on !!did everywhere is pretty annoying, this could probably be two components
122// but i don't want to duplicate or think about how to extract the paging logic
123const BacklinkItems = ({
124 target,
125 collection,
126 path,
127 dids,
128 cursor,
129}: {
130 target: string;
131 collection: string;
132 path: string;
133 dids: boolean;
134 cursor?: string;
135}) => {
136 const [links, setLinks] = createSignal<any>();
137 const [more, setMore] = createSignal<boolean>(false);
138
139 onMount(async () => {
140 const links = await (dids ? getDidBacklinks : getRecordBacklinks)(
141 target,
142 collection,
143 path,
144 cursor,
145 );
146 setLinks(links);
147 });
148
149 // TODO: could pass the `total` into this component, which can be checked against each call to this endpoint to find if it's stale.
150 // also hmm 'total' is misleading/wrong on that api
151
152 return (
153 <Show when={links()} fallback={<p>Loading…</p>}>
154 <Show when={dids}>
155 <For each={links().linking_dids}>
156 {(did) => (
157 <a
158 href={`/at://${did}`}
159 class="relative flex w-full font-mono text-blue-400 hover:underline active:underline"
160 >
161 {did}
162 </a>
163 )}
164 </For>
165 </Show>
166 <Show when={!dids}>
167 <For each={links().linking_records}>
168 {({ did, collection, rkey }) => (
169 <p class="relative flex w-full items-center gap-1 font-mono">
170 <a
171 href={`/at://${did}/${collection}/${rkey}`}
172 class="text-blue-400 hover:underline active:underline"
173 >
174 {rkey}
175 </a>
176 <span class="text-xs text-neutral-500 dark:text-neutral-400">
177 {TID.validate(rkey) ?
178 localDateFromTimestamp(TID.parse(rkey).timestamp / 1000)
179 : undefined}
180 </span>
181 </p>
182 )}
183 </For>
184 </Show>
185 <Show when={links().cursor}>
186 <Show when={more()} fallback={<Button onClick={() => setMore(true)}>Load More</Button>}>
187 <BacklinkItems
188 target={target}
189 collection={collection}
190 path={path}
191 dids={dids}
192 cursor={links().cursor}
193 />
194 </Show>
195 </Show>
196 </Show>
197 );
198};
199
200export { Backlinks };