+139
-82
src/components/create.tsx
+139
-82
src/components/create.tsx
···
1
1
import { Client } from "@atcute/client";
2
2
import { remove } from "@mary/exif-rm";
3
3
import { useNavigate, useParams } from "@solidjs/router";
4
-
import { createSignal, Show } from "solid-js";
4
+
import { createSignal, onCleanup, Show } from "solid-js";
5
5
import { Editor, editorView } from "../components/editor.jsx";
6
6
import { agent } from "../components/login.jsx";
7
7
import { setNotif } from "../layout.jsx";
···
15
15
const params = useParams();
16
16
const [openDialog, setOpenDialog] = createSignal(false);
17
17
const [notice, setNotice] = createSignal("");
18
-
const [uploading, setUploading] = createSignal(false);
18
+
const [openUpload, setOpenUpload] = createSignal(false);
19
+
let blobInput!: HTMLInputElement;
19
20
let formRef!: HTMLFormElement;
20
21
21
22
const placeholder = () => {
···
125
126
}
126
127
};
127
128
128
-
const uploadBlob = async () => {
129
-
setNotice("");
130
-
let blob: Blob;
129
+
const FileUpload = (props: { file: File }) => {
130
+
const [uploading, setUploading] = createSignal(false);
131
+
const [error, setError] = createSignal("");
131
132
132
-
const file = (document.getElementById("blob") as HTMLInputElement)?.files?.[0];
133
-
if (!file) return;
133
+
onCleanup(() => (blobInput.value = ""));
134
134
135
-
const mimetype = (document.getElementById("mimetype") as HTMLInputElement)?.value;
136
-
(document.getElementById("mimetype") as HTMLInputElement).value = "";
137
-
if (mimetype) blob = new Blob([file], { type: mimetype });
138
-
else blob = file;
135
+
const formatFileSize = (bytes: number) => {
136
+
if (bytes === 0) return "0 Bytes";
137
+
const k = 1024;
138
+
const sizes = ["Bytes", "KB", "MB", "GB"];
139
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
140
+
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
141
+
};
142
+
143
+
const uploadBlob = async () => {
144
+
let blob: Blob;
145
+
146
+
const mimetype = (document.getElementById("mimetype") as HTMLInputElement)?.value;
147
+
(document.getElementById("mimetype") as HTMLInputElement).value = "";
148
+
if (mimetype) blob = new Blob([props.file], { type: mimetype });
149
+
else blob = props.file;
150
+
151
+
if ((document.getElementById("exif-rm") as HTMLInputElement).checked) {
152
+
const exifRemoved = remove(new Uint8Array(await blob.arrayBuffer()));
153
+
if (exifRemoved !== null) blob = new Blob([exifRemoved], { type: blob.type });
154
+
}
139
155
140
-
if ((document.getElementById("exif-rm") as HTMLInputElement).checked) {
141
-
const exifRemoved = remove(new Uint8Array(await blob.arrayBuffer()));
142
-
if (exifRemoved !== null) blob = new Blob([exifRemoved], { type: blob.type });
143
-
}
156
+
const rpc = new Client({ handler: agent()! });
157
+
setUploading(true);
158
+
const res = await rpc.post("com.atproto.repo.uploadBlob", {
159
+
input: blob,
160
+
});
161
+
setUploading(false);
162
+
if (!res.ok) {
163
+
setError(res.data.error);
164
+
return;
165
+
}
166
+
editorView.dispatch({
167
+
changes: {
168
+
from: editorView.state.selection.main.head,
169
+
insert: JSON.stringify(res.data.blob, null, 2),
170
+
},
171
+
});
172
+
setOpenUpload(false);
173
+
};
144
174
145
-
const rpc = new Client({ handler: agent()! });
146
-
setUploading(true);
147
-
const res = await rpc.post("com.atproto.repo.uploadBlob", {
148
-
input: blob,
149
-
});
150
-
setUploading(false);
151
-
(document.getElementById("blob") as HTMLInputElement).value = "";
152
-
if (!res.ok) {
153
-
setNotice(res.data.error);
154
-
return;
155
-
}
156
-
editorView.dispatch({
157
-
changes: {
158
-
from: editorView.state.selection.main.head,
159
-
insert: JSON.stringify(res.data.blob, null, 2),
160
-
},
161
-
});
175
+
return (
176
+
<div class="dark:bg-dark-300 dark:shadow-dark-800 absolute top-70 left-[50%] w-[20rem] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0">
177
+
<h2 class="mb-2 font-semibold">Upload blob</h2>
178
+
<div class="flex flex-col gap-2 text-sm">
179
+
<div class="flex flex-col gap-1">
180
+
<p class="flex gap-1">
181
+
<span class="truncate">{props.file.name}</span>
182
+
<span class="shrink-0 text-neutral-600 dark:text-neutral-400">
183
+
({formatFileSize(props.file.size)})
184
+
</span>
185
+
</p>
186
+
</div>
187
+
<div class="flex items-center gap-x-2">
188
+
<label for="mimetype" class="shrink-0 select-none">
189
+
MIME type
190
+
</label>
191
+
<TextInput id="mimetype" placeholder={props.file.type} />
192
+
</div>
193
+
<div class="flex items-center gap-1">
194
+
<input id="exif-rm" type="checkbox" checked />
195
+
<label for="exif-rm" class="select-none">
196
+
Remove EXIF data
197
+
</label>
198
+
</div>
199
+
<p class="text-xs text-neutral-600 dark:text-neutral-400">
200
+
Metadata will be pasted after the cursor
201
+
</p>
202
+
<Show when={error()}>
203
+
<span class="text-red-500 dark:text-red-400">Error: {error()}</span>
204
+
</Show>
205
+
<div class="flex justify-between gap-2">
206
+
<Button onClick={() => setOpenUpload(false)}>Cancel</Button>
207
+
<Show when={uploading()}>
208
+
<div class="flex items-center gap-1">
209
+
<span class="iconify lucide--loader-circle animate-spin"></span>
210
+
<span>Uploading</span>
211
+
</div>
212
+
</Show>
213
+
<Show when={!uploading()}>
214
+
<Button
215
+
onClick={uploadBlob}
216
+
class="dark:shadow-dark-800 flex items-center gap-1 rounded-lg bg-blue-500 px-2 py-1.5 text-xs text-white shadow-xs select-none hover:bg-blue-600 active:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 dark:active:bg-blue-400"
217
+
>
218
+
Upload
219
+
</Button>
220
+
</Show>
221
+
</div>
222
+
</div>
223
+
</div>
224
+
);
162
225
};
163
226
164
227
return (
···
180
243
</button>
181
244
</div>
182
245
<form ref={formRef} class="flex flex-col gap-y-2">
183
-
<div class="flex w-fit flex-col gap-y-1 text-xs sm:text-sm">
246
+
<div class="flex w-fit flex-col gap-y-1 text-sm">
184
247
<Show when={props.create}>
185
248
<div class="flex items-center gap-x-2">
186
249
<label for="collection" class="min-w-20 select-none">
···
189
252
<TextInput
190
253
id="collection"
191
254
name="collection"
192
-
placeholder="Optional (default: record type)"
255
+
placeholder="Optional (default: $type)"
193
256
class="w-[15rem]"
194
257
/>
195
258
</div>
···
219
282
<option value="false">False</option>
220
283
</select>
221
284
</div>
222
-
<div class="flex items-center gap-2">
223
-
<Show when={!uploading()}>
224
-
<div class="dark:hover:bg-dark-200 dark:shadow-dark-800 dark:active:bg-dark-100 flex rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 text-xs font-semibold shadow-xs hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800">
225
-
<input type="file" id="blob" class="sr-only" onChange={() => uploadBlob()} />
226
-
<label class="flex items-center gap-1 px-2 py-1.5 select-none" for="blob">
227
-
<span class="iconify lucide--upload text-sm"></span>
228
-
Upload
229
-
</label>
230
-
</div>
231
-
<p class="text-xs">Metadata will be pasted after the cursor</p>
232
-
</Show>
233
-
<Show when={uploading()}>
234
-
<span class="iconify lucide--loader-circle animate-spin text-xl"></span>
235
-
<p>Uploading...</p>
236
-
</Show>
237
-
</div>
238
-
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
239
-
<div class="flex items-center gap-x-2">
240
-
<label for="mimetype" class="min-w-20 select-none">
241
-
MIME type
242
-
</label>
243
-
<TextInput id="mimetype" placeholder="Optional" class="w-[15rem]" />
244
-
</div>
245
-
<div class="flex items-center gap-1">
246
-
<input id="exif-rm" type="checkbox" checked />
247
-
<label for="exif-rm" class="select-none">
248
-
Remove EXIF data
249
-
</label>
250
-
</div>
251
-
</div>
252
285
</div>
253
286
<Editor
254
287
content={JSON.stringify(props.create ? placeholder() : props.record, null, 2)}
255
288
/>
256
289
<div class="flex flex-col gap-2">
257
290
<Show when={notice()}>
258
-
<div class="text-red-500 dark:text-red-400">{notice()}</div>
291
+
<div class="text-sm text-red-500 dark:text-red-400">{notice()}</div>
259
292
</Show>
260
-
<div class="flex items-center justify-end gap-2">
261
-
<Show when={!props.create}>
262
-
<div class="flex items-center gap-1">
263
-
<input id="recreate" name="recreate" type="checkbox" />
264
-
<label for="recreate" class="text-sm select-none">
265
-
Recreate record
266
-
</label>
267
-
</div>
268
-
</Show>
269
-
<Button
270
-
onClick={() =>
271
-
props.create ?
272
-
createRecord(new FormData(formRef))
273
-
: editRecord(new FormData(formRef))
274
-
}
293
+
<div class="flex justify-between gap-2">
294
+
<div class="dark:hover:bg-dark-200 dark:shadow-dark-800 dark:active:bg-dark-100 flex w-fit rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 text-xs shadow-xs hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800">
295
+
<input
296
+
type="file"
297
+
id="blob"
298
+
class="sr-only"
299
+
ref={blobInput}
300
+
onChange={(e) => {
301
+
if (e.target.files !== null) setOpenUpload(true);
302
+
}}
303
+
/>
304
+
<label class="flex items-center gap-1 px-2 py-1.5 select-none" for="blob">
305
+
<span class="iconify lucide--upload"></span>
306
+
Upload
307
+
</label>
308
+
</div>
309
+
<Modal
310
+
open={openUpload()}
311
+
onClose={() => setOpenUpload(false)}
312
+
closeOnClick={false}
275
313
>
276
-
{props.create ? "Create" : "Edit"}
277
-
</Button>
314
+
<FileUpload file={blobInput.files![0]} />
315
+
</Modal>
316
+
<div class="flex items-center justify-end gap-2">
317
+
<Show when={!props.create}>
318
+
<div class="flex items-center gap-1">
319
+
<input id="recreate" name="recreate" type="checkbox" />
320
+
<label for="recreate" class="text-sm select-none">
321
+
Recreate record
322
+
</label>
323
+
</div>
324
+
</Show>
325
+
<Button
326
+
onClick={() =>
327
+
props.create ?
328
+
createRecord(new FormData(formRef))
329
+
: editRecord(new FormData(formRef))
330
+
}
331
+
>
332
+
{props.create ? "Create" : "Edit"}
333
+
</Button>
334
+
</div>
278
335
</div>
279
336
</div>
280
337
</form>
+62
-6
src/components/search.tsx
+62
-6
src/components/search.tsx
···
2
2
import { A, useLocation, useNavigate } from "@solidjs/router";
3
3
import { createResource, createSignal, For, onCleanup, onMount, Show } from "solid-js";
4
4
import { isTouchDevice } from "../layout";
5
-
import { appHandleLink, appList, AppUrl } from "../utils/app-urls";
5
+
import { appHandleLink, appList, appName, AppUrl } from "../utils/app-urls";
6
6
import { createDebouncedValue } from "../utils/hooks/debounced";
7
+
import { Modal } from "./modal";
7
8
8
9
export const [showSearch, setShowSearch] = createSignal(false);
9
10
···
67
68
input = input.trim().replace(/^@/, "");
68
69
if (!input.length) return;
69
70
setShowSearch(false);
70
-
if (input === "me" && localStorage.getItem("lastSignedIn") !== null) {
71
-
navigate(`/at://${localStorage.getItem("lastSignedIn")}`);
72
-
} else if (search()?.length) {
71
+
if (search()?.length) {
73
72
navigate(`/at://${search()![0].did}`);
74
73
} else if (input.startsWith("https://") || input.startsWith("http://")) {
75
74
const hostLength = input.indexOf("/", 8);
···
87
86
} else {
88
87
navigate(`/at://${input.replace("at://", "")}`);
89
88
}
90
-
setShowSearch(false);
91
89
};
92
90
93
91
return (
···
116
114
value={input() ?? ""}
117
115
onInput={(e) => setInput(e.currentTarget.value)}
118
116
/>
119
-
<Show when={input()}>
117
+
<Show when={input()} fallback={ListUrlsTooltip()}>
120
118
<button
121
119
type="button"
122
120
class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-600 dark:active:bg-neutral-500"
···
146
144
</div>
147
145
</Show>
148
146
</form>
147
+
);
148
+
};
149
+
150
+
const ListUrlsTooltip = () => {
151
+
const [openList, setOpenList] = createSignal(false);
152
+
153
+
let urls: Record<string, AppUrl[]> = {};
154
+
for (const [appUrl, appView] of Object.entries(appList)) {
155
+
if (!urls[appView]) urls[appView] = [appUrl as AppUrl];
156
+
else urls[appView].push(appUrl as AppUrl);
157
+
}
158
+
159
+
return (
160
+
<>
161
+
<Modal open={openList()} onClose={() => setOpenList(false)}>
162
+
<div class="dark:bg-dark-300 dark:shadow-dark-800 absolute top-16 left-[50%] w-[22rem] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 sm:w-[26rem] dark:border-neutral-700 starting:opacity-0">
163
+
<div class="mb-2 flex items-center gap-1 font-semibold">
164
+
<span class="iconify lucide--link"></span>
165
+
<span>Supported URLs</span>
166
+
</div>
167
+
<div class="mb-2 text-sm text-neutral-600 dark:text-neutral-400">
168
+
Links that will be parsed automatically, as long as all the data necessary is on the
169
+
URL.
170
+
</div>
171
+
<div class="flex flex-col gap-2 text-sm">
172
+
<For each={Object.entries(appName)}>
173
+
{([appView, name]) => {
174
+
return (
175
+
<div>
176
+
<p class="font-semibold">{name}</p>
177
+
<div class="grid grid-cols-2 gap-x-4 text-neutral-600 dark:text-neutral-400">
178
+
<For each={urls[appView]}>
179
+
{(url) => (
180
+
<a
181
+
href={`${url.startsWith("localhost:") ? "http://" : "https://"}${url}`}
182
+
target="_blank"
183
+
class="hover:underline active:underline"
184
+
>
185
+
{url}
186
+
</a>
187
+
)}
188
+
</For>
189
+
</div>
190
+
</div>
191
+
);
192
+
}}
193
+
</For>
194
+
</div>
195
+
</div>
196
+
</Modal>
197
+
<button
198
+
type="button"
199
+
class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-600 dark:active:bg-neutral-500"
200
+
onClick={() => setOpenList(true)}
201
+
>
202
+
<span class="iconify lucide--help-circle"></span>
203
+
</button>
204
+
</>
149
205
);
150
206
};
151
207
+9
src/utils/app-urls.ts
+9
src/utils/app-urls.ts
···
9
9
Linkat,
10
10
}
11
11
12
+
export const appName = {
13
+
[App.Bluesky]: "Bluesky",
14
+
[App.Tangled]: "Tangled",
15
+
[App.Whitewind]: "Whitewind",
16
+
[App.Frontpage]: "Frontpage",
17
+
[App.Pinksea]: "Pinksea",
18
+
[App.Linkat]: "Linkat",
19
+
};
20
+
12
21
export const appList: Record<AppUrl, App> = {
13
22
"localhost:19006": App.Bluesky,
14
23
"blacksky.community": App.Bluesky,
+3
-3
src/views/collection.tsx
+3
-3
src/views/collection.tsx
···
41
41
42
42
return (
43
43
<span
44
-
class="relative flex w-full items-baseline rounded px-0.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
44
+
class="relative flex w-full min-w-0 items-baseline rounded px-0.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
45
45
ref={rkeyRef}
46
46
onmouseover={() => setHover(true)}
47
47
onmouseleave={() => setHover(false)}
···
267
267
<Button onClick={() => setOpenDelete(false)}>Cancel</Button>
268
268
<Button
269
269
onClick={deleteRecords}
270
-
class={`dark:shadow-dark-800 rounded-lg px-2 py-1.5 text-xs font-semibold text-neutral-200 shadow-xs select-none ${recreate() ? "bg-green-500 hover:bg-green-400 dark:bg-green-600 dark:hover:bg-green-500" : "bg-red-500 hover:bg-red-400 active:bg-red-400"}`}
270
+
class={`dark:shadow-dark-800 rounded-lg px-2 py-1.5 text-xs text-white shadow-xs select-none ${recreate() ? "bg-green-500 hover:bg-green-400 dark:bg-green-600 dark:hover:bg-green-500" : "bg-red-500 hover:bg-red-400 active:bg-red-400"}`}
271
271
>
272
272
{recreate() ? "Recreate" : "Delete"}
273
273
</Button>
···
301
301
}}
302
302
>
303
303
<span
304
-
class={`iconify ${reverse() ? "lucide--rotate-ccw" : "lucide--rotate-cw"} text-sm`}
304
+
class={`iconify ${reverse() ? "lucide--rotate-ccw" : "lucide--rotate-cw"}`}
305
305
></span>
306
306
Reverse
307
307
</Button>
+4
-6
src/views/labels.tsx
+4
-6
src/views/labels.tsx
···
68
68
initQuery();
69
69
}}
70
70
>
71
-
<div class="w-full">
72
-
<label for="patterns" class="ml-0.5 text-sm">
73
-
URI Patterns (comma-separated)
74
-
</label>
75
-
</div>
76
-
<div class="flex w-full items-center gap-x-1">
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">
77
75
<textarea
78
76
id="patterns"
79
77
name="patterns"
+1
src/views/logs.tsx
+1
src/views/logs.tsx
···
37
37
classList={{
38
38
"flex items-center rounded-full p-1.5": true,
39
39
"bg-neutral-700 dark:bg-neutral-200": activePlcEvent() === props.event,
40
+
"hover:bg-neutral-200 dark:hover:bg-neutral-700": activePlcEvent() !== props.event,
40
41
}}
41
42
onclick={() => setActivePlcEvent(activePlcEvent() === props.event ? undefined : props.event)}
42
43
>
+133
-43
src/views/pds.tsx
+133
-43
src/views/pds.tsx
···
2
2
import { Client, CredentialManager } from "@atcute/client";
3
3
import { InferXRPCBodyOutput } from "@atcute/lexicons";
4
4
import * as TID from "@atcute/tid";
5
-
import { A, useParams } from "@solidjs/router";
5
+
import { A, useLocation, useParams } from "@solidjs/router";
6
6
import { createResource, createSignal, For, Show } from "solid-js";
7
7
import { Button } from "../components/button";
8
8
import { Modal } from "../components/modal";
9
9
import { setPDS } from "../components/navbar";
10
10
import Tooltip from "../components/tooltip";
11
+
import { addToClipboard } from "../utils/copy";
11
12
import { localDateFromTimestamp } from "../utils/date";
12
13
13
14
const LIMIT = 1000;
14
15
15
16
const PdsView = () => {
16
17
const params = useParams();
18
+
const location = useLocation();
17
19
const [version, setVersion] = createSignal<string>();
18
20
const [serverInfos, setServerInfos] =
19
21
createSignal<InferXRPCBodyOutput<ComAtprotoServerDescribeServer.mainSchema["output"]>>();
···
28
30
setVersion((res.data as any).version);
29
31
};
30
32
33
+
const describeServer = async () => {
34
+
const res = await rpc.get("com.atproto.server.describeServer");
35
+
if (!res.ok) console.error(res.data.error);
36
+
else setServerInfos(res.data);
37
+
};
38
+
31
39
const fetchRepos = async () => {
32
40
getVersion();
33
-
const describeRes = await rpc.get("com.atproto.server.describeServer");
34
-
if (!describeRes.ok) console.error(describeRes.data.error);
35
-
else setServerInfos(describeRes.data);
41
+
describeServer();
36
42
const res = await rpc.get("com.atproto.sync.listRepos", {
37
43
params: { limit: LIMIT, cursor: cursor() },
38
44
});
···
58
64
</A>
59
65
<Show when={!repo.active}>
60
66
<Tooltip text={repo.status ?? "Unknown status"}>
61
-
<span class="iconify lucide--unplug"></span>
67
+
<span class="iconify lucide--unplug text-red-500 dark:text-red-400"></span>
62
68
</Tooltip>
63
69
</Show>
64
70
<button
···
103
109
);
104
110
};
105
111
112
+
const Tab = (props: { tab: "repos" | "info"; label: string }) => (
113
+
<div class="flex items-center gap-0.5">
114
+
<A
115
+
classList={{
116
+
"flex items-center gap-1 border-b-2": true,
117
+
"border-transparent hover:border-neutral-400 dark:hover:border-neutral-600":
118
+
(!!location.hash && location.hash !== `#${props.tab}`) ||
119
+
(!location.hash && props.tab !== "repos"),
120
+
}}
121
+
href={`/${params.pds}#${props.tab}`}
122
+
>
123
+
{props.label}
124
+
</A>
125
+
</div>
126
+
);
127
+
106
128
return (
107
129
<Show when={repos() || response()}>
108
-
<div class="flex w-full flex-col px-2">
109
-
<Show when={version()}>
110
-
{(version) => (
111
-
<div class="flex items-baseline gap-x-1">
112
-
<span class="font-semibold">Version</span>
113
-
<span class="truncate text-sm">{version()}</span>
130
+
<div class="flex w-full flex-col">
131
+
<div class="dark:shadow-dark-800 dark:bg-dark-300 mb-2 flex w-full justify-between rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-2 py-1.5 text-sm shadow-xs dark:border-neutral-700">
132
+
<div class="flex gap-3">
133
+
<Tab tab="repos" label="Repositories" />
134
+
<Tab tab="info" label="Info" />
135
+
</div>
136
+
<div class="flex gap-1">
137
+
<Tooltip text="Copy PDS">
138
+
<button
139
+
onClick={() => addToClipboard(params.pds)}
140
+
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"
141
+
>
142
+
<span class="iconify lucide--copy"></span>
143
+
</button>
144
+
</Tooltip>
145
+
<Tooltip text="Firehose">
146
+
<A
147
+
href={`/firehose?instance=wss://${params.pds}`}
148
+
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"
149
+
>
150
+
<span class="iconify lucide--radio-tower"></span>
151
+
</A>
152
+
</Tooltip>
153
+
</div>
154
+
</div>
155
+
<div class="flex flex-col gap-1 px-2">
156
+
<Show when={!location.hash || location.hash === "#repos"}>
157
+
<div class="flex flex-col divide-y-[0.5px] divide-neutral-300 dark:divide-neutral-700">
158
+
<For each={repos()}>{(repo) => <RepoCard {...repo} />}</For>
114
159
</div>
115
-
)}
116
-
</Show>
117
-
<Show when={serverInfos()}>
118
-
{(server) => (
119
-
<>
120
-
<Show when={server().inviteCodeRequired}>
121
-
<span class="font-semibold">Invite Code Required</span>
122
-
</Show>
123
-
<Show when={server().phoneVerificationRequired}>
124
-
<span class="font-semibold">Phone Verification Required</span>
125
-
</Show>
126
-
<Show when={server().availableUserDomains.length}>
127
-
<div class="flex flex-col">
128
-
<span class="font-semibold">Available User Domains</span>
129
-
<For each={server().availableUserDomains}>
130
-
{(domain) => <span class="text-sm wrap-anywhere">{domain}</span>}
131
-
</For>
160
+
</Show>
161
+
<Show when={location.hash === "#info"}>
162
+
<Show when={version()}>
163
+
{(version) => (
164
+
<div class="flex items-baseline gap-x-1">
165
+
<span class="font-semibold">Version</span>
166
+
<span class="truncate text-sm">{version()}</span>
132
167
</div>
133
-
</Show>
134
-
</>
135
-
)}
136
-
</Show>
137
-
<p class="w-full font-semibold">{repos()?.length} Repositories</p>
138
-
<div class="flex flex-col divide-y-[0.5px] divide-neutral-300 dark:divide-neutral-700">
139
-
<For each={repos()}>{(repo) => <RepoCard {...repo} />}</For>
168
+
)}
169
+
</Show>
170
+
<Show when={serverInfos()}>
171
+
{(server) => (
172
+
<>
173
+
<div class="flex items-baseline gap-x-1">
174
+
<span class="font-semibold">DID</span>
175
+
<span class="truncate text-sm">{server().did}</span>
176
+
</div>
177
+
<Show when={server().inviteCodeRequired}>
178
+
<span class="font-semibold">Invite Code Required</span>
179
+
</Show>
180
+
<Show when={server().phoneVerificationRequired}>
181
+
<span class="font-semibold">Phone Verification Required</span>
182
+
</Show>
183
+
<Show when={server().availableUserDomains.length}>
184
+
<div class="flex flex-col">
185
+
<span class="font-semibold">Available User Domains</span>
186
+
<For each={server().availableUserDomains}>
187
+
{(domain) => <span class="text-sm wrap-anywhere">{domain}</span>}
188
+
</For>
189
+
</div>
190
+
</Show>
191
+
<Show when={server().links?.privacyPolicy}>
192
+
<div class="flex flex-col">
193
+
<span class="font-semibold">Privacy Policy</span>
194
+
<a
195
+
href={server().links?.privacyPolicy}
196
+
class="text-sm hover:underline"
197
+
target="_blank"
198
+
>
199
+
{server().links?.privacyPolicy}
200
+
</a>
201
+
</div>
202
+
</Show>
203
+
<Show when={server().links?.termsOfService}>
204
+
<div class="flex flex-col">
205
+
<span class="font-semibold">Terms of Service</span>
206
+
<a
207
+
href={server().links?.termsOfService}
208
+
class="text-sm hover:underline"
209
+
target="_blank"
210
+
>
211
+
{server().links?.termsOfService}
212
+
</a>
213
+
</div>
214
+
</Show>
215
+
<Show when={server().contact?.email}>
216
+
<div class="flex flex-col">
217
+
<span class="font-semibold">Contact</span>
218
+
<a href={`mailto:${server().contact?.email}`} class="text-sm hover:underline">
219
+
{server().contact?.email}
220
+
</a>
221
+
</div>
222
+
</Show>
223
+
</>
224
+
)}
225
+
</Show>
226
+
</Show>
140
227
</div>
141
228
</div>
142
-
<Show when={cursor()}>
143
-
<div class="dark:bg-dark-500 fixed bottom-0 z-5 flex w-screen justify-center bg-neutral-100 py-3">
144
-
<Show when={!response.loading}>
145
-
<Button onClick={() => refetch()}>Load More</Button>
146
-
</Show>
147
-
<Show when={response.loading}>
148
-
<span class="iconify lucide--loader-circle animate-spin py-3.5 text-xl"></span>
149
-
</Show>
229
+
<Show when={!location.hash || location.hash === "#repos"}>
230
+
<div class="dark:bg-dark-500 fixed bottom-0 z-5 flex w-screen justify-center bg-neutral-100 py-2">
231
+
<div class="flex flex-col items-center gap-1 pb-2">
232
+
<p>{repos()?.length} loaded</p>
233
+
<Show when={!response.loading && cursor()}>
234
+
<Button onClick={() => refetch()}>Load More</Button>
235
+
</Show>
236
+
<Show when={response.loading}>
237
+
<span class="iconify lucide--loader-circle animate-spin py-3.5 text-xl"></span>
238
+
</Show>
239
+
</div>
150
240
</div>
151
241
</Show>
152
242
</Show>
+6
-5
src/views/record.tsx
+6
-5
src/views/record.tsx
···
10
10
import { JSONValue } from "../components/json.jsx";
11
11
import { agent } from "../components/login.jsx";
12
12
import { Modal } from "../components/modal.jsx";
13
-
import { pds, setCID } from "../components/navbar.jsx";
13
+
import { pds } from "../components/navbar.jsx";
14
14
import Tooltip from "../components/tooltip.jsx";
15
15
import { setNotif } from "../layout.jsx";
16
16
import { didDocCache, resolveLexiconAuthority, resolvePDS } from "../utils/api.js";
···
34
34
let rpc: Client;
35
35
36
36
const fetchRecord = async () => {
37
-
setCID(undefined);
38
37
setValidRecord(undefined);
39
38
setValidSchema(undefined);
40
39
setLexiconUri(undefined);
···
52
51
setNotice(res.data.error);
53
52
throw new Error(res.data.error);
54
53
}
55
-
setCID(res.data.cid);
56
54
setExternalLink(checkUri(res.data.uri, res.data.value));
57
55
resolveLexicon(params.collection as Nsid);
58
56
verify(res.data);
···
179
177
<Button onClick={() => setOpenDelete(false)}>Cancel</Button>
180
178
<Button
181
179
onClick={deleteRecord}
182
-
class="dark:shadow-dark-800 rounded-lg bg-red-500 px-2 py-1.5 text-xs font-semibold text-neutral-200 shadow-xs select-none hover:bg-red-400 active:bg-red-400"
180
+
class="dark:shadow-dark-800 rounded-lg bg-red-500 px-2 py-1.5 text-xs text-white shadow-xs select-none hover:bg-red-400 active:bg-red-400"
183
181
>
184
182
Delete
185
183
</Button>
···
198
196
label="Copy record"
199
197
icon="lucide--copy"
200
198
/>
199
+
<Show when={record()?.cid}>
200
+
{(cid) => <CopyMenu copyContent={cid()} label="Copy CID" icon="lucide--copy" />}
201
+
</Show>
201
202
<Show when={externalLink()}>
202
203
{(externalLink) => (
203
204
<NavMenu
···
286
287
<div>
287
288
<div class="flex items-center gap-1">
288
289
<span class="iconify lucide--scroll-text"></span>
289
-
<p class="font-semibold">Lexicon document</p>
290
+
<p class="font-semibold">Lexicon schema</p>
290
291
</div>
291
292
<div class="truncate text-xs">
292
293
<A