Files
crumbs/src/lib/components/ImageUpload.svelte
T
Frederic CabassutandClaude Opus 4.6 f2fd2acfff style: replace all inline SVGs with Lucide icons
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>
2026-03-06 23:12:36 +01:00

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>