+65
src/components/file-drop-zone.tsx
+65
src/components/file-drop-zone.tsx
···
···
1
+
import type { JSX } from 'solid-js';
2
+
3
+
import { createDropZone, type CreateDropZoneOptions } from '~/lib/hooks/dropzone';
4
+
5
+
import Button from './inputs/button';
6
+
7
+
interface FileDropZoneProps {
8
+
accept?: string;
9
+
disabled?: boolean;
10
+
onFiles: (files: File[]) => void;
11
+
dataTypes?: CreateDropZoneOptions['dataTypes'];
12
+
multiple?: boolean;
13
+
children?: JSX.Element;
14
+
}
15
+
16
+
const FileDropZone = (props: FileDropZoneProps) => {
17
+
const { ref: dropRef, isDropping } = createDropZone({
18
+
dataTypes: props.dataTypes,
19
+
multiple: props.multiple ?? false,
20
+
onDrop(files) {
21
+
if (files) {
22
+
props.onFiles(files);
23
+
}
24
+
},
25
+
});
26
+
27
+
const handleBrowse = () => {
28
+
const input = document.createElement('input');
29
+
input.type = 'file';
30
+
if (props.accept) {
31
+
input.accept = props.accept;
32
+
}
33
+
if (props.multiple) {
34
+
input.multiple = true;
35
+
}
36
+
input.oninput = () => {
37
+
const files = Array.from(input.files!);
38
+
if (files.length > 0) {
39
+
props.onFiles(files);
40
+
}
41
+
};
42
+
input.click();
43
+
};
44
+
45
+
return (
46
+
<fieldset
47
+
ref={dropRef}
48
+
disabled={props.disabled}
49
+
class={
50
+
`relative grid place-items-center rounded border border-gray-300 px-6 py-12 disabled:opacity-50` +
51
+
(props.disabled || !isDropping() ? ` bg-gray-100` : ` bg-green-100`)
52
+
}
53
+
>
54
+
<div class="flex flex-col items-center gap-4">
55
+
<Button variant="outline" onClick={handleBrowse}>
56
+
Browse files
57
+
</Button>
58
+
<p class="select-none font-medium text-gray-600">or drop your file here</p>
59
+
</div>
60
+
{props.children}
61
+
</fieldset>
62
+
);
63
+
};
64
+
65
+
export default FileDropZone;
+22
src/components/page-header.tsx
+22
src/components/page-header.tsx
···
···
1
+
import type { JSX } from 'solid-js';
2
+
3
+
interface PageHeaderProps {
4
+
title: string;
5
+
subtitle?: string;
6
+
children?: JSX.Element;
7
+
}
8
+
9
+
const PageHeader = (props: PageHeaderProps) => {
10
+
return (
11
+
<>
12
+
<div class="p-4">
13
+
<h1 class="text-lg font-bold text-purple-800">{props.title}</h1>
14
+
{props.subtitle && <p class="text-gray-600">{props.subtitle}</p>}
15
+
{props.children}
16
+
</div>
17
+
<hr class="mx-4 border-gray-300" />
18
+
</>
19
+
);
20
+
};
21
+
22
+
export default PageHeader;
+2
-5
src/views/blob/blob-export.tsx
+2
-5
src/views/blob/blob-export.tsx
···
17
import Button from '~/components/inputs/button';
18
import TextInput from '~/components/inputs/text-input';
19
import Logger, { createLogger } from '~/components/logger';
20
21
const BlobExportPage = () => {
22
const logger = createLogger();
···
229
230
return (
231
<>
232
-
<div class="p-4">
233
-
<h1 class="text-lg font-bold text-purple-800">Export blobs</h1>
234
-
<p class="text-gray-600">Download all blobs from an account into a tarball</p>
235
-
</div>
236
-
<hr class="mx-4 border-gray-300" />
237
238
<form
239
onSubmit={(ev) => {
···
17
import Button from '~/components/inputs/button';
18
import TextInput from '~/components/inputs/text-input';
19
import Logger, { createLogger } from '~/components/logger';
20
+
import PageHeader from '~/components/page-header';
21
22
const BlobExportPage = () => {
23
const logger = createLogger();
···
230
231
return (
232
<>
233
+
<PageHeader title="Export blobs" subtitle="Download all blobs from an account into a tarball" />
234
235
<form
236
onSubmit={(ev) => {
+2
-5
src/views/bluesky/threadgate-applicator/page.tsx
+2
-5
src/views/bluesky/threadgate-applicator/page.tsx
···
10
11
import { useTitle } from '~/lib/navigation/router';
12
13
import { Wizard } from '~/components/wizard';
14
15
import Step1_HandleInput from './steps/step1_handle-input';
···
80
81
return (
82
<>
83
-
<div class="p-4">
84
-
<h1 class="text-lg font-bold text-purple-800">Retroactive thread gating</h1>
85
-
<p class="text-gray-600">Set reply permissions on all of your past Bluesky posts</p>
86
-
</div>
87
-
<hr class="mx-4 border-gray-300" />
88
89
<Wizard<ThreadgateApplicatorConstraints>
90
initialStep="Step1_HandleInput"
···
10
11
import { useTitle } from '~/lib/navigation/router';
12
13
+
import PageHeader from '~/components/page-header';
14
import { Wizard } from '~/components/wizard';
15
16
import Step1_HandleInput from './steps/step1_handle-input';
···
81
82
return (
83
<>
84
+
<PageHeader title="Retroactive thread gating" subtitle="Set reply permissions on all of your past Bluesky posts" />
85
86
<Wizard<ThreadgateApplicatorConstraints>
87
initialStep="Step1_HandleInput"
+2
-5
src/views/crypto/crypto-generate.tsx
+2
-5
src/views/crypto/crypto-generate.tsx
···
6
7
import Button from '~/components/inputs/button';
8
import RadioInput from '~/components/inputs/radio-input';
9
10
type KeyType = 'p256' | 'secp256k1';
11
···
26
27
return (
28
<>
29
-
<div class="p-4">
30
-
<h1 class="text-lg font-bold text-purple-800">Generate secret keys</h1>
31
-
<p class="text-gray-600">Create a new secp256k1/nistp256 keypair</p>
32
-
</div>
33
-
<hr class="mx-4 border-gray-300" />
34
35
<form
36
onSubmit={async (ev) => {
···
6
7
import Button from '~/components/inputs/button';
8
import RadioInput from '~/components/inputs/radio-input';
9
+
import PageHeader from '~/components/page-header';
10
11
type KeyType = 'p256' | 'secp256k1';
12
···
27
28
return (
29
<>
30
+
<PageHeader title="Generate secret keys" subtitle="Create a new secp256k1/nistp256 keypair" />
31
32
<form
33
onSubmit={async (ev) => {
+3
-5
src/views/frontpage.tsx
+3
-5
src/views/frontpage.tsx
···
2
3
import { useTitle } from '~/lib/navigation/router';
4
5
import HistoryIcon from '~/components/ic-icons/baseline-history';
6
import KeyIcon from '~/components/ic-icons/baseline-key';
7
import KeyVisualizerIcon from '~/components/ic-icons/baseline-key-visualizer';
···
170
171
return (
172
<>
173
-
<div class="p-4">
174
-
<h1 class="text-lg font-bold text-purple-800">boat</h1>
175
-
<p class="text-gray-600">handy online tools for AT Protocol</p>
176
-
</div>
177
-
<hr class="mx-4 border-gray-300" />
178
179
<div class="flex grow flex-col pb-2">{nodes}</div>
180
···
2
3
import { useTitle } from '~/lib/navigation/router';
4
5
+
import PageHeader from '~/components/page-header';
6
+
7
import HistoryIcon from '~/components/ic-icons/baseline-history';
8
import KeyIcon from '~/components/ic-icons/baseline-key';
9
import KeyVisualizerIcon from '~/components/ic-icons/baseline-key-visualizer';
···
172
173
return (
174
<>
175
+
<PageHeader title="boat" subtitle="handy online tools for AT Protocol" />
176
177
<div class="flex grow flex-col pb-2">{nodes}</div>
178
+13
-28
src/views/identity/did-lookup.tsx
+13
-28
src/views/identity/did-lookup.tsx
···
15
import ErrorView from '~/components/error-view';
16
import Button from '~/components/inputs/button';
17
import TextInput from '~/components/inputs/text-input';
18
19
const DidLookupPage = () => {
20
const [params, setParams] = useSearchParams({
···
46
47
return (
48
<>
49
-
<div class="p-4">
50
-
<h1 class="text-lg font-bold text-purple-800">View identity info</h1>
51
-
<p class="text-gray-600">Look up an account's DID document</p>
52
-
</div>
53
-
<hr class="mx-4 border-gray-300" />
54
55
<form
56
onSubmit={(ev) => {
···
133
134
<div class="mt-2 flex flex-wrap gap-2 empty:hidden">
135
{isPDS && isServiceUrl && (
136
-
<button
137
-
disabled
138
-
class="flex h-9 select-none items-center rounded border border-gray-300 px-4 text-sm font-semibold text-gray-800 hover:bg-gray-100 active:bg-gray-100 disabled:pointer-events-none disabled:opacity-50"
139
-
>
140
View PDS info
141
-
</button>
142
)}
143
144
{isPDS && isServiceUrl && (
145
-
<button
146
-
disabled
147
-
class="flex h-9 select-none items-center rounded border border-gray-300 px-4 text-sm font-semibold text-gray-800 hover:bg-gray-100 active:bg-gray-100 disabled:pointer-events-none disabled:opacity-50"
148
-
>
149
Explore account repository
150
-
</button>
151
)}
152
153
{isLabeler && isServiceUrl && (
154
-
<button
155
-
disabled
156
-
class="flex h-9 select-none items-center rounded border border-gray-300 px-4 text-sm font-semibold text-gray-800 hover:bg-gray-100 active:bg-gray-100 disabled:pointer-events-none disabled:opacity-50"
157
-
>
158
View emitted labels
159
-
</button>
160
)}
161
</div>
162
</li>
···
185
</div>
186
187
<div class="flex flex-wrap gap-4 p-4 pt-2">
188
-
<button
189
onClick={() => {
190
navigator.clipboard.writeText(JSON.stringify(doc, null, 2));
191
}}
192
-
class="flex h-9 select-none items-center rounded border border-gray-300 px-4 text-sm font-semibold text-gray-800 hover:bg-gray-100 active:bg-gray-100"
193
>
194
Copy DID document
195
-
</button>
196
197
{isDidPlc && (
198
-
<a
199
-
href={`/plc-oplogs?q=${params.q!}`}
200
-
class="flex h-9 select-none items-center rounded border border-gray-300 px-4 text-sm font-semibold text-gray-800 hover:bg-gray-100 active:bg-gray-100"
201
-
>
202
View PLC operation logs
203
-
</a>
204
)}
205
</div>
206
</>
···
15
import ErrorView from '~/components/error-view';
16
import Button from '~/components/inputs/button';
17
import TextInput from '~/components/inputs/text-input';
18
+
import PageHeader from '~/components/page-header';
19
20
const DidLookupPage = () => {
21
const [params, setParams] = useSearchParams({
···
47
48
return (
49
<>
50
+
<PageHeader title="View identity info" subtitle="Look up an account's DID document" />
51
52
<form
53
onSubmit={(ev) => {
···
130
131
<div class="mt-2 flex flex-wrap gap-2 empty:hidden">
132
{isPDS && isServiceUrl && (
133
+
<Button variant="outline" disabled>
134
View PDS info
135
+
</Button>
136
)}
137
138
{isPDS && isServiceUrl && (
139
+
<Button variant="outline" disabled>
140
Explore account repository
141
+
</Button>
142
)}
143
144
{isLabeler && isServiceUrl && (
145
+
<Button variant="outline" disabled>
146
View emitted labels
147
+
</Button>
148
)}
149
</div>
150
</li>
···
173
</div>
174
175
<div class="flex flex-wrap gap-4 p-4 pt-2">
176
+
<Button
177
+
variant="outline"
178
onClick={() => {
179
navigator.clipboard.writeText(JSON.stringify(doc, null, 2));
180
}}
181
>
182
Copy DID document
183
+
</Button>
184
185
{isDidPlc && (
186
+
<Button variant="outline" href={`/plc-oplogs?q=${params.q!}`}>
187
View PLC operation logs
188
+
</Button>
189
)}
190
</div>
191
</>
+2
-5
src/views/identity/plc-applicator/page.tsx
+2
-5
src/views/identity/plc-applicator/page.tsx
···
13
14
import { useTitle } from '~/lib/navigation/router';
15
16
import { Wizard } from '~/components/wizard';
17
18
import Step1_HandleInput from './steps/step1_handle-input';
···
101
102
return (
103
<>
104
-
<div class="p-4">
105
-
<h1 class="text-lg font-bold text-purple-800">Apply PLC operations</h1>
106
-
<p class="text-gray-600">Submit operations to your did:plc identity</p>
107
-
</div>
108
-
<hr class="mx-4 border-gray-300" />
109
110
<Wizard<PlcApplicatorConstraints>
111
initialStep="Step1_HandleInput"
···
13
14
import { useTitle } from '~/lib/navigation/router';
15
16
+
import PageHeader from '~/components/page-header';
17
import { Wizard } from '~/components/wizard';
18
19
import Step1_HandleInput from './steps/step1_handle-input';
···
102
103
return (
104
<>
105
+
<PageHeader title="Apply PLC operations" subtitle="Submit operations to your did:plc identity" />
106
107
<Wizard<PlcApplicatorConstraints>
108
initialStep="Step1_HandleInput"
+2
-5
src/views/identity/plc-oplogs.tsx
+2
-5
src/views/identity/plc-oplogs.tsx
···
20
import ContentCopyIcon from '~/components/ic-icons/baseline-content-copy';
21
import Button from '~/components/inputs/button';
22
import TextInput from '~/components/inputs/text-input';
23
24
const PlcOperationLogPage = () => {
25
const [params, setParams] = useSearchParams({
···
55
56
return (
57
<>
58
-
<div class="p-4">
59
-
<h1 class="text-lg font-bold text-purple-800">View PLC operation logs</h1>
60
-
<p class="text-gray-600">Show history of a did:plc identity</p>
61
-
</div>
62
-
<hr class="mx-4 border-gray-300" />
63
64
<form
65
onSubmit={(ev) => {
···
20
import ContentCopyIcon from '~/components/ic-icons/baseline-content-copy';
21
import Button from '~/components/inputs/button';
22
import TextInput from '~/components/inputs/text-input';
23
+
import PageHeader from '~/components/page-header';
24
25
const PlcOperationLogPage = () => {
26
const [params, setParams] = useSearchParams({
···
56
57
return (
58
<>
59
+
<PageHeader title="View PLC operation logs" subtitle="Show history of a did:plc identity" />
60
61
<form
62
onSubmit={(ev) => {
+8
-41
src/views/repository/repo-archive-explore/views/welcome.tsx
+8
-41
src/views/repository/repo-archive-explore/views/welcome.tsx
···
3
import type { MutationReturn } from '~/lib/utils/mutation';
4
5
import CircularProgress from '~/components/circular-progress';
6
-
import { createDropZone } from '~/lib/hooks/dropzone';
7
8
import type { Archive } from '../types';
9
···
12
}
13
14
const WelcomeView = ({ mutation }: WelcomeViewProps) => {
15
-
const { ref: dropRef, isDropping } = createDropZone({
16
-
// Checked, the mime type for CAR files is blank.
17
-
dataTypes: [''],
18
-
multiple: false,
19
-
onDrop(files) {
20
-
if (files) {
21
-
mutation.mutate({ file: files[0] });
22
-
}
23
-
},
24
-
});
25
-
26
return (
27
<>
28
-
<div class="p-4">
29
-
<h1 class="text-lg font-bold text-purple-800">Explore archive</h1>
30
-
<p class="text-gray-600">Explore a repository archive</p>
31
-
</div>
32
-
<hr class="mx-4 border-gray-300" />
33
34
<div class="flex flex-col gap-4 p-4">
35
-
<fieldset
36
-
ref={dropRef}
37
-
class={
38
-
`grid place-items-center rounded border border-gray-300 px-6 py-12 disabled:opacity-50` +
39
-
(!isDropping() ? ` bg-gray-100` : ` bg-green-100`)
40
-
}
41
>
42
-
<div class="flex flex-col items-center gap-4">
43
-
<button
44
-
onClick={() => {
45
-
const input = document.createElement('input');
46
-
input.type = 'file';
47
-
input.accept = '.car,application/vnd.ipld.car';
48
-
input.oninput = () => mutation.mutate({ file: input.files![0] });
49
-
50
-
input.click();
51
-
}}
52
-
class="flex h-9 select-none items-center rounded border border-gray-400 px-4 text-sm font-semibold text-gray-800 hover:bg-gray-200 active:bg-gray-200 disabled:pointer-events-none"
53
-
>
54
-
Browse files
55
-
</button>
56
-
<p class="select-none font-medium text-gray-600">or drop your file here</p>
57
-
</div>
58
-
59
<div
60
hidden={!mutation.isPending}
61
class="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-gray-50"
···
63
<CircularProgress />
64
<span class="font-medium">Reading CAR file</span>
65
</div>
66
-
</fieldset>
67
68
<Show when={mutation.error}>
69
<p class="whitespace-pre-wrap text-[0.8125rem] font-medium leading-5 text-red-800">
···
3
import type { MutationReturn } from '~/lib/utils/mutation';
4
5
import CircularProgress from '~/components/circular-progress';
6
+
import FileDropZone from '~/components/file-drop-zone';
7
+
import PageHeader from '~/components/page-header';
8
9
import type { Archive } from '../types';
10
···
13
}
14
15
const WelcomeView = ({ mutation }: WelcomeViewProps) => {
16
return (
17
<>
18
+
<PageHeader title="Explore archive" subtitle="Explore a repository archive" />
19
20
<div class="flex flex-col gap-4 p-4">
21
+
<FileDropZone
22
+
accept=".car,application/vnd.ipld.car"
23
+
dataTypes={['']}
24
+
onFiles={(files) => mutation.mutate({ file: files[0] })}
25
>
26
<div
27
hidden={!mutation.isPending}
28
class="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-gray-50"
···
30
<CircularProgress />
31
<span class="font-medium">Reading CAR file</span>
32
</div>
33
+
</FileDropZone>
34
35
<Show when={mutation.error}>
36
<p class="whitespace-pre-wrap text-[0.8125rem] font-medium leading-5 text-red-800">
+8
-41
src/views/repository/repo-archive-unpack.tsx
+8
-41
src/views/repository/repo-archive-unpack.tsx
···
4
import { fromStream } from '@atcute/repo';
5
import { writeTarEntry } from '@mary/tar';
6
7
-
import { createDropZone } from '~/lib/hooks/dropzone';
8
import { useTitle } from '~/lib/navigation/router';
9
import { makeAbortable } from '~/lib/utils/abortable';
10
11
import Logger, { createLogger } from '~/components/logger';
12
13
// @ts-expect-error: new API
14
const yieldToScheduler: () => Promise<void> = window?.scheduler?.yield
···
21
22
const [getSignal, cleanup] = makeAbortable();
23
const [pending, setPending] = createSignal(false);
24
-
25
-
const { ref: dropRef, isDropping } = createDropZone({
26
-
// Checked, the mime type for CAR files is blank.
27
-
dataTypes: [''],
28
-
multiple: false,
29
-
onDrop(files) {
30
-
if (files) {
31
-
onFileDrop(files);
32
-
}
33
-
},
34
-
});
35
36
const mutate = async (file: File, signal: AbortSignal) => {
37
logger.log(`Starting extraction`);
···
155
156
return (
157
<>
158
-
<div class="p-4">
159
-
<h1 class="text-lg font-bold text-purple-800">Unpack archive</h1>
160
-
<p class="text-gray-600">Extract a repository archive into a tarball</p>
161
-
</div>
162
-
<hr class="mx-4 border-gray-300" />
163
164
<div class="p-4">
165
-
<fieldset
166
-
ref={dropRef}
167
disabled={pending()}
168
-
class={
169
-
`grid place-items-center rounded border border-gray-300 px-6 py-12 disabled:opacity-50` +
170
-
(pending() || !isDropping() ? ` bg-gray-100` : ` bg-green-100`)
171
-
}
172
-
>
173
-
<div class="flex flex-col items-center gap-4">
174
-
<button
175
-
onClick={() => {
176
-
const input = document.createElement('input');
177
-
input.type = 'file';
178
-
input.accept = '.car,application/vnd.ipld.car';
179
-
input.oninput = () => onFileDrop(Array.from(input.files!));
180
-
181
-
input.click();
182
-
}}
183
-
class="flex h-9 select-none items-center rounded border border-gray-400 px-4 text-sm font-semibold text-gray-800 hover:bg-gray-200 active:bg-gray-200 disabled:pointer-events-none"
184
-
>
185
-
Browse files
186
-
</button>
187
-
<p class="select-none font-medium text-gray-600">or drop your file here</p>
188
-
</div>
189
-
</fieldset>
190
</div>
191
<hr class="mx-4 border-gray-300" />
192
···
4
import { fromStream } from '@atcute/repo';
5
import { writeTarEntry } from '@mary/tar';
6
7
import { useTitle } from '~/lib/navigation/router';
8
import { makeAbortable } from '~/lib/utils/abortable';
9
10
+
import FileDropZone from '~/components/file-drop-zone';
11
import Logger, { createLogger } from '~/components/logger';
12
+
import PageHeader from '~/components/page-header';
13
14
// @ts-expect-error: new API
15
const yieldToScheduler: () => Promise<void> = window?.scheduler?.yield
···
22
23
const [getSignal, cleanup] = makeAbortable();
24
const [pending, setPending] = createSignal(false);
25
26
const mutate = async (file: File, signal: AbortSignal) => {
27
logger.log(`Starting extraction`);
···
145
146
return (
147
<>
148
+
<PageHeader title="Unpack archive" subtitle="Extract a repository archive into a tarball" />
149
150
<div class="p-4">
151
+
<FileDropZone
152
+
accept=".car,application/vnd.ipld.car"
153
+
dataTypes={['']}
154
disabled={pending()}
155
+
onFiles={onFileDrop}
156
+
/>
157
</div>
158
<hr class="mx-4 border-gray-300" />
159
+2
-5
src/views/repository/repo-export.tsx
+2
-5
src/views/repository/repo-export.tsx
···
15
import Button from '~/components/inputs/button';
16
import TextInput from '~/components/inputs/text-input';
17
import Logger, { createLogger } from '~/components/logger';
18
19
const RepoExportPage = () => {
20
const logger = createLogger();
···
135
136
return (
137
<>
138
-
<div class="p-4">
139
-
<h1 class="text-lg font-bold text-purple-800">Export repository</h1>
140
-
<p class="text-gray-600">Download an archive of an account's repository</p>
141
-
</div>
142
-
<hr class="mx-4 border-gray-300" />
143
144
<form
145
onSubmit={(ev) => {
···
15
import Button from '~/components/inputs/button';
16
import TextInput from '~/components/inputs/text-input';
17
import Logger, { createLogger } from '~/components/logger';
18
+
import PageHeader from '~/components/page-header';
19
20
const RepoExportPage = () => {
21
const logger = createLogger();
···
136
137
return (
138
<>
139
+
<PageHeader title="Export repository" subtitle="Download an archive of an account's repository" />
140
141
<form
142
onSubmit={(ev) => {