mirror of
https://github.com/bretzel-app/crumbs.git
synced 2026-10-12 20:06:57 +02:00
feat: wire ImageUpload into NoteEditor and show thumbnails on NoteCard
- Import ImageUpload into NoteEditor with attachment state management - Add $effect to fetch attachments for existing notes loaded from IDB - Add upload/remove handlers and image toggle button in toolbar - Show ImageUpload panel between content and formatting toolbar - Add full-bleed thumbnail strip at top of NoteCard (up to 3 images) - Use &thumb=1 URL for card previews with lazy loading - Show +N badge when more than 3 attachments Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
committed by
Fred Cabassut
co-authored by
Claude Opus 4.6
parent
f7e7aaa97d
commit
cc6181aba1
@@ -78,6 +78,28 @@
|
||||
data-testid="note-card"
|
||||
data-note-id={note.id}
|
||||
>
|
||||
<!-- Thumbnail strip -->
|
||||
{#if note.attachments && note.attachments.length > 0}
|
||||
<div class="-mx-4 -mt-4 mb-3 flex overflow-hidden rounded-t-sm" data-testid="card-thumbnails">
|
||||
{#each note.attachments.slice(0, 3) as attachment}
|
||||
<div class="relative min-w-0 flex-1">
|
||||
<img
|
||||
src="/api/notes/{note.id}/attachments?attachmentId={attachment.id}&thumb=1"
|
||||
alt={attachment.filename}
|
||||
class="h-24 w-full object-cover"
|
||||
loading="lazy"
|
||||
data-testid="card-thumbnail"
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{#if note.attachments.length > 3}
|
||||
<div class="absolute right-1 top-1 rounded-sm bg-black/60 px-1.5 py-0.5 text-xs font-medium text-white" data-testid="card-thumbnail-count">
|
||||
+{note.attachments.length - 3}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if note.pinned}
|
||||
<button
|
||||
onclick={stop(() => togglePin(note.id, note.pinned))}
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
import Checklist from './Checklist.svelte';
|
||||
import FormattingToolbar from './FormattingToolbar.svelte';
|
||||
import TiptapEditor from './TiptapEditor.svelte';
|
||||
import ImageUpload from './ImageUpload.svelte';
|
||||
import { updateNote, createNote } from '$lib/stores/notes.js';
|
||||
import { NOTE_COLORS } from '$lib/utils/colors.js';
|
||||
import type { Editor } from '@tiptap/core';
|
||||
import type { Note, NoteColor } from '$lib/types/index.js';
|
||||
import type { Note, NoteColor, Attachment } from '$lib/types/index.js';
|
||||
import Palette from 'lucide-svelte/icons/palette';
|
||||
import SquareCheck from 'lucide-svelte/icons/square-check';
|
||||
import ImageIcon from 'lucide-svelte/icons/image';
|
||||
import Type from 'lucide-svelte/icons/type';
|
||||
import FileCode from 'lucide-svelte/icons/file-code';
|
||||
import FileText from 'lucide-svelte/icons/file-text';
|
||||
@@ -30,11 +32,43 @@
|
||||
// svelte-ignore state_referenced_locally
|
||||
let checklistMode = $state(note?.checklistMode ?? false);
|
||||
let showColorPicker = $state(false);
|
||||
let showImageUpload = $state(false);
|
||||
let rawMarkdownMode = $state(note?.checklistMode ?? false);
|
||||
let textareaEl: HTMLTextAreaElement | undefined = $state();
|
||||
let tiptapEditor: Editor | undefined = $state();
|
||||
let editorTick = $state(0);
|
||||
|
||||
// svelte-ignore state_referenced_locally
|
||||
let attachmentsList = $state<Attachment[]>(note?.attachments ?? []);
|
||||
|
||||
// Fetch attachments for existing notes if not pre-populated (e.g. loaded from IDB)
|
||||
$effect(() => {
|
||||
if (note && !isNew && (!note.attachments || note.attachments.length === 0)) {
|
||||
fetch(`/api/notes/${note.id}/attachments`)
|
||||
.then((res) => res.ok ? res.json() : [])
|
||||
.then((data: Attachment[]) => {
|
||||
if (data.length > 0) attachmentsList = data;
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
function handleAttachmentUpload(attachment: Attachment) {
|
||||
attachmentsList = [...attachmentsList, attachment];
|
||||
}
|
||||
|
||||
async function handleAttachmentRemove(attachmentId: string) {
|
||||
if (!note) return;
|
||||
try {
|
||||
await fetch(`/api/notes/${note.id}/attachments?attachmentId=${attachmentId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
attachmentsList = attachmentsList.filter((a) => a.id !== attachmentId);
|
||||
} catch (err) {
|
||||
console.error('Failed to remove attachment:', err);
|
||||
}
|
||||
}
|
||||
|
||||
let bgStyle = $state('');
|
||||
$effect(() => {
|
||||
const colors = NOTE_COLORS[color];
|
||||
@@ -124,6 +158,30 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Image attachments -->
|
||||
{#if !isNew && note && showImageUpload}
|
||||
<div class="border-t border-[var(--border-subtle)] px-4 py-2">
|
||||
<ImageUpload
|
||||
noteId={note.id}
|
||||
attachments={attachmentsList}
|
||||
onUpload={handleAttachmentUpload}
|
||||
onRemove={handleAttachmentRemove}
|
||||
/>
|
||||
</div>
|
||||
{:else if !isNew && attachmentsList.length > 0}
|
||||
<div class="border-t border-[var(--border-subtle)] px-4 py-2">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each attachmentsList as attachment}
|
||||
<img
|
||||
src="/api/notes/{note?.id}/attachments?attachmentId={attachment.id}"
|
||||
alt={attachment.filename}
|
||||
class="h-20 w-20 rounded-sm object-cover"
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Formatting toolbar -->
|
||||
{#if !rawMarkdownMode && !checklistMode}
|
||||
<FormattingToolbar editor={tiptapEditor} tick={editorTick} />
|
||||
@@ -163,6 +221,17 @@
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<!-- Image attachment toggle -->
|
||||
<button
|
||||
onclick={() => (showImageUpload = !showImageUpload)}
|
||||
class="rounded-sm p-2 hover:bg-[var(--border)]/10 disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
title="Image attachments"
|
||||
disabled={isNew}
|
||||
data-testid="image-toggle"
|
||||
>
|
||||
<ImageIcon class="h-5 w-5 {showImageUpload ? 'text-[var(--primary)]' : ''}" />
|
||||
</button>
|
||||
|
||||
<!-- Raw markdown mode toggle -->
|
||||
<button
|
||||
onclick={toggleMarkdownMode}
|
||||
|
||||
Reference in New Issue
Block a user