mirror of
https://github.com/bretzel-app/crumbs.git
synced 2026-10-12 20:06:57 +02:00
Swap every inline SVG across Header, SearchBar, NoteCard, NoteEditor, Checklist, and ImageUpload for Lucide components. Use Palette for the color picker icon instead of the paintbrush. Zero inline SVGs remain. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
107 lines
2.7 KiB
Svelte
107 lines
2.7 KiB
Svelte
<script lang="ts">
|
|
import XIcon from 'lucide-svelte/icons/x';
|
|
import type { Attachment } from '$lib/types/index.js';
|
|
|
|
interface Props {
|
|
noteId: string;
|
|
attachments: Attachment[];
|
|
onUpload: (attachment: Attachment) => void;
|
|
onRemove: (attachmentId: string) => void;
|
|
}
|
|
|
|
let { noteId, attachments = [], onUpload, onRemove }: Props = $props();
|
|
|
|
let uploading = $state(false);
|
|
let dragOver = $state(false);
|
|
|
|
async function handleFiles(files: FileList | null) {
|
|
if (!files || !noteId) return;
|
|
|
|
uploading = true;
|
|
for (const file of files) {
|
|
if (!file.type.startsWith('image/')) continue;
|
|
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
try {
|
|
const res = await fetch(`/api/notes/${noteId}/attachments`, {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
if (res.ok) {
|
|
const attachment = await res.json();
|
|
onUpload(attachment);
|
|
}
|
|
} catch (err) {
|
|
console.error('Upload failed:', err);
|
|
}
|
|
}
|
|
uploading = false;
|
|
}
|
|
|
|
function handleDrop(e: DragEvent) {
|
|
e.preventDefault();
|
|
dragOver = false;
|
|
handleFiles(e.dataTransfer?.files ?? null);
|
|
}
|
|
|
|
function handleDragOver(e: DragEvent) {
|
|
e.preventDefault();
|
|
dragOver = true;
|
|
}
|
|
</script>
|
|
|
|
<div data-testid="image-upload">
|
|
<!-- Existing attachments -->
|
|
{#if attachments.length > 0}
|
|
<div class="mb-2 flex flex-wrap gap-2">
|
|
{#each attachments as attachment}
|
|
<div class="group relative">
|
|
<img
|
|
src="/api/notes/{noteId}/attachments/{attachment.id}"
|
|
alt={attachment.filename}
|
|
class="h-20 w-20 rounded-sm object-cover"
|
|
data-testid="attachment-thumbnail"
|
|
/>
|
|
<button
|
|
onclick={() => onRemove(attachment.id)}
|
|
class="absolute -right-1 -top-1 hidden rounded-full bg-red-500 p-0.5 text-white group-hover:block"
|
|
aria-label="Remove attachment"
|
|
data-testid="remove-attachment"
|
|
>
|
|
<XIcon class="h-3 w-3" />
|
|
</button>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Upload area -->
|
|
<div
|
|
class="rounded-sm border-2 border-dashed p-4 text-center transition-colors {dragOver ? 'border-[var(--primary)] bg-[var(--primary)]/5' : 'border-[var(--border-subtle)]'}"
|
|
ondrop={handleDrop}
|
|
ondragover={handleDragOver}
|
|
ondragleave={() => (dragOver = false)}
|
|
role="button"
|
|
tabindex="0"
|
|
data-testid="upload-dropzone"
|
|
>
|
|
{#if uploading}
|
|
<p class="text-sm text-[var(--text-muted)]">Uploading...</p>
|
|
{:else}
|
|
<label class="cursor-pointer">
|
|
<span class="text-sm text-[var(--text-muted)]">Drop images here or <span class="text-[var(--primary)] underline">browse</span></span>
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
multiple
|
|
class="hidden"
|
|
onchange={(e) => handleFiles((e.target as HTMLInputElement).files)}
|
|
data-testid="file-input"
|
|
/>
|
|
</label>
|
|
{/if}
|
|
</div>
|
|
</div>
|