forked from
pds.ls/pdsls
atmosphere explorer
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};