learn and share notes on atproto (wip) 馃 malfestio.stormlightlabs.org/
readability solid axum atproto srs
at main 127 lines 4.7 kB view raw
1import type { Note } from "$lib/model"; 2import { extractWikilinkTitles } from "$lib/wikilink"; 3import { A } from "@solidjs/router"; 4import type { Component } from "solid-js"; 5import { createMemo, For, Show } from "solid-js"; 6 7type FilterType = "all" | "linked" | "orphaned"; 8 9type NotesSidebarProps = { 10 notes: Note[]; 11 selectedTag?: string; 12 selectedFilter?: FilterType; 13 onTagSelect?: (tag: string | undefined) => void; 14 onFilterSelect?: (filter: FilterType) => void; 15}; 16 17type TagCount = { tag: string; count: number }; 18 19export const NotesSidebar: Component<NotesSidebarProps> = (props) => { 20 const tagCounts = createMemo<TagCount[]>(() => { 21 const counts = new Map<string, number>(); 22 props.notes.forEach((note) => { 23 note.tags.forEach((tag) => { 24 counts.set(tag, (counts.get(tag) ?? 0) + 1); 25 }); 26 }); 27 return Array.from(counts.entries()).map(([tag, count]) => ({ tag, count })).sort((a, b) => b.count - a.count); 28 }); 29 30 const recentNotes = createMemo(() => { 31 return [...props.notes].sort((a, b) => { 32 const dateA = a.updated_at ?? a.created_at ?? ""; 33 const dateB = b.updated_at ?? b.created_at ?? ""; 34 return dateB.localeCompare(dateA); 35 }).slice(0, 5); 36 }); 37 38 const linkedCount = createMemo(() => { 39 return props.notes.filter((note) => extractWikilinkTitles(note.body).length > 0).length; 40 }); 41 42 const orphanedCount = createMemo(() => { 43 return props.notes.filter((note) => extractWikilinkTitles(note.body).length === 0).length; 44 }); 45 46 const filterButtonClass = (filter: FilterType) => { 47 const isActive = props.selectedFilter === filter; 48 return `px-3 py-1.5 text-sm rounded-md transition-colors ${ 49 isActive ? "bg-blue-500/20 text-blue-400 font-medium" : "text-slate-400 hover:text-white hover:bg-slate-800" 50 }`; 51 }; 52 53 return ( 54 <aside class="w-64 shrink-0 space-y-6" data-testid="notes-sidebar"> 55 {/* Quick Filters */} 56 <section> 57 <h3 class="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2">Filters</h3> 58 <div class="flex flex-col gap-1"> 59 <button 60 class={filterButtonClass("all")} 61 onClick={() => props.onFilterSelect?.("all")} 62 data-testid="filter-all"> 63 All ({props.notes.length}) 64 </button> 65 <button 66 class={filterButtonClass("linked")} 67 onClick={() => props.onFilterSelect?.("linked")} 68 data-testid="filter-linked"> 69 Linked ({linkedCount()}) 70 </button> 71 <button 72 class={filterButtonClass("orphaned")} 73 onClick={() => props.onFilterSelect?.("orphaned")} 74 data-testid="filter-orphaned"> 75 Orphaned ({orphanedCount()}) 76 </button> 77 </div> 78 </section> 79 80 {/* Tags */} 81 <Show when={tagCounts().length > 0}> 82 <section> 83 <h3 class="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2">Tags</h3> 84 <div class="space-y-1"> 85 <For each={tagCounts().slice(0, 10)}> 86 {(item) => ( 87 <button 88 class={`flex items-center justify-between w-full px-2 py-1 text-sm rounded transition-colors ${ 89 props.selectedTag === item.tag 90 ? "bg-blue-500/20 text-blue-400" 91 : "text-slate-400 hover:text-white hover:bg-slate-800" 92 }`} 93 onClick={() => props.onTagSelect?.(props.selectedTag === item.tag ? undefined : item.tag)} 94 data-testid={`tag-${item.tag}`}> 95 <span class="truncate">#{item.tag}</span> 96 <span class="text-xs text-slate-500">{item.count}</span> 97 </button> 98 )} 99 </For> 100 <Show when={tagCounts().length > 10}> 101 <p class="text-xs text-slate-500 px-2">+{tagCounts().length - 10} more tags</p> 102 </Show> 103 </div> 104 </section> 105 </Show> 106 107 {/* Recent Notes */} 108 <Show when={recentNotes().length > 0}> 109 <section> 110 <h3 class="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2">Recent</h3> 111 <div class="space-y-1"> 112 <For each={recentNotes()}> 113 {(note) => ( 114 <A 115 href={`/notes/${note.id}`} 116 class="block px-2 py-1 text-sm text-slate-400 hover:text-white hover:bg-slate-800 rounded truncate transition-colors" 117 data-testid={`recent-${note.id}`}> 118 {note.title || "Untitled"} 119 </A> 120 )} 121 </For> 122 </div> 123 </section> 124 </Show> 125 </aside> 126 ); 127};