import { type Component, createSignal, createMemo, For, Show } from "solid-js"; import { store, renameTagDefinition } from "@/store"; import { pb } from "@/lib/pocketbase"; import { type Note } from "@/store"; import { Trash2, Lock, Unlock, Search, Link, X, ChevronRight } from "lucide-solid"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { TaskEditor } from "@/components/TaskEditor"; import { NOTES_COLLECTION } from "@/lib/constants"; import { TaskCard } from "@/components/TaskCard"; import { NotesSidebar } from "@/components/NotesSidebar"; export const NotepadView: Component<{ selectedNoteId: string | null; setSelectedNoteId: (id: string | null) => void; hideNavigation?: boolean; }> = (props) => { const [isLinking, setIsLinking] = createSignal(false); const [linkSearchQuery, setLinkSearchQuery] = createSignal(""); const [isLinkedTasksOpen, setIsLinkedTasksOpen] = createSignal(true); const currentUserId = pb.authStore.model?.id; // Derived states const activeNote = createMemo(() => { const id = props.selectedNoteId?.trim(); return store.notes.find(n => n.id === id) || null; }); const handleUpdateNote = async (id: string, data: Partial) => { try { await pb.collection(NOTES_COLLECTION).update(id, data); } catch (e: any) { if (!e.isAbort) { console.error("Failed to update note", e); } } }; let contentUpdateTimeout: number | undefined; const handleUpdateContent = (id: string, html: string) => { clearTimeout(contentUpdateTimeout); contentUpdateTimeout = window.setTimeout(async () => { try { await pb.collection(NOTES_COLLECTION).update(id, { content: html }); } catch (e: any) { if (!e.isAbort) { console.error("Failed to update note content", e); } } }, 500); }; const handleRenameNote = async (id: string, oldTitle: string, newTitle: string) => { // 1. Update the note title await handleUpdateNote(id, { title: newTitle }); // 2. Rename the TagDefinition (preserves color) and propagate to all tagged tasks // Use '#' prefix for note tags await renameTagDefinition(`#${oldTitle}`, `#${newTitle}`); }; const handleDeleteNote = async (id: string) => { try { await pb.collection(NOTES_COLLECTION).delete(id); if (props.selectedNoteId === id) { props.setSelectedNoteId(null); } } catch (e) { console.error("Failed to delete note", e); } }; const handleLinkTask = async (taskId: string) => { const note = activeNote(); if (!note) return; const newTasks = [...(note.tasks || []), taskId]; await handleUpdateNote(note.id, { tasks: newTasks }); setIsLinking(false); setLinkSearchQuery(""); }; const handleUnlinkTask = async (taskId: string) => { const note = activeNote(); if (!note) return; const newTasks = (note.tasks || []).filter(id => id !== taskId); await handleUpdateNote(note.id, { tasks: newTasks }); }; // Linked tasks calculation const linkedTasks = createMemo(() => { const note = activeNote(); if (!note) return []; const noteTagName = `#${note.title}`; return store.tasks.filter(t => { // Explicit relation if (note.tasks && note.tasks.includes(t.id)) return true; // Tag relation (case-insensitive title match with # prefix) if (t.tags && t.tags.some(tag => tag.toLowerCase() === noteTagName.toLowerCase())) return true; return false; }); }); const unlinkedTasks = createMemo(() => { const note = activeNote(); if (!note) return []; const q = linkSearchQuery().toLowerCase(); // Get names of pinned/subscribed buckets const pinnedBucketNames = store.buckets .filter(b => store.subscribedBuckets.includes(b.id)) .map(b => b.name.toLowerCase()); const allBucketNames = store.buckets.map(b => b.name.toLowerCase()); return store.tasks.filter(t => { if (t.deletedAt) return false; // Don't show trashed tasks // Filter out tasks from unpinned buckets const taskBucketTags = (t.tags || []).filter(tag => allBucketNames.includes(tag.toLowerCase())); if (taskBucketTags.length > 0) { // If it belongs to a bucket, at least one must be pinned const hasPinnedBucket = taskBucketTags.some(tag => pinnedBucketNames.includes(tag.toLowerCase())); if (!hasPinnedBucket) return false; } if (note.tasks && note.tasks.includes(t.id)) return false; // Already linked if (!q) return false; // Don't show all by default return t.title.toLowerCase().includes(q); }).slice(0, 10); }); const openQuickEntry = () => { window.dispatchEvent(new KeyboardEvent('keydown', { metaKey: true, key: 'k' })); }; return (
{/* Mobile Notes List */}
{/* Note Editor (Main Panel) */}

No Note Selected

Select an existing note from the sidebar or create a new one to get started.

}>

Loading note...

}> {(note) => { const isOwner = note().user === currentUserId; const canEdit = isOwner || !note().isPrivate; return ( <> {/* Editor Area */}
{ const val = e.currentTarget.value.trim() || 'Untitled'; if (val !== note().title) handleRenameNote(note().id, note().title, val); }} />
{/* Editor Instance */}
handleUpdateContent(note().id, html)} editable={canEdit} />
{/* Close button (Mobile only) */}
{/* Linked Tasks Sidebar */}
{ if (!isLinkedTasksOpen()) { setIsLinkedTasksOpen(true); } }}>
{ e.stopPropagation(); setIsLinkedTasksOpen(!isLinkedTasksOpen()); }} >
}>

Linked Tasks

Linked Tasks
setLinkSearchQuery(e.currentTarget.value)} class="w-full bg-background border border-border/50 rounded-lg pl-8 pr-3 py-1.5 text-xs outline-none focus:ring-1 focus:ring-primary/30 shadow-sm" />
0}>
{(t) => ( )}

No tasks linked yet.

Link an existing task, create a new one, or add the tag #{note().title} to a task.

}> {(task) => (
{/* If explicitly linked, allow unlinking. If linked by tag, they should remove the tag from the task, which we can't easily do here unless we implement it. */}
)}
); }} ); };