forked from
pds.ls/pdsls
atproto explorer
1import { ComAtprotoLabelDefs } from "@atcute/atproto";
2import { Client, CredentialManager } from "@atcute/client";
3import { A, useParams, useSearchParams } from "@solidjs/router";
4import { createResource, createSignal, For, onMount, Show } from "solid-js";
5import { Button } from "../components/button.jsx";
6import { StickyOverlay } from "../components/sticky.jsx";
7import { TextInput } from "../components/text-input.jsx";
8import { labelerCache, resolvePDS } from "../utils/api.js";
9import { localDateFromTimestamp } from "../utils/date.js";
10
11const LabelView = () => {
12 const params = useParams();
13 const [searchParams, setSearchParams] = useSearchParams();
14 const [cursor, setCursor] = createSignal<string>();
15 const [labels, setLabels] = createSignal<ComAtprotoLabelDefs.Label[]>([]);
16 const [filter, setFilter] = createSignal<string>();
17 const [labelCount, setLabelCount] = createSignal(0);
18 const did = params.repo;
19 let rpc: Client;
20
21 onMount(async () => {
22 await resolvePDS(did);
23 rpc = new Client({
24 handler: new CredentialManager({ service: labelerCache[did] }),
25 });
26 refetch();
27 });
28
29 const fetchLabels = async () => {
30 const uriPatterns = (document.getElementById("patterns") as HTMLInputElement).value;
31 if (!uriPatterns) return;
32 const res = await rpc.get("com.atproto.label.queryLabels", {
33 params: {
34 uriPatterns: uriPatterns.toString().trim().split(","),
35 sources: [did as `did:${string}:${string}`],
36 cursor: cursor(),
37 },
38 });
39 if (!res.ok) throw new Error(res.data.error);
40 setCursor(res.data.labels.length < 50 ? undefined : res.data.cursor);
41 setLabels(labels().concat(res.data.labels) ?? res.data.labels);
42 return res.data.labels;
43 };
44
45 const [response, { refetch }] = createResource(fetchLabels);
46
47 const initQuery = async () => {
48 setLabels([]);
49 setCursor("");
50 setSearchParams({
51 uriPatterns: (document.getElementById("patterns") as HTMLInputElement).value,
52 });
53 refetch();
54 };
55
56 const filterLabels = () => {
57 const newFilter = labels().filter((label) => (filter() ? filter() === label.val : true));
58 setLabelCount(newFilter.length);
59 return newFilter;
60 };
61
62 return (
63 <div class="flex w-full flex-col items-center">
64 <form
65 class="flex w-full flex-col items-center gap-y-1 px-2"
66 onsubmit={(e) => {
67 e.preventDefault();
68 initQuery();
69 }}
70 >
71 <label for="patterns" class="ml-2 w-full text-sm">
72 URI Patterns (comma-separated)
73 </label>
74 <div class="flex w-full items-center gap-x-1 px-1">
75 <textarea
76 id="patterns"
77 name="patterns"
78 spellcheck={false}
79 rows={3}
80 value={searchParams.uriPatterns ?? "*"}
81 class="dark:bg-dark-100 dark:shadow-dark-800 grow rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1 shadow-xs focus:outline-[1px] focus:outline-neutral-900 dark:border-neutral-700 dark:focus:outline-neutral-200"
82 />
83 <div class="flex justify-center">
84 <Show when={!response.loading}>
85 <button
86 type="submit"
87 class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
88 >
89 <span class="iconify lucide--search text-lg"></span>
90 </button>
91 </Show>
92 <Show when={response.loading}>
93 <div class="m-1 flex items-center">
94 <span class="iconify lucide--loader-circle animate-spin text-lg"></span>
95 </div>
96 </Show>
97 </div>
98 </div>
99 </form>
100 <StickyOverlay>
101 <TextInput
102 placeholder="Filter by label"
103 name="filter"
104 onInput={(e) => setFilter(e.currentTarget.value)}
105 class="w-full"
106 />
107 <div class="flex items-center gap-x-2">
108 <Show when={labelCount() && labels().length}>
109 <div>
110 <span>
111 {labelCount()} label{labelCount() > 1 ? "s" : ""}
112 </span>
113 </div>
114 </Show>
115 <Show when={cursor()}>
116 <div class="flex h-[2rem] w-[5.5rem] items-center justify-center text-nowrap">
117 <Show when={!response.loading}>
118 <Button onClick={() => refetch()}>Load More</Button>
119 </Show>
120 <Show when={response.loading}>
121 <div class="iconify lucide--loader-circle animate-spin text-xl" />
122 </Show>
123 </div>
124 </Show>
125 </div>
126 </StickyOverlay>
127 <Show when={labels().length}>
128 <div class="flex flex-col gap-2 divide-y-[0.5px] divide-neutral-400 text-sm wrap-anywhere whitespace-pre-wrap dark:divide-neutral-600">
129 <For each={filterLabels()}>
130 {(label) => (
131 <div class="flex items-center justify-between gap-2 pb-2">
132 <div class="flex flex-col">
133 <div class="flex items-center gap-x-2">
134 <div class="min-w-[4rem] font-semibold">URI</div>
135 <A
136 href={`/at://${label.uri.replace("at://", "")}`}
137 class="text-blue-400 hover:underline active:underline"
138 >
139 {label.uri}
140 </A>
141 </div>
142 <Show when={label.cid}>
143 <div class="flex items-center gap-x-2">
144 <div class="min-w-[4rem] font-semibold">CID</div>
145 {label.cid}
146 </div>
147 </Show>
148 <div class="flex items-center gap-x-2">
149 <div class="min-w-[4rem] font-semibold">Label</div>
150 {label.val}
151 </div>
152 <div class="flex items-center gap-x-2">
153 <div class="min-w-[4rem] font-semibold">Created</div>
154 {localDateFromTimestamp(new Date(label.cts).getTime())}
155 </div>
156 <Show when={label.exp}>
157 {(exp) => (
158 <div class="flex items-center gap-x-2">
159 <div class="min-w-[4rem] font-semibold">Expires</div>
160 {localDateFromTimestamp(new Date(exp()).getTime())}
161 </div>
162 )}
163 </Show>
164 </div>
165 <Show when={label.neg}>
166 <div class="iconify lucide--minus shrink-0 text-lg text-red-500 dark:text-red-400" />
167 </Show>
168 </div>
169 )}
170 </For>
171 </div>
172 </Show>
173 <Show when={!labels().length && !response.loading && searchParams.uriPatterns}>
174 <div class="mt-2">No results</div>
175 </Show>
176 </div>
177 );
178};
179
180export { LabelView };