refactor: simplify notes components to use service directly
- 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>
This commit is contained in:
@@ -1,104 +1,96 @@
|
||||
<script lang="ts">
|
||||
import * as Card from '$lib/components/ui/card/index.js';
|
||||
import { Badge } from '$lib/components/ui/badge/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { Pin } from '@lucide/svelte';
|
||||
import type { Note } from '$lib/triplit/schema';
|
||||
import NoteActionMenu from './NoteActionMenu.svelte';
|
||||
import { Carta, Markdown } from 'carta-md';
|
||||
import DOMPurify from 'isomorphic-dompurify';
|
||||
import { getNoteService } from '$lib/context/notes.svelte';
|
||||
|
||||
let { note, view = 'grid', onclick }: { note: Note; view?: 'grid' | 'list'; onclick?: () => void } = $props();
|
||||
let { noteId, view = 'grid', onclick }: { noteId: string; view?: 'grid' | 'list'; onclick?: () => void } = $props();
|
||||
|
||||
const carta = new Carta({ sanitizer: DOMPurify.sanitize });
|
||||
const noteService = getNoteService();
|
||||
const note = $derived(noteService.getNoteById(noteId));
|
||||
|
||||
// Get first line of content as title if title is empty
|
||||
const displayTitle = $derived(note.title || note.content.split('\n')[0] || 'Untitled');
|
||||
|
||||
// Truncate content for preview
|
||||
const previewContent = $derived(() => {
|
||||
const lines = view === 'grid' ? 3 : 2;
|
||||
const contentLines = note.content.split('\n').slice(0, lines);
|
||||
const preview = contentLines.join('\n');
|
||||
return preview.length > 150 ? preview.substring(0, 150) + '...' : preview;
|
||||
const carta = new Carta({
|
||||
sanitizer: DOMPurify.sanitize,
|
||||
theme: { light: 'github-light', dark: 'github-dark' }
|
||||
});
|
||||
|
||||
// Format relative time
|
||||
function formatRelativeTime(date: Date): string {
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const seconds = Math.floor(diff / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
function handleUnpin(e: Event) {
|
||||
e.stopPropagation();
|
||||
if (note) noteService.updateNote(note.id, { pinned: false });
|
||||
}
|
||||
|
||||
function getPreview(content: string) {
|
||||
const lines = view === 'grid' ? 3 : 2;
|
||||
const preview = content.split('\n').slice(0, lines).join('\n');
|
||||
return preview.length > 150 ? preview.substring(0, 150) + '...' : preview;
|
||||
}
|
||||
|
||||
function getRelativeTime(date: Date) {
|
||||
const diff = Date.now() - date.getTime();
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
const months = Math.floor(days / 30);
|
||||
const years = Math.floor(days / 365);
|
||||
|
||||
if (years > 0) return `${years}y ago`;
|
||||
if (months > 0) return `${months}mo ago`;
|
||||
if (days > 0) return `${days}d ago`;
|
||||
if (hours > 0) return `${hours}h ago`;
|
||||
if (minutes > 0) return `${minutes}m ago`;
|
||||
return 'Just now';
|
||||
}
|
||||
|
||||
const tags = $derived(Array.from(note.tags));
|
||||
</script>
|
||||
|
||||
<Card.Root
|
||||
class="cursor-pointer transition-all hover:shadow-md gap-2 {view === 'list' ? 'flex flex-row' : ''}"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
{onclick}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
onclick?.();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="flex items-start justify-between gap-2 px-6">
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||
{#if note.pinned}
|
||||
<Pin class="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
{/if}
|
||||
<h3 class="font-semibold text-base line-clamp-1">{displayTitle}</h3>
|
||||
</div>
|
||||
<div onclick={(e) => e.stopPropagation()}>
|
||||
<NoteActionMenu {note} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card.Content class="flex-1">
|
||||
<!-- Content Preview -->
|
||||
{#if note.content}
|
||||
<div class="note-preview text-sm text-muted-foreground {view === 'grid' ? 'line-clamp-3' : 'line-clamp-2'} overflow-hidden">
|
||||
<Markdown {carta} value={previewContent()} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Tags -->
|
||||
{#if tags.length > 0}
|
||||
<div class="flex flex-wrap gap-1 mt-3">
|
||||
{#each tags.slice(0, 3) as tag (tag)}
|
||||
<Badge variant="outline" class="text-xs">
|
||||
{tag}
|
||||
</Badge>
|
||||
{/each}
|
||||
{#if tags.length > 3}
|
||||
<Badge variant="outline" class="text-xs">
|
||||
+{tags.length - 3}
|
||||
</Badge>
|
||||
{#if note}
|
||||
<Card.Root
|
||||
class="cursor-pointer transition-all hover:shadow-md gap-2 {view === 'list' ? 'flex flex-row' : ''}"
|
||||
role="button"
|
||||
tabindex={0}
|
||||
{onclick}
|
||||
onkeydown={(e) => (e.key === 'Enter' || e.key === ' ') && (e.preventDefault(), onclick?.())}
|
||||
>
|
||||
<div class="flex items-center justify-between gap-2 pl-6 pr-2">
|
||||
<h3 class="font-semibold text-base line-clamp-1 flex-1 min-w-0">
|
||||
{note.title || note.content.split('\n')[0] || 'Untitled'}
|
||||
</h3>
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
{#if note.pinned}
|
||||
<Button variant="ghost" size="icon-sm" onclick={handleUnpin} title="Unpin note" class="group">
|
||||
<Pin class="h-4 w-4 text-primary transition-all group-hover:rotate-45 group-hover:text-muted-foreground" />
|
||||
<span class="sr-only">Unpin note</span>
|
||||
</Button>
|
||||
{/if}
|
||||
<NoteActionMenu {note} />
|
||||
</div>
|
||||
{/if}
|
||||
</Card.Content>
|
||||
</div>
|
||||
|
||||
<Card.Footer>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{formatRelativeTime(note.updatedAt)}
|
||||
</span>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
<Card.Content class="flex-1">
|
||||
{#if note.content}
|
||||
<div class="note-preview text-sm text-muted-foreground {view === 'grid' ? 'line-clamp-3' : 'line-clamp-2'} overflow-hidden">
|
||||
{#key note.content}
|
||||
<Markdown {carta} value={getPreview(note.content)} />
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if note.tags.size > 0}
|
||||
{@const tags = Array.from(note.tags)}
|
||||
<div class="flex flex-wrap gap-1 mt-3">
|
||||
{#each tags.slice(0, 3) as tag (tag)}
|
||||
<Badge variant="outline" class="text-xs">{tag}</Badge>
|
||||
{/each}
|
||||
{#if tags.length > 3}
|
||||
<Badge variant="outline" class="text-xs">+{tags.length - 3}</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</Card.Content>
|
||||
|
||||
<Card.Footer class="justify-end">
|
||||
<span class="text-xs text-muted-foreground">{getRelativeTime(note.updatedAt)}</span>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.note-preview :global(*) {
|
||||
|
||||
Reference in New Issue
Block a user