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…</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 };