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