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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user