diff --git a/src/components/NotesSidebar.tsx b/src/components/NotesSidebar.tsx index 72c3036..39ac19f 100644 --- a/src/components/NotesSidebar.tsx +++ b/src/components/NotesSidebar.tsx @@ -1,7 +1,7 @@ -import { type Component, createSignal, createMemo, For, Show } from "solid-js"; -import { store } from "@/store"; +import { type Component, createSignal, createMemo, For, Show, onCleanup, createEffect } from "solid-js"; +import { store, setStore, syncPreferences } from "@/store"; import { pb } from "@/lib/pocketbase"; -import { Plus, Lock, Search } from "lucide-solid"; +import { Plus, Lock, Search, Filter } from "lucide-solid"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { NOTES_COLLECTION } from "@/lib/constants"; @@ -10,15 +10,66 @@ export const NotesSidebar: Component<{ selectedNoteId: string | null; setSelectedNoteId: (id: string | null) => void; }> = (props) => { + let containerRef: HTMLDivElement | undefined; const [searchQuery, setSearchQuery] = createSignal(""); + const [showFilters, setShowFilters] = createSignal(false); const currentUserId = pb.authStore.model?.id; + createEffect(() => { + if (!showFilters()) return; + + const handleClickOutside = (e: MouseEvent) => { + if (containerRef && !containerRef.contains(e.target as Node)) { + setShowFilters(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + onCleanup(() => document.removeEventListener("mousedown", handleClickOutside)); + }); + + const availableTags = createMemo(() => { + const tags = new Set(); + store.notes.forEach(n => { + if (!n.deletedAt) n.tags.forEach(t => tags.add(t)); + }); + return Array.from(tags).sort(); + }); + + const toggleTagFilter = (tag: string) => { + const current = store.noteFilter.tags; + const next = current.includes(tag) ? current.filter(t => t !== tag) : [...current, tag]; + setStore("noteFilter", "tags", next); + syncPreferences(); + }; + + const toggleOwnedByMe = () => { + setStore("noteFilter", "ownedByMe", !store.noteFilter.ownedByMe); + syncPreferences(); + }; + + const isFilterActive = createMemo(() => { + return store.noteFilter.ownedByMe || store.noteFilter.tags.length > 0; + }); + const filteredNotes = createMemo(() => { - let n = store.notes.filter(note => !note.deletedAt); + let n = store.notes.filter(note => { + if (note.deletedAt) return false; + // Hide child notes from the main list + if (note.tags && note.tags.some(t => t.startsWith("child-of-"))) return false; + return true; + }); + if (searchQuery()) { const q = searchQuery().toLowerCase(); n = n.filter(note => note.title.toLowerCase().includes(q) || note.tags.some(t => t.toLowerCase().includes(q))); } + if (store.noteFilter.ownedByMe) { + n = n.filter(note => note.user === currentUserId); + } + if (store.noteFilter.tags.length > 0) { + n = n.filter(note => store.noteFilter.tags.some(t => note.tags.includes(t))); + } return n; }); @@ -40,21 +91,72 @@ export const NotesSidebar: Component<{ return (
- {/* Header */} -
-
- - setSearchQuery(e.currentTarget.value)} - class="w-full bg-muted/50 border border-border/50 rounded-lg pl-8 pr-3 py-1.5 text-xs outline-none focus:ring-1 focus:ring-primary/30" - /> +
+ {/* Header */} +
+
+ + setSearchQuery(e.currentTarget.value)} + class="w-full bg-muted/50 border border-border/50 rounded-lg pl-8 pr-3 py-1.5 text-xs outline-none focus:ring-1 focus:ring-primary/30" + /> +
+ +
- + + +
+
+ + +
+
+ +
+ No tags found}> + {(tag) => { + const isSelected = store.noteFilter.tags.includes(tag); + return ( + + ); + }} + +
+
+
+
{/* List */} diff --git a/src/store/index.ts b/src/store/index.ts index c1d597d..6d25537 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -269,6 +269,7 @@ interface TaskStore { notes: Note[]; isNotepadMode: boolean; quickloadTasks: string[]; + noteFilter: { tags: string[]; ownedByMe: boolean }; } // Initial empty state @@ -295,7 +296,8 @@ export const [store, setStore] = createStore({ subscribedBuckets: [], notes: [], isNotepadMode: false, - quickloadTasks: [] + quickloadTasks: [], + noteFilter: { tags: [], ownedByMe: false } }); export const matchesFilter = (task: Task) => { @@ -1135,7 +1137,8 @@ export const initStore = async () => { matrixScaleDays: prefs.matrixScaleDays || 30, prefId: userId, subscribedBuckets: user.subscribedBuckets || [], - quickloadTasks: prefs.quickloadTasks || [] + quickloadTasks: prefs.quickloadTasks || [], + noteFilter: prefs.noteFilter || { tags: [], ownedByMe: false } }); } } catch (prefErr) { @@ -2337,7 +2340,8 @@ export const syncPreferences = async () => { uWeight: store.uWeight, matrixScaleDays: store.matrixScaleDays, tagDefinitions: store.tagDefinitions, - quickloadTasks: store.quickloadTasks + quickloadTasks: store.quickloadTasks, + noteFilter: store.noteFilter }; await pb.collection('users').update(userId, { diff --git a/src/views/NotepadView.tsx b/src/views/NotepadView.tsx index 15eff1b..a810bb3 100644 --- a/src/views/NotepadView.tsx +++ b/src/views/NotepadView.tsx @@ -2,9 +2,10 @@ import { type Component, createSignal, createMemo, createEffect, onCleanup, For, import { store, renameTagDefinition, updateNote, removeNote, restoreNote, setActiveNoteId, cleanupNoteAttachments } from "@/store"; import { pb } from "@/lib/pocketbase"; import { type Note } from "@/store"; -import { Trash2, Lock, Unlock, Search, Link, X, ChevronRight } from "lucide-solid"; +import { Trash2, Lock, Unlock, Search, Link, X, ChevronRight, ChevronDown, FileText, ArrowLeft, Plus } from "lucide-solid"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; +import { NOTES_COLLECTION } from "@/lib/constants"; import { TaskEditor } from "@/components/TaskEditor"; import { TaskCard } from "@/components/TaskCard"; import { NotesSidebar } from "@/components/NotesSidebar"; @@ -161,12 +162,75 @@ export const NotepadView: Component<{ }).slice(0, 10); }); + // --- Tabs Logic --- + const childTabs = createMemo(() => { + const note = activeNote(); + if (!note) return []; + const childTag = `child-of-${note.id}`; + return store.notes.filter(n => !n.deletedAt && n.tags?.includes(childTag)); + }); + + const parentNote = createMemo(() => { + const note = activeNote(); + if (!note) return null; + const parentTag = note.tags?.find(t => t.startsWith("child-of-")); + if (!parentTag) return null; + const parentId = parentTag.replace("child-of-", ""); + return store.notes.find(n => n.id === parentId) || null; + }); + + const siblingTabs = createMemo(() => { + const parent = parentNote(); + if (!parent) return []; + const childTag = `child-of-${parent.id}`; + return store.notes.filter(n => !n.deletedAt && n.tags?.includes(childTag)); + }); + + const outsideTabs = createMemo(() => parentNote() ? siblingTabs() : childTabs()); + const insideTabs = createMemo(() => parentNote() ? childTabs() : []); + + const rootParentNote = createMemo(() => { + let current = activeNote(); + let count = 0; + while (current && count < 10) { + const parentTag = current.tags?.find(t => t.startsWith("child-of-")); + if (!parentTag) break; + const parentId = parentTag.replace("child-of-", ""); + const parent = store.notes.find(n => n.id === parentId); + if (!parent) break; + current = parent; + count++; + } + return current; + }); + + const handleCreateTab = async (parentIdOverride?: string) => { + const note = activeNote(); + if (!note || !currentUserId) return; + + const parentId = parentIdOverride || note.id; + + try { + const result = await pb.collection(NOTES_COLLECTION).create({ + title: "New Tab", + content: "", + tags: [`child-of-${parentId}`], + isPrivate: note.isPrivate, // inherit privacy + user: currentUserId, + }); + props.setSelectedNoteId(result.id); + } catch (e) { + console.error("Failed to create tab", e); + toast.error("Failed to create tab"); + } + }; + const openQuickEntry = () => { window.dispatchEvent(new KeyboardEvent('keydown', { metaKey: true, key: 'k' })); }; return ( -
+
{/* Mobile Notes List */}
@@ -205,197 +269,408 @@ export const NotepadView: Component<{ return ( <> - {/* Editor Area */} -
-
-
-