atmosphere explorer
at better-random 146 lines 4.8 kB view raw
1import * as CBOR from "@atcute/cbor"; 2import * as CID from "@atcute/cid"; 3import { A } from "@solidjs/router"; 4import { Show } from "solid-js"; 5import { type JSONType } from "../../components/json.jsx"; 6 7export const isIOS = 8 /iPad|iPhone|iPod/.test(navigator.userAgent) || 9 (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1); 10 11// Convert CBOR-decoded objects to JSON-friendly format 12export const toJsonValue = (obj: unknown): JSONType => { 13 if (obj === null || obj === undefined) return null; 14 15 if (CID.isCidLink(obj)) { 16 return { $link: obj.$link }; 17 } 18 19 if ( 20 obj && 21 typeof obj === "object" && 22 "version" in obj && 23 "codec" in obj && 24 "digest" in obj && 25 "bytes" in obj 26 ) { 27 try { 28 return { $link: CID.toString(obj as CID.Cid) }; 29 } catch {} 30 } 31 32 if (CBOR.isBytes(obj)) { 33 return { $bytes: obj.$bytes }; 34 } 35 36 if (Array.isArray(obj)) { 37 return obj.map(toJsonValue); 38 } 39 40 if (typeof obj === "object") { 41 const result: Record<string, JSONType> = {}; 42 for (const [key, value] of Object.entries(obj)) { 43 result[key] = toJsonValue(value); 44 } 45 return result; 46 } 47 48 return obj as JSONType; 49}; 50 51export interface Archive { 52 file: File; 53 did: string; 54 entries: CollectionEntry[]; 55} 56 57export interface CollectionEntry { 58 name: string; 59 entries: RecordEntry[]; 60} 61 62export interface RecordEntry { 63 key: string; 64 cid: string; 65 record: JSONType; 66} 67 68export type View = 69 | { type: "repo" } 70 | { type: "collection"; collection: CollectionEntry } 71 | { type: "record"; collection: CollectionEntry; record: RecordEntry }; 72 73export const WelcomeView = (props: { 74 title: string; 75 subtitle: string; 76 loading: boolean; 77 progress?: number; 78 error?: string; 79 onFileChange: (e: Event) => void; 80 onDrop: (e: DragEvent) => void; 81 onDragOver: (e: DragEvent) => void; 82}) => { 83 return ( 84 <div class="flex w-full max-w-3xl flex-col gap-y-4 px-2"> 85 <div class="flex flex-col gap-y-1"> 86 <div class="flex items-center gap-2 text-lg"> 87 <A 88 href="/car" 89 class="flex size-7 items-center justify-center rounded text-neutral-500 transition-colors hover:bg-neutral-200 hover:text-neutral-700 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-neutral-200" 90 > 91 <span class="iconify lucide--arrow-left" /> 92 </A> 93 <h1 class="font-semibold">{props.title}</h1> 94 </div> 95 <p class="text-sm text-neutral-600 dark:text-neutral-400">{props.subtitle}</p> 96 </div> 97 98 <div 99 class="dark:bg-dark-300 flex flex-col items-center justify-center gap-3 rounded-lg border-2 border-dashed border-neutral-300 bg-neutral-50 p-8 transition-colors hover:border-neutral-400 dark:border-neutral-600 dark:hover:border-neutral-500" 100 onDrop={props.onDrop} 101 onDragOver={props.onDragOver} 102 > 103 <Show 104 when={!props.loading} 105 fallback={ 106 <div class="flex flex-col items-center gap-2"> 107 <span class="iconify lucide--loader-circle animate-spin text-3xl text-neutral-400" /> 108 <span class="text-sm font-medium text-neutral-600 dark:text-neutral-400"> 109 Reading CAR file... 110 </span> 111 <Show when={props.progress && props.progress > 0}> 112 <span class="text-xs text-neutral-500 dark:text-neutral-400"> 113 {props.progress?.toLocaleString()} records processed 114 </span> 115 </Show> 116 </div> 117 } 118 > 119 <span class="iconify lucide--folder-archive text-3xl text-neutral-400" /> 120 <div class="text-center"> 121 <p class="text-sm font-medium text-neutral-700 dark:text-neutral-300"> 122 Drag and drop a CAR file here 123 </p> 124 <p class="text-xs text-neutral-500 dark:text-neutral-400">or</p> 125 </div> 126 <label class="dark:bg-dark-300 dark:hover:bg-dark-200 dark:active:bg-dark-100 flex items-center gap-1 rounded-md border border-neutral-300 bg-neutral-50 px-2.5 py-1.5 text-sm text-neutral-700 transition-colors select-none hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:text-neutral-300"> 127 <input 128 type="file" 129 accept={isIOS ? undefined : ".car,application/vnd.ipld.car"} 130 onChange={props.onFileChange} 131 class="hidden" 132 /> 133 <span class="iconify lucide--upload text-sm" /> 134 Choose file 135 </label> 136 </Show> 137 </div> 138 139 <Show when={props.error}> 140 <div class="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800 dark:border-red-800 dark:bg-red-900/20 dark:text-red-300"> 141 {props.error} 142 </div> 143 </Show> 144 </div> 145 ); 146};