56b8e9956a
- Remove prop drilling, components get notes from NoteService context
- NoteCard gets note by ID from service, uses {#key} for Markdown reactivity
- NotesGrid handles filtering internally via service methods
- NoteEditor gets service from context instead of prop
- Add PageState type for shallow routing selectedNoteId
- Add +layout.server.ts to pass user data to client
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
147 lines
4.0 KiB
Svelte
147 lines
4.0 KiB
Svelte
<script lang="ts">
|
|
import type { User } from 'better-auth';
|
|
import { pushState } from '$app/navigation';
|
|
import { page } from '$app/state';
|
|
import { setNoteService } 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 * as Dialog from '$lib/components/ui/dialog/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';
|
|
import NoteEditor from './NoteEditor.svelte';
|
|
|
|
let { data }: { data: { user: User } } = $props();
|
|
|
|
// Create note service with authenticated user
|
|
// svelte-ignore state_referenced_locally
|
|
const noteService = setNoteService(data.user.id);
|
|
|
|
let searchQuery = $state('');
|
|
let currentFilter = $state<'all' | 'pinned' | 'archived' | 'trash'>('all');
|
|
let view = $state<'grid' | 'list'>('grid');
|
|
|
|
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));
|
|
|
|
pushState(`/notes/${note.id}`, { selectedNoteId: note.id });
|
|
}
|
|
|
|
function openNote(noteId: string) {
|
|
pushState(`/notes/${noteId}`, { selectedNoteId: noteId });
|
|
}
|
|
|
|
function closeNote() {
|
|
history.back();
|
|
}
|
|
|
|
const selectedNoteId = $derived(page.state.selectedNoteId as string | undefined);
|
|
</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
|
|
filter={currentFilter}
|
|
search={searchQuery}
|
|
{view}
|
|
onNoteClick={openNote}
|
|
/>
|
|
</div>
|
|
|
|
<!-- Note Editor Modal -->
|
|
<Dialog.Root open={!!selectedNoteId} onOpenChange={(open) => !open && closeNote()}>
|
|
<Dialog.Content class="w-[90vw]! max-w-6xl! h-[90vh] flex flex-col overflow-hidden">
|
|
{#if selectedNoteId}
|
|
<NoteEditor noteId={selectedNoteId} />
|
|
{/if}
|
|
</Dialog.Content>
|
|
</Dialog.Root>
|