feat(04-02): add pin button, due date picker, and pinned section UI

- Add pin button in expanded view with toggle functionality
- Add due date picker in expanded view with date input
- Show pin indicator and due date in collapsed view
- Separate EntryList into pinned and unpinned sections
- Pinned section appears at top with header label
This commit is contained in:
Thomas Richter
2026-01-31 13:04:10 +01:00
parent 378d928b53
commit 164fc73532
2 changed files with 85 additions and 5 deletions

View File

@@ -27,6 +27,7 @@
let editTitle = $state(entry.title || ''); let editTitle = $state(entry.title || '');
let editContent = $state(entry.content); let editContent = $state(entry.content);
let editType = $state(entry.type); let editType = $state(entry.type);
let editDueDate = $state(entry.dueDate || '');
// Sync edit state when entry changes (after invalidateAll) // Sync edit state when entry changes (after invalidateAll)
$effect(() => { $effect(() => {
@@ -34,6 +35,7 @@
editTitle = entry.title || ''; editTitle = entry.title || '';
editContent = entry.content; editContent = entry.content;
editType = entry.type; editType = entry.type;
editDueDate = entry.dueDate || '';
} }
}); });
@@ -155,6 +157,34 @@
// invalidateAll is called by ImageUpload, so nothing extra needed here // invalidateAll is called by ImageUpload, so nothing extra needed here
} }
async function handleTogglePin() {
const formData = new FormData();
formData.append('id', entry.id);
await fetch('?/togglePin', {
method: 'POST',
body: formData
});
await invalidateAll();
}
async function handleDueDateChange(e: Event) {
const input = e.target as HTMLInputElement;
editDueDate = input.value;
const formData = new FormData();
formData.append('id', entry.id);
formData.append('dueDate', input.value);
await fetch('?/updateDueDate', {
method: 'POST',
body: formData
});
await invalidateAll();
}
async function handleCameraInput(e: Event) { async function handleCameraInput(e: Event) {
const input = e.target as HTMLInputElement; const input = e.target as HTMLInputElement;
const file = input.files?.[0]; const file = input.files?.[0];
@@ -266,6 +296,14 @@
{#if isSaving} {#if isSaving}
<span class="text-xs text-gray-400">Saving...</span> <span class="text-xs text-gray-400">Saving...</span>
{/if} {/if}
{#if entry.pinned}
<svg class="w-4 h-4 text-yellow-500" fill="currentColor" viewBox="0 0 20 20" aria-label="Pinned">
<path d="M5 4a2 2 0 012-2h6a2 2 0 012 2v3.586l1.707 1.707A1 1 0 0117 10v1a1 1 0 01-1 1h-4v5a1 1 0 11-2 0v-5H6a1 1 0 01-1-1v-1a1 1 0 01.293-.707L7 7.586V4z" />
</svg>
{/if}
{#if entry.dueDate && !expanded}
<span class="text-xs text-gray-500">{entry.dueDate}</span>
{/if}
{#if entry.images?.length > 0 && !expanded} {#if entry.images?.length > 0 && !expanded}
<span <span
class="flex items-center gap-1 text-xs text-gray-500" class="flex items-center gap-1 text-xs text-gray-500"
@@ -404,6 +442,31 @@
</div> </div>
</div> </div>
<!-- Pin and Due Date row -->
<div class="flex items-center gap-4">
<button
type="button"
onclick={handleTogglePin}
class="p-2 rounded-lg hover:bg-gray-100 {entry.pinned ? 'text-yellow-500' : 'text-gray-400'}"
aria-label={entry.pinned ? 'Unpin entry' : 'Pin entry'}
>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path d="M5 4a2 2 0 012-2h6a2 2 0 012 2v3.586l1.707 1.707A1 1 0 0117 10v1a1 1 0 01-1 1h-4v5a1 1 0 11-2 0v-5H6a1 1 0 01-1-1v-1a1 1 0 01.293-.707L7 7.586V4z" />
</svg>
</button>
<div>
<label for="due-date-{entry.id}" class="block text-sm font-medium text-gray-700 mb-1">Due Date</label>
<input
id="due-date-{entry.id}"
type="date"
value={editDueDate}
onchange={handleDueDateChange}
class="px-3 py-2 border border-gray-200 rounded-lg text-base focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<label for="edit-type-{entry.id}" class="block text-sm font-medium text-gray-700 mb-1" <label for="edit-type-{entry.id}" class="block text-sm font-medium text-gray-700 mb-1"

View File

@@ -11,6 +11,10 @@
} }
let { entries }: Props = $props(); let { entries }: Props = $props();
// Separate entries into pinned and unpinned
let pinnedEntries = $derived(entries.filter(e => e.pinned));
let unpinnedEntries = $derived(entries.filter(e => !e.pinned));
</script> </script>
{#if entries.length === 0} {#if entries.length === 0}
@@ -19,9 +23,22 @@
<p class="text-sm mt-1">Use the capture bar below to add your first entry</p> <p class="text-sm mt-1">Use the capture bar below to add your first entry</p>
</div> </div>
{:else} {:else}
<div class="divide-y divide-gray-100 md:divide-y-0 md:space-y-3"> {#if pinnedEntries.length > 0}
{#each entries as entry (entry.id)} <div class="mb-4">
<EntryCard {entry} /> <h2 class="text-sm font-medium text-gray-500 uppercase tracking-wide mb-2 px-4 md:px-0">Pinned</h2>
{/each} <div class="divide-y divide-gray-100 md:divide-y-0 md:space-y-3">
</div> {#each pinnedEntries as entry (entry.id)}
<EntryCard {entry} />
{/each}
</div>
</div>
{/if}
{#if unpinnedEntries.length > 0}
<div class="divide-y divide-gray-100 md:divide-y-0 md:space-y-3">
{#each unpinnedEntries as entry (entry.id)}
<EntryCard {entry} />
{/each}
</div>
{/if}
{/if} {/if}