feat: add notes UI components and routes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 16:13:22 -05:00
parent da73b25b39
commit edbfe02ee0
11 changed files with 1068 additions and 0 deletions
+157
View File
@@ -0,0 +1,157 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { getNoteService } from '$lib/context/notes.svelte';
import { Button } from '$lib/components/ui/button/index.js';
import * as Tabs from '$lib/components/ui/tabs/index.js';
import { Separator } from '$lib/components/ui/separator/index.js';
import { Plus, LayoutGrid, List } from '@lucide/svelte';
import NotesGrid from './NotesGrid.svelte';
import NoteSearchBar from './NoteSearchBar.svelte';
// Get note service from context
const noteService = getNoteService();
let searchQuery = $state('');
let currentFilter = $state<'all' | 'pinned' | 'archived' | 'trash'>('all');
let view = $state<'grid' | 'list'>('grid');
// Get filtered notes based on current tab
const filteredNotes = $derived(() => {
let notes = noteService.activeNotes;
switch (currentFilter) {
case 'pinned':
notes = noteService.pinnedNotes;
break;
case 'archived':
notes = noteService.archivedNotes;
break;
case 'trash':
notes = noteService.trashedNotes;
break;
}
// Apply search filter
if (searchQuery.trim()) {
return noteService.searchNotes(searchQuery, notes);
}
return notes;
});
async function createNewNote() {
const note = await noteService.createNote({
title: '',
content: '',
tags: new Set()
});
// Wait a bit for the subscription to update
await new Promise((resolve) => setTimeout(resolve, 100));
goto(`/notes/${note.id}`);
}
function openNote(noteId: string) {
goto(`/notes/${noteId}`);
}
</script>
<div class="w-full container mx-auto p-6 space-y-6">
<!-- Page Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold tracking-tight text-foreground">Notes</h1>
<p class="text-muted-foreground">Capture your thoughts with markdown</p>
</div>
<div class="flex items-center gap-3">
<!-- View Toggle -->
<div class="flex items-center border rounded-md">
<Button
variant={view === 'grid' ? 'secondary' : 'ghost'}
size="icon-sm"
onclick={() => (view = 'grid')}
aria-label="Grid view"
>
<LayoutGrid class="h-4 w-4" />
</Button>
<Button
variant={view === 'list' ? 'secondary' : 'ghost'}
size="icon-sm"
onclick={() => (view = 'list')}
aria-label="List view"
>
<List class="h-4 w-4" />
</Button>
</div>
<!-- New Note Button -->
<Button onclick={createNewNote} class="gap-2">
<Plus class="h-4 w-4" />
New Note
</Button>
</div>
</div>
<Separator />
<!-- Search and Filters -->
<div class="space-y-4">
<NoteSearchBar bind:value={searchQuery} />
<Tabs.Root value={currentFilter} onValueChange={(v) => (currentFilter = v as any)}>
<Tabs.List class="grid w-full grid-cols-4">
<Tabs.Trigger value="all">
All
{#if !searchQuery}
<span class="ml-1 text-xs text-muted-foreground">
({noteService.activeNotes.length})
</span>
{/if}
</Tabs.Trigger>
<Tabs.Trigger value="pinned">
Pinned
{#if !searchQuery}
<span class="ml-1 text-xs text-muted-foreground">
({noteService.pinnedNotes.length})
</span>
{/if}
</Tabs.Trigger>
<Tabs.Trigger value="archived">
Archived
{#if !searchQuery}
<span class="ml-1 text-xs text-muted-foreground">
({noteService.archivedNotes.length})
</span>
{/if}
</Tabs.Trigger>
<Tabs.Trigger value="trash">
Trash
{#if !searchQuery}
<span class="ml-1 text-xs text-muted-foreground">
({noteService.trashedNotes.length})
</span>
{/if}
</Tabs.Trigger>
</Tabs.List>
</Tabs.Root>
</div>
<!-- Notes Grid -->
<NotesGrid
notes={filteredNotes()}
{view}
loading={noteService.loading}
emptyMessage={searchQuery
? 'No notes match your search'
: currentFilter === 'pinned'
? 'No pinned notes'
: currentFilter === 'archived'
? 'No archived notes'
: currentFilter === 'trash'
? 'Trash is empty'
: 'No notes yet'}
onNoteClick={(note) => openNote(note.id)}
/>
</div>