feat(04-03): create TagInput component and integrate into EntryCard

- Create TagInput.svelte with Svelecte for multi-select autocomplete
- Support creating new tags inline via creatable mode
- Display tags on collapsed entry cards (max 3 with +N indicator)
- Add TagInput in expanded view for editing entry tags
- Pass availableTags through EntryList to EntryCard
- Handle tag changes with immediate save via updateTags action
This commit is contained in:
Thomas Richter
2026-01-31 13:09:34 +01:00
parent cfdb804118
commit 0c1a66b4c6
4 changed files with 117 additions and 12 deletions

View File

@@ -1,20 +1,23 @@
<script lang="ts">
import type { Entry, Image } from '$lib/server/db/schema';
import type { Entry, Image, Tag } from '$lib/server/db/schema';
import { enhance } from '$app/forms';
import { invalidateAll } from '$app/navigation';
import { slide } from 'svelte/transition';
import ImageGallery from './ImageGallery.svelte';
import ImageUpload from './ImageUpload.svelte';
interface EntryWithImages extends Entry {
import TagInput from './TagInput.svelte';
interface EntryWithData extends Entry {
images: Image[];
tags: Tag[];
}
interface Props {
entry: EntryWithImages;
entry: EntryWithData;
availableTags: Tag[];
}
let { entry }: Props = $props();
let { entry, availableTags }: Props = $props();
// Expand/collapse state
let expanded = $state(false);
@@ -28,6 +31,7 @@
let editContent = $state(entry.content);
let editType = $state(entry.type);
let editDueDate = $state(entry.dueDate || '');
let editTags = $state<Tag[]>(entry.tags || []);
// Sync edit state when entry changes (after invalidateAll)
$effect(() => {
@@ -36,6 +40,7 @@
editContent = entry.content;
editType = entry.type;
editDueDate = entry.dueDate || '';
editTags = entry.tags || [];
}
});
@@ -185,6 +190,22 @@
await invalidateAll();
}
async function handleTagsChange(newTags: Tag[]) {
editTags = newTags;
// Save immediately (no debounce for tags)
const formData = new FormData();
formData.append('id', entry.id);
formData.append('tags', JSON.stringify(newTags.map((t) => t.name)));
await fetch('?/updateTags', {
method: 'POST',
body: formData
});
await invalidateAll();
}
async function handleCameraInput(e: Event) {
const input = e.target as HTMLInputElement;
const file = input.files?.[0];
@@ -289,6 +310,18 @@
>
{entry.content}
</p>
{#if entry.tags?.length > 0}
<div class="flex flex-wrap gap-1 mt-1">
{#each entry.tags.slice(0, 3) as tag}
<span class="text-xs px-2 py-0.5 bg-gray-100 text-gray-600 rounded-full"
>{tag.name}</span
>
{/each}
{#if entry.tags.length > 3}
<span class="text-xs text-gray-400">+{entry.tags.length - 3}</span>
{/if}
</div>
{/if}
{/if}
</div>
@@ -467,6 +500,12 @@
</div>
</div>
<!-- Tags section -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Tags</label>
<TagInput {availableTags} selectedTags={editTags} onchange={handleTagsChange} />
</div>
<div class="flex items-center justify-between">
<div>
<label for="edit-type-{entry.id}" class="block text-sm font-medium text-gray-700 mb-1"