handy online tools for AT Protocol boat.kelinci.net
atproto bluesky atcute typescript solidjs

refactor: clean up ui components

mary.my.id d444f7eb b92f92d2

verified
Changed files
+158 -157
src
components
views
blob
bluesky
threadgate-applicator
crypto
identity
repository
+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;
+27 -12
src/components/inputs/button.tsx
··· 1 - import type { JSX } from 'solid-js'; 1 + import { createMemo, type JSX } from 'solid-js'; 2 2 3 3 interface ButtonProps { 4 4 children?: JSX.Element; 5 5 disabled?: boolean; 6 - variant?: 'primary' | 'secondary'; 6 + variant?: 'primary' | 'secondary' | 'outline'; 7 7 type?: 'button' | 'submit'; 8 + href?: string; 8 9 onClick?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>; 9 10 } 10 11 ··· 15 16 cn += ` bg-purple-800 text-white hover:bg-purple-700 active:bg-purple-700`; 16 17 } else if (variant === 'secondary') { 17 18 cn += ` bg-gray-200 text-black hover:bg-gray-300 active:bg-gray-300`; 19 + } else if (variant === 'outline') { 20 + cn += ` border border-gray-300 text-gray-800 hover:bg-gray-100 active:bg-gray-100`; 18 21 } 19 22 20 23 if (disabled) { ··· 25 28 }; 26 29 27 30 const Button = (props: ButtonProps) => { 28 - return ( 29 - <button 30 - type={props.type ?? 'button'} 31 - disabled={props.disabled} 32 - class={buttonStyles(props)} 33 - onClick={props.onClick} 34 - > 35 - {props.children} 36 - </button> 37 - ); 31 + const hasLink = createMemo(() => props.href !== undefined); 32 + 33 + return (() => { 34 + if (hasLink()) { 35 + return ( 36 + <a href={!props.disabled ? props.href : undefined} class={buttonStyles(props)}> 37 + {props.children} 38 + </a> 39 + ); 40 + } 41 + 42 + return ( 43 + <button 44 + type={props.type ?? 'button'} 45 + disabled={props.disabled} 46 + class={buttonStyles(props)} 47 + onClick={props.onClick} 48 + > 49 + {props.children} 50 + </button> 51 + ); 52 + }) as unknown as JSX.Element; 38 53 }; 39 54 40 55 export default Button;
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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) => {