tangled
alpha
login
or
join now
vielle.dev
/
pdsls
forked from
pds.ls/pdsls
0
fork
atom
atproto explorer
0
fork
atom
overview
issues
pulls
pipelines
search shortcut
handle.invalid
6 months ago
667c994f
bc95ef71
+190
-152
10 changed files
expand all
collapse all
unified
split
src
components
navbar.tsx
search.tsx
tooltip.tsx
layout.tsx
views
collection.tsx
home.tsx
labels.tsx
pds.tsx
record.tsx
repo.tsx
+1
-1
src/components/navbar.tsx
···
52
52
});
53
53
54
54
return (
55
55
-
<nav class="mt-4 flex w-[22rem] flex-col text-sm wrap-anywhere sm:w-[24rem]">
55
55
+
<nav class="flex w-[22rem] flex-col text-sm wrap-anywhere sm:w-[24rem]">
56
56
<div class="relative flex items-center justify-between gap-1">
57
57
<div class="flex min-h-[1.25rem] basis-full items-center gap-2">
58
58
<Tooltip text="PDS">
+48
-29
src/components/search.tsx
···
1
1
-
import { Handle } from "@atcute/lexicons";
2
2
-
import { useNavigate } from "@solidjs/router";
3
3
-
import { createSignal, Show } from "solid-js";
4
4
-
import { resolveHandle } from "../utils/api.js";
1
1
+
import { useLocation, useNavigate } from "@solidjs/router";
2
2
+
import { createSignal, onCleanup, onMount, Show } from "solid-js";
3
3
+
import { isTouchDevice } from "../layout";
4
4
+
5
5
+
export const [showSearch, setShowSearch] = createSignal(false);
6
6
+
7
7
+
const SearchButton = () => {
8
8
+
onMount(() => window.addEventListener("keydown", keyEvent));
9
9
+
onCleanup(() => window.removeEventListener("keydown", keyEvent));
10
10
+
11
11
+
const keyEvent = (ev: KeyboardEvent) => {
12
12
+
if (document.querySelector("dialog")) return;
13
13
+
14
14
+
if ((ev.ctrlKey || ev.metaKey) && ev.key == "k") {
15
15
+
ev.preventDefault();
16
16
+
setShowSearch(!showSearch());
17
17
+
} else if (ev.key == "Escape") {
18
18
+
ev.preventDefault();
19
19
+
setShowSearch(false);
20
20
+
}
21
21
+
};
22
22
+
23
23
+
return (
24
24
+
<button
25
25
+
onclick={() => setShowSearch(!showSearch())}
26
26
+
class={`flex items-center gap-0.5 rounded-lg ${isTouchDevice ? "p-1 text-xl hover:bg-neutral-200 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-700" : "bg-neutral-200 p-1.5 text-xs hover:bg-neutral-300 active:bg-neutral-300 dark:bg-neutral-700 dark:hover:bg-neutral-600 dark:active:bg-neutral-600"}`}
27
27
+
>
28
28
+
<span class="iconify lucide--search"></span>
29
29
+
<Show when={!isTouchDevice}>
30
30
+
<kbd class="font-sans text-neutral-500 dark:text-neutral-400">
31
31
+
{/Mac/i.test(navigator.platform) ? "⌘" : "⌃"}K
32
32
+
</kbd>
33
33
+
</Show>
34
34
+
</button>
35
35
+
);
36
36
+
};
5
37
6
38
const Search = () => {
7
39
const navigate = useNavigate();
8
40
let searchInput!: HTMLInputElement;
9
9
-
const [loading, setLoading] = createSignal(false);
41
41
+
42
42
+
onMount(() => {
43
43
+
if (useLocation().pathname !== "/") searchInput.focus();
44
44
+
});
10
45
11
46
const processInput = async (input: string) => {
12
47
(document.getElementById("uriForm") as HTMLFormElement).reset();
···
16
51
.replace(/^\u202a/, "")
17
52
.replace(/^@/, "");
18
53
if (!input.length) return;
19
19
-
(document.getElementById("input") as HTMLInputElement).blur();
54
54
+
setShowSearch(false);
20
55
if (
21
56
!input.startsWith("https://bsky.app/") &&
22
57
!input.startsWith("https://deer.social/") &&
···
32
67
.replace("https://bsky.app/profile/", "")
33
68
.replace("/post/", "/app.bsky.feed.post/");
34
69
const uriParts = uri.split("/");
35
35
-
const actor = uriParts[0];
36
36
-
let did = "";
37
37
-
try {
38
38
-
setLoading(true);
39
39
-
did = uri.startsWith("did:") ? actor : await resolveHandle(actor as Handle);
40
40
-
setLoading(false);
41
41
-
} catch {
42
42
-
setLoading(false);
43
43
-
navigate(`/${actor}`);
44
44
-
return;
45
45
-
}
46
46
-
navigate(`/at://${did}${uriParts.length > 1 ? `/${uriParts.slice(1).join("/")}` : ""}`);
70
70
+
navigate(`/at://${uriParts[0]}${uriParts.length > 1 ? `/${uriParts.slice(1).join("/")}` : ""}`);
47
71
};
48
72
49
73
return (
···
65
89
id="input"
66
90
class="grow placeholder:text-sm focus:outline-none"
67
91
/>
68
68
-
<Show when={loading()}>
69
69
-
<span class="iconify lucide--loader-circle animate-spin text-lg"></span>
70
70
-
</Show>
71
71
-
<Show when={!loading()}>
72
72
-
<button
73
73
-
type="submit"
74
74
-
class="iconify lucide--arrow-right text-lg text-neutral-500 dark:text-neutral-400"
75
75
-
onclick={() => processInput(searchInput.value)}
76
76
-
></button>
77
77
-
</Show>
92
92
+
<button
93
93
+
type="submit"
94
94
+
class="iconify lucide--arrow-right text-lg text-neutral-500 dark:text-neutral-400"
95
95
+
onclick={() => processInput(searchInput.value)}
96
96
+
></button>
78
97
</div>
79
98
</div>
80
99
</form>
81
100
);
82
101
};
83
102
84
84
-
export { Search };
103
103
+
export { Search, SearchButton };
+1
-2
src/components/tooltip.tsx
···
1
1
import { JSX, Show } from "solid-js";
2
2
-
3
3
-
const isTouchDevice = "ontouchstart" in window || navigator.maxTouchPoints > 1;
2
2
+
import { isTouchDevice } from "../layout";
4
3
5
4
const Tooltip = (props: { text: string; children: JSX.Element }) => (
6
5
<div class="group/tooltip relative flex items-center">
+8
-3
src/layout.tsx
···
7
7
import { DropdownMenu, MenuProvider, NavMenu } from "./components/dropdown.jsx";
8
8
import { agent } from "./components/login.jsx";
9
9
import { NavBar } from "./components/navbar.jsx";
10
10
-
import { Search } from "./components/search.jsx";
10
10
+
import { Search, SearchButton, showSearch } from "./components/search.jsx";
11
11
import { themeEvent, ThemeSelection } from "./components/theme.jsx";
12
12
import { resolveHandle } from "./utils/api.js";
13
13
+
14
14
+
export const isTouchDevice = "ontouchstart" in window || navigator.maxTouchPoints > 1;
13
15
14
16
export const [notif, setNotif] = createSignal<{
15
17
show: boolean;
···
57
59
<span>PDSls</span>
58
60
</A>
59
61
<div class="relative flex items-center gap-1">
62
62
+
<Show when={location.pathname !== "/"}>
63
63
+
<SearchButton />
64
64
+
</Show>
60
65
<Show when={agent()}>
61
66
<RecordEditor create={true} />
62
67
</Show>
···
75
80
</MenuProvider>
76
81
</div>
77
82
</header>
78
78
-
<div class="mb-4 flex max-w-full min-w-[22rem] flex-col items-center text-pretty sm:min-w-[24rem] md:max-w-[48rem]">
79
79
-
<Show when={!["/jetstream", "/firehose", "/settings"].includes(location.pathname)}>
83
83
+
<div class="flex max-w-full min-w-[22rem] flex-col items-center gap-4 text-pretty sm:min-w-[24rem] md:max-w-[48rem]">
84
84
+
<Show when={showSearch() || location.pathname === "/"}>
80
85
<Search />
81
86
</Show>
82
87
<Show when={props.params.pds}>
+107
-105
src/views/collection.tsx
···
159
159
160
160
return (
161
161
<Show when={records.length || response()}>
162
162
-
<div class="dark:bg-dark-500/70 sticky top-0 z-5 flex w-screen flex-col items-center justify-center gap-2 bg-neutral-100/70 py-3 backdrop-blur-xs">
163
163
-
<div class="flex w-[22rem] items-center gap-2 sm:w-[24rem]">
164
164
-
<Show when={agent() && agent()?.sub === did}>
165
165
-
<div class="flex items-center gap-x-2">
166
166
-
<Tooltip
167
167
-
text={batchDelete() ? "Cancel" : "Delete"}
168
168
-
children={
169
169
-
<button
170
170
-
onclick={() => {
171
171
-
setRecords(
172
172
-
{ from: 0, to: untrack(() => records.length) - 1 },
173
173
-
"toDelete",
174
174
-
false,
175
175
-
);
176
176
-
setLastSelected(undefined);
177
177
-
setBatchDelete(!batchDelete());
178
178
-
}}
179
179
-
class="flex items-center"
180
180
-
>
181
181
-
<span
182
182
-
class={`iconify text-lg ${batchDelete() ? "lucide--circle-x" : "lucide--trash-2"} `}
183
183
-
></span>
184
184
-
</button>
185
185
-
}
186
186
-
/>
187
187
-
<Show when={batchDelete()}>
162
162
+
<div class="flex w-full flex-col items-center">
163
163
+
<div class="dark:bg-dark-500/70 sticky top-0 z-5 flex w-screen flex-col items-center justify-center gap-2 bg-neutral-100/70 pt-1 pb-3 backdrop-blur-xs">
164
164
+
<div class="flex w-[22rem] items-center gap-2 sm:w-[24rem]">
165
165
+
<Show when={agent() && agent()?.sub === did}>
166
166
+
<div class="flex items-center gap-x-2">
188
167
<Tooltip
189
189
-
text="Select all"
190
190
-
children={
191
191
-
<button onclick={() => selectAll()} class="flex items-center">
192
192
-
<span class="iconify lucide--copy-check text-lg"></span>
193
193
-
</button>
194
194
-
}
195
195
-
/>
196
196
-
<Tooltip
197
197
-
text="Confirm"
168
168
+
text={batchDelete() ? "Cancel" : "Delete"}
198
169
children={
199
199
-
<button onclick={() => deleteRecords()} class="flex items-center">
200
200
-
<span class="iconify lucide--trash-2 text-lg text-red-500 dark:text-red-400"></span>
170
170
+
<button
171
171
+
onclick={() => {
172
172
+
setRecords(
173
173
+
{ from: 0, to: untrack(() => records.length) - 1 },
174
174
+
"toDelete",
175
175
+
false,
176
176
+
);
177
177
+
setLastSelected(undefined);
178
178
+
setBatchDelete(!batchDelete());
179
179
+
}}
180
180
+
class="flex items-center"
181
181
+
>
182
182
+
<span
183
183
+
class={`iconify text-lg ${batchDelete() ? "lucide--circle-x" : "lucide--trash-2"} `}
184
184
+
></span>
201
185
</button>
202
186
}
203
187
/>
204
204
-
</Show>
188
188
+
<Show when={batchDelete()}>
189
189
+
<Tooltip
190
190
+
text="Select all"
191
191
+
children={
192
192
+
<button onclick={() => selectAll()} class="flex items-center">
193
193
+
<span class="iconify lucide--copy-check text-lg"></span>
194
194
+
</button>
195
195
+
}
196
196
+
/>
197
197
+
<Tooltip
198
198
+
text="Confirm"
199
199
+
children={
200
200
+
<button onclick={() => deleteRecords()} class="flex items-center">
201
201
+
<span class="iconify lucide--trash-2 text-lg text-red-500 dark:text-red-400"></span>
202
202
+
</button>
203
203
+
}
204
204
+
/>
205
205
+
</Show>
206
206
+
</div>
207
207
+
</Show>
208
208
+
<TextInput
209
209
+
placeholder="Filter by substring"
210
210
+
class="w-full"
211
211
+
onInput={(e) => setFilter(e.currentTarget.value)}
212
212
+
/>
213
213
+
</div>
214
214
+
<Show when={records.length > 1}>
215
215
+
<div class="flex w-[22rem] items-center justify-between gap-x-2 sm:w-[24rem]">
216
216
+
<Button
217
217
+
onClick={() => {
218
218
+
setReverse(!reverse());
219
219
+
setRecords([]);
220
220
+
setCursor(undefined);
221
221
+
refetch();
222
222
+
}}
223
223
+
>
224
224
+
<span
225
225
+
class={`iconify ${reverse() ? "lucide--rotate-ccw" : "lucide--rotate-cw"} text-sm`}
226
226
+
></span>
227
227
+
Reverse
228
228
+
</Button>
229
229
+
<div>
230
230
+
<Show when={batchDelete()}>
231
231
+
<span>{records.filter((rec) => rec.toDelete).length}</span>
232
232
+
<span>/</span>
233
233
+
</Show>
234
234
+
<span>{records.length} records</span>
235
235
+
</div>
236
236
+
<div class="flex w-[5rem] items-center justify-end">
237
237
+
<Show when={cursor()}>
238
238
+
<Show when={!response.loading}>
239
239
+
<Button onClick={() => refetch()}>Load More</Button>
240
240
+
</Show>
241
241
+
<Show when={response.loading}>
242
242
+
<div class="iconify lucide--loader-circle w-[5rem] animate-spin text-xl" />
243
243
+
</Show>
244
244
+
</Show>
245
245
+
</div>
205
246
</div>
206
247
</Show>
207
207
-
<TextInput
208
208
-
placeholder="Filter by substring"
209
209
-
class="w-full"
210
210
-
onInput={(e) => setFilter(e.currentTarget.value)}
211
211
-
/>
212
248
</div>
213
213
-
<Show when={records.length > 1}>
214
214
-
<div class="flex w-[22rem] items-center justify-between gap-x-2 sm:w-[24rem]">
215
215
-
<Button
216
216
-
onClick={() => {
217
217
-
setReverse(!reverse());
218
218
-
setRecords([]);
219
219
-
setCursor(undefined);
220
220
-
refetch();
221
221
-
}}
222
222
-
>
223
223
-
<span
224
224
-
class={`iconify ${reverse() ? "lucide--rotate-ccw" : "lucide--rotate-cw"} text-sm`}
225
225
-
></span>
226
226
-
Reverse
227
227
-
</Button>
228
228
-
<div>
229
229
-
<Show when={batchDelete()}>
230
230
-
<span>{records.filter((rec) => rec.toDelete).length}</span>
231
231
-
<span>/</span>
232
232
-
</Show>
233
233
-
<span>{records.length} records</span>
234
234
-
</div>
235
235
-
<div class="flex w-[5rem] items-center justify-end">
236
236
-
<Show when={cursor()}>
237
237
-
<Show when={!response.loading}>
238
238
-
<Button onClick={() => refetch()}>Load More</Button>
249
249
+
<div class="flex max-w-full flex-col font-mono">
250
250
+
<For
251
251
+
each={records.filter((rec) =>
252
252
+
filter() ? JSON.stringify(rec.record.value).includes(filter()!) : true,
253
253
+
)}
254
254
+
>
255
255
+
{(record, index) => (
256
256
+
<>
257
257
+
<Show when={batchDelete()}>
258
258
+
<label
259
259
+
class="flex items-center gap-1 select-none"
260
260
+
onclick={(e) => handleSelectionClick(e, index())}
261
261
+
>
262
262
+
<input
263
263
+
type="checkbox"
264
264
+
checked={record.toDelete}
265
265
+
onchange={(e) => setRecords(index(), "toDelete", e.currentTarget.checked)}
266
266
+
/>
267
267
+
<RecordLink record={record} />
268
268
+
</label>
239
269
</Show>
240
240
-
<Show when={response.loading}>
241
241
-
<div class="iconify lucide--loader-circle w-[5rem] animate-spin text-xl" />
270
270
+
<Show when={!batchDelete()}>
271
271
+
<A href={`/at://${did}/${params.collection}/${record.rkey}`}>
272
272
+
<RecordLink record={record} />
273
273
+
</A>
242
274
</Show>
243
243
-
</Show>
244
244
-
</div>
245
245
-
</div>
246
246
-
</Show>
247
247
-
</div>
248
248
-
<div class="flex max-w-full flex-col font-mono">
249
249
-
<For
250
250
-
each={records.filter((rec) =>
251
251
-
filter() ? JSON.stringify(rec.record.value).includes(filter()!) : true,
252
252
-
)}
253
253
-
>
254
254
-
{(record, index) => (
255
255
-
<>
256
256
-
<Show when={batchDelete()}>
257
257
-
<label
258
258
-
class="flex items-center gap-1 select-none"
259
259
-
onclick={(e) => handleSelectionClick(e, index())}
260
260
-
>
261
261
-
<input
262
262
-
type="checkbox"
263
263
-
checked={record.toDelete}
264
264
-
onchange={(e) => setRecords(index(), "toDelete", e.currentTarget.checked)}
265
265
-
/>
266
266
-
<RecordLink record={record} />
267
267
-
</label>
268
268
-
</Show>
269
269
-
<Show when={!batchDelete()}>
270
270
-
<A href={`/at://${did}/${params.collection}/${record.rkey}`}>
271
271
-
<RecordLink record={record} />
272
272
-
</A>
273
273
-
</Show>
274
274
-
</>
275
275
-
)}
276
276
-
</For>
275
275
+
</>
276
276
+
)}
277
277
+
</For>
278
278
+
</div>
277
279
</div>
278
280
</Show>
279
281
);
+2
-2
src/views/home.tsx
···
2
2
3
3
const Home = () => {
4
4
return (
5
5
-
<div class="mt-4 flex w-[22rem] flex-col gap-3 break-words sm:w-[24rem]">
5
5
+
<div class="flex w-[22rem] flex-col gap-4 break-words sm:w-[24rem]">
6
6
<div>
7
7
<div>
8
8
-
<span class="font-semibold">AT Protocol Explorer</span>
8
8
+
<span class="text-lg font-semibold">AT Protocol Explorer</span>
9
9
</div>
10
10
<div class="flex items-center gap-1">
11
11
<div class="iconify lucide--search" />
+8
-5
src/views/labels.tsx
···
59
59
};
60
60
61
61
return (
62
62
-
<>
63
63
-
<form class="mt-3 flex flex-col items-center gap-y-1" onsubmit={(e) => e.preventDefault()}>
62
62
+
<div class="flex w-full flex-col items-center">
63
63
+
<form
64
64
+
class="flex w-[22rem] flex-col items-center gap-y-1 sm:w-[24rem]"
65
65
+
onsubmit={(e) => e.preventDefault()}
66
66
+
>
64
67
<div class="w-full">
65
68
<label for="patterns" class="ml-0.5 text-sm">
66
69
URI Patterns (comma-separated)
67
70
</label>
68
71
</div>
69
69
-
<div class="flex w-[22rem] items-center gap-x-1 sm:w-[24rem]">
72
72
+
<div class="flex w-full items-center gap-x-1">
70
73
<textarea
71
74
id="patterns"
72
75
name="patterns"
···
120
123
</div>
121
124
</div>
122
125
<Show when={labels().length}>
123
123
-
<div class="flex w-[22rem] flex-col gap-2 divide-y-[0.5px] divide-neutral-400 text-sm wrap-anywhere whitespace-pre-wrap sm:min-w-[24rem] dark:divide-neutral-600">
126
126
+
<div class="flex max-w-full min-w-[22rem] flex-col gap-2 divide-y-[0.5px] divide-neutral-400 text-sm wrap-anywhere whitespace-pre-wrap sm:min-w-[24rem] dark:divide-neutral-600">
124
127
<For each={filterLabels()}>
125
128
{(label) => (
126
129
<div class="flex items-center justify-between gap-2 pb-2">
···
169
172
<Show when={!labels().length && !response.loading && searchParams.uriPatterns}>
170
173
<div class="mt-2">No results</div>
171
174
</Show>
172
172
-
</>
175
175
+
</div>
173
176
);
174
177
};
175
178
+1
-1
src/views/pds.tsx
···
48
48
49
49
return (
50
50
<Show when={repos() || response()}>
51
51
-
<div class="mt-3 flex w-[22rem] flex-col sm:w-[24rem]">
51
51
+
<div class="flex w-[22rem] flex-col sm:w-[24rem]">
52
52
<Show when={version()}>
53
53
{(version) => (
54
54
<div class="flex items-baseline gap-x-1">
+1
-1
src/views/record.tsx
···
120
120
return (
121
121
<Show when={record()} keyed>
122
122
<div class="flex w-full flex-col items-center">
123
123
-
<div class="dark:shadow-dark-800 dark:bg-dark-300 my-3 flex w-[22rem] justify-between rounded-lg bg-neutral-50 px-2 py-1.5 shadow-sm sm:w-[24rem]">
123
123
+
<div class="dark:shadow-dark-800 dark:bg-dark-300 mb-3 flex w-[22rem] justify-between rounded-lg bg-neutral-50 px-2 py-1.5 shadow-sm sm:w-[24rem]">
124
124
<div class="flex gap-3 text-sm">
125
125
<A
126
126
classList={{
+13
-3
src/views/repo.tsx
···
7
7
processIndexedEntryLog,
8
8
} from "@atcute/did-plc";
9
9
import { DidDocument } from "@atcute/identity";
10
10
-
import { ActorIdentifier } from "@atcute/lexicons";
10
10
+
import { ActorIdentifier, Handle } from "@atcute/lexicons";
11
11
+
import { resolveHandle } from "@atcute/oauth-browser-client";
11
12
import { A, useLocation, useNavigate, useParams } from "@solidjs/router";
12
13
import { createResource, createSignal, ErrorBoundary, For, Show, Suspense } from "solid-js";
13
14
import { Backlinks } from "../components/backlinks.jsx";
···
184
185
);
185
186
186
187
const fetchRepo = async () => {
187
187
-
pds = await resolvePDS(did);
188
188
+
try {
189
189
+
pds = await resolvePDS(did);
190
190
+
} catch {
191
191
+
try {
192
192
+
const did = await resolveHandle(params.repo as Handle);
193
193
+
navigate(location.pathname.replace(params.repo, did));
194
194
+
} catch {
195
195
+
navigate(`/${did}`);
196
196
+
}
197
197
+
}
188
198
setDidDoc(didDocCache[did] as DidDocument);
189
199
190
200
rpc = new Client({ handler: new CredentialManager({ service: pds }) });
···
257
267
258
268
return (
259
269
<Show when={repo()}>
260
260
-
<div class="mt-3 flex w-[22rem] flex-col gap-2 break-words sm:w-[24rem]">
270
270
+
<div class="flex w-[22rem] flex-col gap-2 break-words sm:w-[24rem]">
261
271
<Show when={error()}>
262
272
<div class="rounded-lg bg-red-100 p-2 text-sm text-red-700 dark:bg-red-200 dark:text-red-600">
263
273
{error()}