atproto explorer pdsls.dev
atproto tool

new

juli.ee bc07dfe1 7acbe3a1

verified
Changed files
+48 -64
src
+3 -24
src/components/navbar.tsx
···
-
import { A, Params, useLocation } from "@solidjs/router";
import { createEffect, createSignal, Show } from "solid-js";
import { isTouchDevice } from "../layout";
-
import { didDocCache, labelerCache } from "../utils/api";
import { addToClipboard } from "../utils/copy";
import Tooltip from "./tooltip";
···
};
export const NavBar = (props: { params: Params }) => {
-
const location = useLocation();
const [handle, setHandle] = createSignal(props.params.repo);
const [showHandle, setShowHandle] = createSignal(localStorage.showHandle === "true");
···
<Tooltip text="Repository">
<span class="iconify lucide--book-user shrink-0 text-neutral-500 transition-colors duration-200 group-hover:text-neutral-700 dark:text-neutral-400 dark:group-hover:text-neutral-200"></span>
</Tooltip>
-
{props.params.collection || location.pathname.includes("/labels") ?
<A
end
href={`/at://${props.params.repo}`}
···
</Tooltip>
<CopyButton content={props.params.repo} label="Copy DID" />
</div>
-
</div>
-
</Show>
-
-
{/* Labels Level */}
-
<Show
-
when={
-
!props.params.collection &&
-
(props.params.repo in labelerCache || location.pathname.endsWith("/labels"))
-
}
-
>
-
<div class="group flex items-center gap-2 rounded-md border-[0.5px] border-transparent bg-transparent px-2 transition-all duration-200 hover:border-neutral-300 hover:bg-neutral-50/40 dark:hover:border-neutral-600 dark:hover:bg-neutral-800/40">
-
<span class="iconify lucide--tag text-neutral-500 transition-colors duration-200 group-hover:text-neutral-700 dark:text-neutral-400 dark:group-hover:text-neutral-200"></span>
-
<A
-
end
-
href={`/at://${props.params.repo}/labels`}
-
class="py-0.5 font-medium"
-
inactiveClass="text-blue-400 grow hover:text-blue-500 transition-colors duration-150 dark:hover:text-blue-300"
-
>
-
labels
-
</A>
</div>
</Show>
···
+
import { A, Params } from "@solidjs/router";
import { createEffect, createSignal, Show } from "solid-js";
import { isTouchDevice } from "../layout";
+
import { didDocCache } from "../utils/api";
import { addToClipboard } from "../utils/copy";
import Tooltip from "./tooltip";
···
};
export const NavBar = (props: { params: Params }) => {
const [handle, setHandle] = createSignal(props.params.repo);
const [showHandle, setShowHandle] = createSignal(localStorage.showHandle === "true");
···
<Tooltip text="Repository">
<span class="iconify lucide--book-user shrink-0 text-neutral-500 transition-colors duration-200 group-hover:text-neutral-700 dark:text-neutral-400 dark:group-hover:text-neutral-200"></span>
</Tooltip>
+
{props.params.collection ?
<A
end
href={`/at://${props.params.repo}`}
···
</Tooltip>
<CopyButton content={props.params.repo} label="Copy DID" />
</div>
</div>
</Show>
+1 -1
src/index.tsx
···
<Router root={Layout}>
<Route path="/" component={Home} />
<Route path={["/jetstream", "/firehose"]} component={StreamView} />
<Route path="/settings" component={Settings} />
<Route path="/:pds" component={PdsView} />
<Route path="/:pds/:repo" component={RepoView} />
-
<Route path="/:pds/:repo/labels" component={LabelView} />
<Route path="/:pds/:repo/:collection" component={CollectionView} />
<Route path="/:pds/:repo/:collection/:rkey" component={RecordView} />
</Router>
···
<Router root={Layout}>
<Route path="/" component={Home} />
<Route path={["/jetstream", "/firehose"]} component={StreamView} />
+
<Route path="/labels" component={LabelView} />
<Route path="/settings" component={Settings} />
<Route path="/:pds" component={PdsView} />
<Route path="/:pds/:repo" component={RepoView} />
<Route path="/:pds/:repo/:collection" component={CollectionView} />
<Route path="/:pds/:repo/:collection/:rkey" component={RecordView} />
</Router>
+1
src/layout.tsx
···
>
<NavMenu href="/jetstream" label="Jetstream" />
<NavMenu href="/firehose" label="Firehose" />
<NavMenu href="/settings" label="Settings" />
<NavMenu
href="https://bsky.app/profile/did:plc:6q5daed5gutiyerimlrnojnz"
···
>
<NavMenu href="/jetstream" label="Jetstream" />
<NavMenu href="/firehose" label="Firehose" />
+
<NavMenu href="/labels" label="Labels" />
<NavMenu href="/settings" label="Settings" />
<NavMenu
href="https://bsky.app/profile/did:plc:6q5daed5gutiyerimlrnojnz"
+43 -39
src/views/labels.tsx
···
import { ComAtprotoLabelDefs } from "@atcute/atproto";
import { Client, CredentialManager } from "@atcute/client";
-
import { A, useParams, useSearchParams } from "@solidjs/router";
-
import { createResource, createSignal, For, onMount, Show } from "solid-js";
import { Button } from "../components/button.jsx";
import { StickyOverlay } from "../components/sticky.jsx";
import { TextInput } from "../components/text-input.jsx";
import { labelerCache, resolvePDS } from "../utils/api.js";
import { localDateFromTimestamp } from "../utils/date.js";
-
const LabelView = () => {
-
const params = useParams();
const [searchParams, setSearchParams] = useSearchParams();
const [cursor, setCursor] = createSignal<string>();
const [labels, setLabels] = createSignal<ComAtprotoLabelDefs.Label[]>([]);
const [filter, setFilter] = createSignal<string>();
const [labelCount, setLabelCount] = createSignal(0);
-
const did = params.repo;
let rpc: Client;
onMount(async () => {
await resolvePDS(did);
rpc = new Client({
handler: new CredentialManager({ service: labelerCache[did] }),
});
-
refetch();
-
});
-
const fetchLabels = async () => {
-
const uriPatterns = (document.getElementById("patterns") as HTMLInputElement).value;
if (!uriPatterns) return;
const res = await rpc.get("com.atproto.label.queryLabels", {
params: {
uriPatterns: uriPatterns.toString().trim().split(","),
···
cursor: cursor(),
},
});
if (!res.ok) throw new Error(res.data.error);
setCursor(res.data.labels.length < 50 ? undefined : res.data.cursor);
setLabels(labels().concat(res.data.labels) ?? res.data.labels);
return res.data.labels;
};
-
const [response, { refetch }] = createResource(fetchLabels);
-
-
const initQuery = async () => {
-
setLabels([]);
-
setCursor("");
-
setSearchParams({
-
uriPatterns: (document.getElementById("patterns") as HTMLInputElement).value,
-
});
-
refetch();
-
};
-
const filterLabels = () => {
const newFilter = labels().filter((label) => (filter() ? filter() === label.val : true));
setLabelCount(newFilter.length);
···
return (
<div class="flex w-full flex-col items-center">
-
<form
-
class="flex w-full flex-col items-center gap-y-1 px-2"
-
onsubmit={(e) => {
-
e.preventDefault();
-
initQuery();
-
}}
-
>
-
<label for="patterns" class="ml-2 w-full text-sm">
URI Patterns (comma-separated)
</label>
<div class="flex w-full items-center gap-x-1 px-1">
<textarea
-
id="patterns"
-
name="patterns"
spellcheck={false}
rows={2}
value={searchParams.uriPatterns ?? "*"}
class="dark:bg-dark-100 dark:shadow-dark-700 grow rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1 text-sm shadow-xs focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400"
/>
<div class="flex justify-center">
-
<Show when={!response.loading}>
<button
-
type="submit"
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"
>
<span class="iconify lucide--search text-lg"></span>
</button>
</Show>
-
<Show when={response.loading}>
<div class="m-1 flex items-center">
<span class="iconify lucide--loader-circle animate-spin text-lg"></span>
</div>
···
</Show>
<Show when={cursor()}>
<div class="flex h-8 w-22 items-center justify-center text-nowrap">
-
<Show when={!response.loading}>
-
<Button onClick={() => refetch()}>Load More</Button>
</Show>
-
<Show when={response.loading}>
<div class="iconify lucide--loader-circle animate-spin text-xl" />
</Show>
</div>
···
</For>
</div>
</Show>
-
<Show when={!labels().length && !response.loading && searchParams.uriPatterns}>
<div class="mt-2">No results</div>
</Show>
</div>
);
};
-
-
export { LabelView };
···
import { ComAtprotoLabelDefs } from "@atcute/atproto";
import { Client, CredentialManager } from "@atcute/client";
+
import { A, useSearchParams } from "@solidjs/router";
+
import { createSignal, For, onMount, Show } from "solid-js";
import { Button } from "../components/button.jsx";
import { StickyOverlay } from "../components/sticky.jsx";
import { TextInput } from "../components/text-input.jsx";
import { labelerCache, resolvePDS } from "../utils/api.js";
import { localDateFromTimestamp } from "../utils/date.js";
+
export const LabelView = () => {
const [searchParams, setSearchParams] = useSearchParams();
const [cursor, setCursor] = createSignal<string>();
const [labels, setLabels] = createSignal<ComAtprotoLabelDefs.Label[]>([]);
const [filter, setFilter] = createSignal<string>();
const [labelCount, setLabelCount] = createSignal(0);
+
const [loading, setLoading] = createSignal(false);
let rpc: Client;
+
let formRef!: HTMLFormElement;
onMount(async () => {
+
const formData = new FormData();
+
if (searchParams.did) formData.append("did", searchParams.did.toString());
+
if (searchParams.did) fetchLabels(formData);
+
});
+
+
const fetchLabels = async (formData: FormData, reset?: boolean) => {
+
if (reset) {
+
setLabels([]);
+
setCursor(undefined);
+
}
+
+
const did = formData.get("did")?.toString();
+
if (!did) return;
await resolvePDS(did);
rpc = new Client({
handler: new CredentialManager({ service: labelerCache[did] }),
});
+
const uriPatterns = formData.get("uriPatterns")?.toString();
if (!uriPatterns) return;
+
+
setSearchParams({
+
did: formData.get("did")?.toString(),
+
uriPatterns: formData.get("uriPatterns")?.toString(),
+
});
+
+
setLoading(true);
const res = await rpc.get("com.atproto.label.queryLabels", {
params: {
uriPatterns: uriPatterns.toString().trim().split(","),
···
cursor: cursor(),
},
});
+
setLoading(false);
if (!res.ok) throw new Error(res.data.error);
setCursor(res.data.labels.length < 50 ? undefined : res.data.cursor);
setLabels(labels().concat(res.data.labels) ?? res.data.labels);
return res.data.labels;
};
const filterLabels = () => {
const newFilter = labels().filter((label) => (filter() ? filter() === label.val : true));
setLabelCount(newFilter.length);
···
return (
<div class="flex w-full flex-col items-center">
+
<form ref={formRef} class="flex w-full flex-col items-center gap-y-1 px-2">
+
<label class="flex w-full items-center gap-x-2 px-1">
+
<span class="">DID</span>
+
<TextInput name="did" value={searchParams.did ?? ""} class="grow" />
+
</label>
+
<label for="uriPatterns" class="ml-2 w-full text-sm">
URI Patterns (comma-separated)
</label>
<div class="flex w-full items-center gap-x-1 px-1">
<textarea
+
id="uriPatterns"
+
name="uriPatterns"
spellcheck={false}
rows={2}
value={searchParams.uriPatterns ?? "*"}
class="dark:bg-dark-100 dark:shadow-dark-700 grow rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1 text-sm shadow-xs focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400"
/>
<div class="flex justify-center">
+
<Show when={!loading()}>
<button
+
type="button"
+
onClick={() => fetchLabels(new FormData(formRef), true)}
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"
>
<span class="iconify lucide--search text-lg"></span>
</button>
</Show>
+
<Show when={loading()}>
<div class="m-1 flex items-center">
<span class="iconify lucide--loader-circle animate-spin text-lg"></span>
</div>
···
</Show>
<Show when={cursor()}>
<div class="flex h-8 w-22 items-center justify-center text-nowrap">
+
<Show when={!loading()}>
+
<Button onClick={() => fetchLabels(new FormData(formRef))}>Load More</Button>
</Show>
+
<Show when={loading()}>
<div class="iconify lucide--loader-circle animate-spin text-xl" />
</Show>
</div>
···
</For>
</div>
</Show>
+
<Show when={!labels().length && !loading() && searchParams.uriPatterns}>
<div class="mt-2">No results</div>
</Show>
</div>
);
};