forked from
pds.ls/pdsls
atproto explorer
1import { A, useParams } from "@solidjs/router";
2import { createEffect, createSignal, ErrorBoundary, For, Show } from "solid-js";
3import { hideMedia } from "../views/settings";
4import { pds } from "./navbar";
5import Tooltip from "./tooltip";
6import VideoPlayer from "./video-player";
7
8interface AtBlob {
9 $type: string;
10 ref: { $link: string };
11 mimeType: string;
12}
13
14const ATURI_RE =
15 /^at:\/\/([a-zA-Z0-9._:%-]+)(?:\/([a-zA-Z0-9-.]+)(?:\/([a-zA-Z0-9._~:@!$&%')(*+,;=-]+))?)?(?:#(\/[a-zA-Z0-9._~:@!$&%')(*+,;=\-[\]/\\]*))?$/;
16
17const DID_RE = /^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$/;
18
19const JSONString = ({ data }: { data: string }) => {
20 const isURL =
21 URL.canParse ??
22 ((url, base) => {
23 try {
24 new URL(url, base);
25 return true;
26 } catch {
27 return false;
28 }
29 });
30
31 return (
32 <span>
33 "
34 <For each={data.split(/(\s)/)}>
35 {(part) => (
36 <>
37 {ATURI_RE.test(part) ?
38 <A class="text-blue-400 hover:underline active:underline" href={`/${part}`}>
39 {part}
40 </A>
41 : DID_RE.test(part) ?
42 <A class="text-blue-400 hover:underline active:underline" href={`/at://${part}`}>
43 {part}
44 </A>
45 : (
46 isURL(part) &&
47 ["http:", "https:", "web+at:"].includes(new URL(part).protocol) &&
48 part.split("\n").length === 1
49 ) ?
50 <a class="underline" href={part} target="_blank" rel="noopener noreferrer">
51 {part}
52 </a>
53 : part}
54 </>
55 )}
56 </For>
57 "
58 </span>
59 );
60};
61
62const JSONNumber = ({ data }: { data: number }) => {
63 return <span>{data}</span>;
64};
65
66const JSONBoolean = ({ data }: { data: boolean }) => {
67 return <span>{data ? "true" : "false"}</span>;
68};
69
70const JSONNull = () => {
71 return <span>null</span>;
72};
73
74const JSONObject = ({ data, repo }: { data: { [x: string]: JSONType }; repo: string }) => {
75 const params = useParams();
76 const [hide, setHide] = createSignal(
77 localStorage.hideMedia === "true" || params.rkey === undefined,
78 );
79
80 createEffect(() => {
81 if (hideMedia()) setHide(hideMedia());
82 });
83
84 const Obj = ({ key, value }: { key: string; value: JSONType }) => {
85 const [show, setShow] = createSignal(true);
86
87 return (
88 <span
89 classList={{
90 "group/indent flex gap-x-1 w-full": true,
91 "flex-col": value === Object(value),
92 }}
93 >
94 <button
95 class="group/clip relative flex size-fit max-w-[40%] shrink-0 items-center wrap-anywhere text-neutral-500 hover:text-neutral-700 active:text-neutral-700 sm:max-w-[50%] dark:text-neutral-400 dark:hover:text-neutral-300 dark:active:text-neutral-300"
96 onclick={() => setShow(!show())}
97 >
98 <span
99 classList={{
100 "dark:bg-dark-500 absolute w-5 flex items-center -left-5 bg-neutral-100 text-sm": true,
101 "hidden group-hover/clip:flex": show(),
102 }}
103 >
104 {show() ?
105 <span class="iconify lucide--chevron-down"></span>
106 : <span class="iconify lucide--chevron-right"></span>}
107 </span>
108 {key}:
109 </button>
110 <span
111 classList={{
112 "self-center": value !== Object(value),
113 "pl-[calc(2ch-0.5px)] border-l-[0.5px] border-neutral-500/50 dark:border-neutral-400/50 has-hover:group-hover/indent:border-neutral-700 dark:has-hover:group-hover/indent:border-neutral-300":
114 value === Object(value),
115 "invisible h-0": !show(),
116 }}
117 >
118 <JSONValue data={value} repo={repo} />
119 </span>
120 </span>
121 );
122 };
123
124 const rawObj = (
125 <For each={Object.entries(data)}>{([key, value]) => <Obj key={key} value={value} />}</For>
126 );
127
128 const blob: AtBlob = data as any;
129
130 if (blob.$type === "blob") {
131 return (
132 <>
133 <Show when={pds() && params.rkey}>
134 <span class="flex gap-x-1">
135 <Show when={blob.mimeType.startsWith("image/") && !hide()}>
136 <img
137 class="max-h-[16rem] w-fit max-w-[16rem]"
138 src={`https://${pds()}/xrpc/com.atproto.sync.getBlob?did=${repo}&cid=${blob.ref.$link}`}
139 />
140 </Show>
141 <Show when={blob.mimeType === "video/mp4" && !hide()}>
142 <ErrorBoundary fallback={() => <span>Failed to load video</span>}>
143 <VideoPlayer did={repo} cid={blob.ref.$link} />
144 </ErrorBoundary>
145 </Show>
146 <span
147 classList={{
148 "flex items-center justify-between gap-1": true,
149 "flex-col": !hide(),
150 }}
151 >
152 <Show when={blob.mimeType.startsWith("image/") || blob.mimeType === "video/mp4"}>
153 <Tooltip text={hide() ? "Show" : "Hide"}>
154 <button
155 onclick={() => setHide(!hide())}
156 class={`${!hide() ? "-mt-1 -ml-0.5" : ""} 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`}
157 >
158 <span
159 class={`iconify text-base ${hide() ? "lucide--eye-off" : "lucide--eye"}`}
160 ></span>
161 </button>
162 </Tooltip>
163 </Show>
164 <Tooltip text="Blob on PDS">
165 <a
166 href={`https://${pds()}/xrpc/com.atproto.sync.getBlob?did=${repo}&cid=${blob.ref.$link}`}
167 target="_blank"
168 class={`${!hide() ? "-mb-1 -ml-0.5" : ""} 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`}
169 >
170 <span class="iconify lucide--external-link text-base"></span>
171 </a>
172 </Tooltip>
173 </span>
174 </span>
175 </Show>
176 {rawObj}
177 </>
178 );
179 }
180
181 return rawObj;
182};
183
184const JSONArray = ({ data, repo }: { data: JSONType[]; repo: string }) => {
185 return (
186 <For each={data}>
187 {(value, index) => (
188 <span
189 classList={{
190 "flex before:content-['-']": true,
191 "mb-2": value === Object(value) && index() !== data.length - 1,
192 }}
193 >
194 <span class="ml-[1ch] w-full">
195 <JSONValue data={value} repo={repo} />
196 </span>
197 </span>
198 )}
199 </For>
200 );
201};
202
203export const JSONValue = ({ data, repo }: { data: JSONType; repo: string }) => {
204 if (typeof data === "string") return <JSONString data={data} />;
205 if (typeof data === "number") return <JSONNumber data={data} />;
206 if (typeof data === "boolean") return <JSONBoolean data={data} />;
207 if (data === null) return <JSONNull />;
208 if (Array.isArray(data)) return <JSONArray data={data} repo={repo} />;
209 return <JSONObject data={data} repo={repo} />;
210};
211
212export type JSONType = string | number | boolean | null | { [x: string]: JSONType } | JSONType[];