learn and share notes on atproto (wip) 馃 malfestio.stormlightlabs.org/
readability solid axum atproto srs
at main 213 lines 7.9 kB view raw
1import { NoteCard } from "$components/NoteCard"; 2import { NotesGraph } from "$components/notes/NotesGraph"; 3import { Button } from "$components/ui/Button"; 4import { EmptyState } from "$components/ui/EmptyState"; 5import { api } from "$lib/api"; 6import type { Note } from "$lib/model"; 7import { authStore } from "$lib/store"; 8import { syncStore } from "$lib/sync-store"; 9import { A, useLocation } from "@solidjs/router"; 10import type { Component } from "solid-js"; 11import { createEffect, createMemo, createResource, createSignal, For, Show } from "solid-js"; 12 13const fetchNotes = async (): Promise<Note[]> => { 14 const user = authStore.user(); 15 const remoteNotes: Note[] = []; 16 const localNotes: Note[] = []; 17 18 try { 19 const res = await api.getNotes(); 20 if (res.ok) { 21 remoteNotes.push(...(await res.json())); 22 } 23 } catch { 24 console.log("Offline - continuing with local only"); 25 } 26 27 if (user) { 28 const locals = await syncStore.getLocalNotes(user.did); 29 for (const local of locals) { 30 if (local.syncStatus === "local_only" || local.syncStatus === "pending_push" || local.syncStatus === "conflict") { 31 localNotes.push( 32 { 33 id: local.id, 34 owner_did: local.ownerDid, 35 title: local.title, 36 body: local.body, 37 tags: local.tags, 38 visibility: local.visibility, 39 updated_at: local.updatedAt, 40 links: local.links ?? [], 41 } as Note, 42 ); 43 } 44 } 45 } 46 47 return [...localNotes, ...remoteNotes]; 48}; 49 50type ViewMode = "grid" | "list" | "graph"; 51 52const Notes: Component = () => { 53 const location = useLocation(); 54 const [notes, { refetch }] = createResource(fetchNotes); 55 const [viewMode, setViewMode] = createSignal<ViewMode>("grid"); 56 const [searchQuery, setSearchQuery] = createSignal(""); 57 58 createEffect(() => { 59 if (location.pathname === "/notes") { 60 refetch(); 61 } 62 }); 63 64 const filteredNotes = createMemo(() => { 65 const allNotes = notes() || []; 66 const query = searchQuery().toLowerCase().trim(); 67 if (!query) return allNotes; 68 return allNotes.filter((note) => 69 note.title.toLowerCase().includes(query) 70 || note.body.toLowerCase().includes(query) 71 || note.tags.some((tag) => tag.toLowerCase().includes(query)) 72 ); 73 }); 74 75 return ( 76 <div class="max-w-7xl mx-auto p-6 space-y-6"> 77 <header class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> 78 <div> 79 <h1 class="text-3xl font-bold tracking-tight text-slate-900 dark:text-white">Notes</h1> 80 <p class="text-slate-600 dark:text-slate-400 mt-1">Your personal knowledge base</p> 81 </div> 82 <A href="/notes/new"> 83 <Button variant="primary">New Note</Button> 84 </A> 85 </header> 86 87 <div class="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between"> 88 <div class="relative flex-1 max-w-md"> 89 <input 90 type="text" 91 placeholder="Search notes..." 92 value={searchQuery()} 93 onInput={(e) => setSearchQuery(e.currentTarget.value)} 94 class="w-full bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg px-4 py-2 pl-10 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" /> 95 <svg 96 class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" 97 fill="none" 98 stroke="currentColor" 99 viewBox="0 0 24 24"> 100 <path 101 stroke-linecap="round" 102 stroke-linejoin="round" 103 stroke-width="2" 104 d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> 105 </svg> 106 </div> 107 108 <div class="flex items-center gap-2"> 109 <button 110 onClick={() => setViewMode("grid")} 111 class={`p-2 rounded ${ 112 viewMode() === "grid" ? "bg-slate-200 dark:bg-slate-700" : "hover:bg-slate-100 dark:hover:bg-slate-800" 113 }`} 114 aria-label="Grid view"> 115 <svg 116 class="w-5 h-5 text-slate-600 dark:text-slate-300" 117 fill="none" 118 stroke="currentColor" 119 viewBox="0 0 24 24"> 120 <path 121 stroke-linecap="round" 122 stroke-linejoin="round" 123 stroke-width="2" 124 d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" /> 125 </svg> 126 </button> 127 <button 128 onClick={() => setViewMode("list")} 129 class={`p-2 rounded ${ 130 viewMode() === "list" ? "bg-slate-200 dark:bg-slate-700" : "hover:bg-slate-100 dark:hover:bg-slate-800" 131 }`} 132 aria-label="List view"> 133 <svg 134 class="w-5 h-5 text-slate-600 dark:text-slate-300" 135 fill="none" 136 stroke="currentColor" 137 viewBox="0 0 24 24"> 138 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /> 139 </svg> 140 </button> 141 <button 142 onClick={() => setViewMode("graph")} 143 class={`p-2 rounded ${ 144 viewMode() === "graph" ? "bg-slate-200 dark:bg-slate-700" : "hover:bg-slate-100 dark:hover:bg-slate-800" 145 }`} 146 aria-label="Graph view"> 147 <svg 148 class="w-5 h-5 text-slate-600 dark:text-slate-300" 149 fill="none" 150 stroke="currentColor" 151 viewBox="0 0 24 24"> 152 <circle cx="5" cy="6" r="2" stroke-width="2" /> 153 <circle cx="12" cy="18" r="2" stroke-width="2" /> 154 <circle cx="19" cy="10" r="2" stroke-width="2" /> 155 <path stroke-linecap="round" stroke-width="2" d="M6.5 7.5L10.5 16M13.5 16.5L17 11.5M7 6h10" /> 156 </svg> 157 </button> 158 </div> 159 </div> 160 161 <Show 162 when={!notes.loading} 163 fallback={ 164 <div class="flex justify-center p-12"> 165 <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" /> 166 </div> 167 }> 168 <Show 169 when={filteredNotes().length > 0} 170 fallback={ 171 <Show 172 when={searchQuery()} 173 fallback={ 174 <EmptyState 175 title="No notes yet" 176 description="Start capturing your thoughts and ideas" 177 action={ 178 <A href="/notes/new"> 179 <Button variant="primary">Create your first note</Button> 180 </A> 181 } /> 182 }> 183 <EmptyState 184 title="No matching notes" 185 description={`No notes found for "${searchQuery()}"`} 186 action={ 187 <button 188 onClick={() => setSearchQuery("")} 189 class="text-sm font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400"> 190 Clear search 191 </button> 192 } /> 193 </Show> 194 }> 195 <Show 196 when={viewMode() === "graph"} 197 fallback={ 198 <div 199 class={viewMode() === "grid" 200 ? "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" 201 : "flex flex-col gap-4"}> 202 <For each={filteredNotes()}>{(note) => <NoteCard note={note} />}</For> 203 </div> 204 }> 205 <NotesGraph notes={filteredNotes()} /> 206 </Show> 207 </Show> 208 </Show> 209 </div> 210 ); 211}; 212 213export default Notes;