learn and share notes on atproto (wip) 馃
malfestio.stormlightlabs.org/
readability
solid
axum
atproto
srs
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};