feat(03-02): integrate image upload into QuickCapture

- Add image button next to submit button
- Show pending image preview with filename
- Allow removing pending image before submit
- Upload image after entry creation with new entry ID
- Disable button during upload to prevent duplicates
- Clean up object URLs properly
This commit is contained in:
Thomas Richter
2026-01-29 15:28:22 +01:00
parent 8248e0cd91
commit 400e4999fd

View File

@@ -6,26 +6,84 @@
let content = $state('');
let type = $state<'task' | 'thought'>('thought');
// Pending image state
let pendingImage: File | null = $state(null);
let pendingPreviewUrl: string | null = $state(null);
let isUploading = $state(false);
let fileInput: HTMLInputElement;
// Initialize from preferences (client-side only)
$effect(() => {
if (typeof window !== 'undefined') {
type = $preferences.lastEntryType;
}
});
function handleImageSelect(e: Event) {
const input = e.target as HTMLInputElement;
const file = input.files?.[0];
if (file && file.type.startsWith('image/')) {
// Revoke previous preview if exists
if (pendingPreviewUrl) {
URL.revokeObjectURL(pendingPreviewUrl);
}
pendingImage = file;
pendingPreviewUrl = URL.createObjectURL(file);
}
// Reset input so same file can be selected again
input.value = '';
}
function clearPendingImage() {
if (pendingPreviewUrl) {
URL.revokeObjectURL(pendingPreviewUrl);
}
pendingImage = null;
pendingPreviewUrl = null;
}
async function uploadImageForEntry(entryId: string, file: File) {
const formData = new FormData();
formData.append('image', file);
formData.append('entryId', entryId);
const response = await fetch('?/uploadImage', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.type === 'failure') {
console.error('Image upload failed:', result.data?.error);
}
}
</script>
<form
method="POST"
action="?/create"
use:enhance={() => {
const imageToUpload = pendingImage;
return async ({ result, update }) => {
if (result.type === 'success') {
if (result.type === 'success' && result.data && 'entryId' in result.data) {
// Update preference
$preferences.lastEntryType = type;
// Upload pending image if exists
if (imageToUpload) {
isUploading = true;
try {
await uploadImageForEntry(result.data.entryId as string, imageToUpload);
} finally {
isUploading = false;
clearPendingImage();
}
}
// Clear form
title = '';
content = '';
// Let SvelteKit refresh data
}
await update();
};
@@ -40,15 +98,50 @@
placeholder="Title (optional)"
class="w-full px-3 py-2 border border-gray-200 rounded-lg text-base focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<div class="flex gap-2">
<div class="flex gap-2 items-end">
<div class="flex-1 flex flex-col gap-2">
<textarea
name="content"
bind:value={content}
placeholder="What's on your mind?"
required
rows="1"
class="flex-1 px-3 py-2 border border-gray-200 rounded-lg text-base resize-none focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
class="w-full px-3 py-2 border border-gray-200 rounded-lg text-base resize-none focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
></textarea>
{#if pendingPreviewUrl}
<div class="flex items-center gap-2">
<img
src={pendingPreviewUrl}
alt="Pending upload"
class="w-10 h-10 object-cover rounded border border-gray-200"
/>
<span class="text-xs text-gray-500 truncate max-w-[150px]">
{pendingImage?.name}
</span>
<button
type="button"
onclick={clearPendingImage}
class="p-1 text-gray-400 hover:text-red-500"
title="Remove image"
>
<svg
class="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
{/if}
</div>
<div class="flex flex-col gap-1">
<select
name="type"
@@ -58,14 +151,47 @@
<option value="thought">Thought</option>
<option value="task">Task</option>
</select>
<div class="flex gap-1">
<button
type="button"
onclick={() => fileInput?.click()}
disabled={!!pendingImage || isUploading}
class="p-2 border border-gray-200 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
title="Add image"
>
<svg
class="w-5 h-5 text-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
</button>
<button
type="submit"
class="px-4 py-2 bg-blue-600 text-white rounded-lg touch-target font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
disabled={isUploading}
class="px-4 py-2 bg-blue-600 text-white rounded-lg touch-target font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50"
>
Add
{isUploading ? '...' : 'Add'}
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Hidden file input -->
<input
bind:this={fileInput}
type="file"
accept="image/*"
onchange={handleImageSelect}
class="hidden"
/>
</form>