Phase 03: Images - 4 plan(s) in 3 wave(s) - Wave 1: 03-01 (foundation) - Wave 2: 03-02, 03-03 (parallel - upload + camera) - Wave 3: 03-04 (integration + verification) - Ready for execution Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
8.8 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 03-images | 02 | execute | 2 |
|
|
true |
|
Purpose: Enables users to attach images to entries via file selection or drag-and-drop on desktop. This is the primary upload path for desktop users.
Output:
- Form action that handles multipart file uploads
- ImageUpload component with button and drag-drop zone
- Integration with QuickCapture for upload during entry creation
<execution_context> @/home/tho/.claude/get-shit-done/workflows/execute-plan.md @/home/tho/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/phases/03-images/03-RESEARCH.md @.planning/phases/03-images/03-01-SUMMARY.md @src/routes/+page.server.ts @src/lib/components/QuickCapture.svelte Task 1: Upload form action with thumbnail generation src/routes/+page.server.ts Add uploadImage action to existing +page.server.ts actions:-
Import at top:
- import { imageRepository } from '$lib/server/db/repository'
- import { saveOriginal, saveThumbnail, ensureDirectories } from '$lib/server/images/storage'
- import { generateThumbnail } from '$lib/server/images/thumbnails'
- import { nanoid } from 'nanoid'
-
Add uploadImage action:
uploadImage: async ({ request }) => { const formData = await request.formData(); const file = formData.get('image') as File; const entryId = formData.get('entryId') as string; if (!file || file.size === 0) { return fail(400, { error: 'No file uploaded' }); } if (!entryId) { return fail(400, { error: 'Entry ID required' }); } // Verify entry exists const entry = entryRepository.getById(entryId); if (!entry) { return fail(404, { error: 'Entry not found' }); } // Validate image type if (!file.type.startsWith('image/')) { return fail(400, { error: 'File must be an image' }); } const imageId = nanoid(); const ext = file.name.split('.').pop()?.toLowerCase() || 'jpg'; const buffer = Buffer.from(await file.arrayBuffer()); try { await ensureDirectories(); // Save original await saveOriginal(imageId, ext, buffer); // Generate and save thumbnail const thumbnailBuffer = await generateThumbnail(buffer); await saveThumbnail(imageId, thumbnailBuffer); // Save to database imageRepository.create({ entryId, filename: file.name, ext }); return { success: true, imageId }; } catch (err) { console.error('Upload error:', err); return fail(500, { error: 'Failed to save image' }); } } -
Update the load function to include images for each entry:
- After getting entries, for each entry fetch its images via imageRepository.getByEntryId(entry.id)
- Return entries with their images attached (map over entries)
Run
npm run check- TypeScript compiles. Verify action exists:grep -n "uploadImage:" src/routes/+page.server.tsuploadImage action handles file upload, generates thumbnail, saves to database. load function returns entries with their images attached.
Props:
- entryId: string (required)
- onUploadStart?: () => void (optional callback for optimistic UI)
- onUploadComplete?: (imageId: string) => void (optional callback)
- onUploadError?: (error: string) => void (optional callback)
State:
- isDragging: boolean ($state)
- isUploading: boolean ($state)
- previewUrl: string | null ($state) - for optimistic preview
- fileInput: HTMLInputElement reference
Functions:
- handleDragOver(e: DragEvent): preventDefault, set isDragging true
- handleDragLeave(e: DragEvent): preventDefault, set isDragging false
- handleDrop(e: DragEvent): preventDefault, isDragging false, get first file, call uploadFile
- handleFileSelect(e: Event): get file from input, call uploadFile
- uploadFile(file: File):
- Validate file.type starts with 'image/'
- Create optimistic preview: previewUrl = URL.createObjectURL(file)
- Call onUploadStart?.()
- Set isUploading true
- Create FormData with 'image' and 'entryId'
- fetch('?/uploadImage', { method: 'POST', body: formData })
- Parse response, handle errors
- Call onUploadComplete or onUploadError
- Revoke preview URL, clear state
- Call invalidateAll() to refresh list
Template:
- Drop zone div with:
- Border dashed when not dragging, solid blue when dragging
- ondragover, ondragleave, ondrop handlers
- role="button", tabindex="0"
- onclick opens file input
- onkeydown Enter opens file input
- Hidden file input with accept="image/*"
- Show upload icon and "Drop image or click to upload" text
- Show spinner when isUploading
- Show optimistic preview image if previewUrl
Styling (Tailwind):
- Rounded border, dashed gray
- Blue border/bg when dragging
- Centered content with icon
- Touch-friendly size (min-h-[100px])
Run
npm run check- TypeScript compiles. File exists with core functions:grep -c "handleDrop\|uploadFile" src/lib/components/ImageUpload.svelteImageUpload component renders drag-drop zone with button fallback. Shows optimistic preview during upload. Calls form action and refreshes list on completion.
-
Add state for pending image:
- pendingImage: File | null ($state)
- pendingPreviewUrl: string | null ($state)
-
Add hidden file input with accept="image/*" after the form
-
Add image upload button next to the submit button:
- Icon-only button (camera/image icon)
- Opens file picker on click
- Disabled when already has pending image
-
When file selected:
- Store in pendingImage
- Create preview URL for display
- Show small thumbnail preview next to input
-
Modify form submission flow:
- If pendingImage exists: a. First submit the entry via existing create action b. Get the new entry ID from response c. Then upload the image with that entry ID d. Revoke preview URL e. Clear pendingImage
-
Add cancel button to remove pending image before submit
-
Show thumbnail preview (small, ~40px) inline when image is pending
Keep the quick capture bar clean - image attachment is optional, not prominent.
Run npm run check - TypeScript compiles.
Run npm run dev and verify QuickCapture shows image button.
QuickCapture has image attachment button.
User can add image before submitting new entry.
Image uploads after entry creation with the new entry ID.
<success_criteria>
- Form action accepts multipart file upload
- Thumbnails generated with Sharp (EXIF rotation handled)
- ImageUpload component provides drag-drop and button interfaces
- Optimistic preview shows image immediately
- QuickCapture allows attaching image to new entry </success_criteria>