atproto explorer
at main 180 lines 7.0 kB view raw
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 };