diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index f7b146c..e9dcdb0 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -68,12 +68,13 @@ Plans: 3. User can view attached images inline with entry 4. User can remove image attachments from an entry 5. Images are stored on filesystem (not in database) -**Plans**: TBD +**Plans**: 4 plans Plans: -- [ ] 03-01: TBD -- [ ] 03-02: TBD -- [ ] 03-03: TBD +- [ ] 03-01-PLAN.md — Database schema, file storage, thumbnail generation, and API endpoints +- [ ] 03-02-PLAN.md — File upload form action and ImageUpload component with drag-drop +- [ ] 03-03-PLAN.md — CameraCapture component with getUserMedia and preview/confirm flow +- [ ] 03-04-PLAN.md — EntryCard integration with gallery, lightbox, and delete functionality ### Phase 4: Tags & Organization **Goal**: Users can organize entries with tags and quick access features @@ -136,7 +137,7 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 -> 6 |-------|----------------|--------|-----------| | 1. Foundation | 2/2 | Complete ✓ | 2026-01-29 | | 2. Core CRUD | 4/4 | Complete ✓ | 2026-01-29 | -| 3. Images | 0/3 | Not started | - | +| 3. Images | 0/4 | Ready | - | | 4. Tags & Organization | 0/3 | Not started | - | | 5. Search | 0/3 | Not started | - | | 6. Deployment | 0/2 | Not started | - | diff --git a/.planning/phases/03-images/03-01-PLAN.md b/.planning/phases/03-images/03-01-PLAN.md new file mode 100644 index 0000000..b1efd4b --- /dev/null +++ b/.planning/phases/03-images/03-01-PLAN.md @@ -0,0 +1,224 @@ +--- +phase: 03-images +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - src/lib/server/db/schema.ts + - src/lib/server/db/repository.ts + - src/lib/server/images/storage.ts + - src/lib/server/images/thumbnails.ts + - src/routes/api/images/[id]/+server.ts + - src/routes/api/images/[id]/thumbnail/+server.ts +autonomous: true + +must_haves: + truths: + - "Images table exists in database with entryId foreign key" + - "Image files can be written to filesystem" + - "Thumbnails can be generated from uploaded images" + - "Images can be served via API endpoints" + artifacts: + - path: "src/lib/server/db/schema.ts" + provides: "images table definition" + contains: "images = sqliteTable" + - path: "src/lib/server/db/repository.ts" + provides: "imageRepository with CRUD operations" + contains: "imageRepository" + - path: "src/lib/server/images/storage.ts" + provides: "File write/read operations" + exports: ["saveOriginal", "saveThumbnail", "deleteImage"] + - path: "src/lib/server/images/thumbnails.ts" + provides: "Sharp thumbnail generation" + exports: ["generateThumbnail"] + - path: "src/routes/api/images/[id]/+server.ts" + provides: "GET endpoint for original images" + exports: ["GET"] + - path: "src/routes/api/images/[id]/thumbnail/+server.ts" + provides: "GET endpoint for thumbnails" + exports: ["GET"] + key_links: + - from: "src/lib/server/images/thumbnails.ts" + to: "sharp" + via: "import sharp" + pattern: "import sharp from 'sharp'" + - from: "src/routes/api/images/[id]/+server.ts" + to: "src/lib/server/db/repository.ts" + via: "imageRepository.getById" + pattern: "imageRepository\\.getById" +--- + + +Create the foundation layer for image attachments: database schema, file storage utilities, thumbnail generation, and API endpoints for serving images. + +Purpose: Establishes the data model and infrastructure that upload components will use. Without this layer, images cannot be stored, processed, or retrieved. + +Output: +- Database table for images with entry relationship +- Repository layer for image CRUD +- Filesystem utilities for storing originals and thumbnails +- API endpoints that serve images to the browser + + + +@/home/tho/.claude/get-shit-done/workflows/execute-plan.md +@/home/tho/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/phases/03-images/03-RESEARCH.md +@src/lib/server/db/schema.ts +@src/lib/server/db/repository.ts +@src/lib/server/db/index.ts + + + + + + Task 1: Database schema and repository for images + + src/lib/server/db/schema.ts + src/lib/server/db/repository.ts + + +1. Install sharp: `npm install sharp` + +2. Add images table to schema.ts: + - id: text primary key (nanoid) + - entryId: text foreign key to entries.id (not null) + - filename: text (original filename for display) + - ext: text (file extension without dot: jpg, png, etc.) + - createdAt: text ISO timestamp + + Export Image and NewImage types. + +3. Add ImageRepository interface and implementation to repository.ts: + - create(image: Omit): Image + - getById(id: string): Image | undefined + - getByEntryId(entryId: string): Image[] + - delete(id: string): boolean + - deleteByEntryId(entryId: string): number (returns count deleted) + +4. Export imageRepository singleton alongside entryRepository. + +Use same patterns as existing entryRepository (nanoid for IDs, synchronous Drizzle operations). + + +Run `npm run check` - TypeScript compiles without errors. +Verify exports: `grep -n "imageRepository" src/lib/server/db/repository.ts` + + +images table defined with entryId relationship. +imageRepository exported with create, getById, getByEntryId, delete, deleteByEntryId methods. + + + + + Task 2: File storage and thumbnail generation utilities + + src/lib/server/images/storage.ts + src/lib/server/images/thumbnails.ts + + +1. Create src/lib/server/images/storage.ts: + - UPLOAD_DIR constant: 'data/uploads' + - ORIGINALS_DIR: 'data/uploads/originals' + - THUMBNAILS_DIR: 'data/uploads/thumbnails' + + Functions: + - ensureDirectories(): Creates directories if they don't exist (use mkdir recursive) + - saveOriginal(id: string, ext: string, buffer: Buffer): Promise + Writes to `${ORIGINALS_DIR}/${id}.${ext}` + - saveThumbnail(id: string, buffer: Buffer): Promise + Writes to `${THUMBNAILS_DIR}/${id}.jpg` (thumbnails always jpg) + - getOriginalPath(id: string, ext: string): string + - getThumbnailPath(id: string): string + - deleteImage(id: string, ext: string): Promise + Removes both original and thumbnail files (use try/catch, don't throw if missing) + +2. Create src/lib/server/images/thumbnails.ts: + - import sharp from 'sharp' + + Function: + - generateThumbnail(buffer: Buffer, size?: number): Promise + Default size: 150 + CRITICAL: Call sharp.rotate() FIRST to handle EXIF orientation from mobile photos + Then resize with fit: 'cover', position: 'center', withoutEnlargement: true + Output as jpeg with quality 80 + Return the thumbnail buffer (caller saves it) + + +Run `npm run check` - TypeScript compiles. +Run quick test: `node -e "import('sharp').then(s => console.log('sharp version:', s.default.versions?.sharp || 'loaded'))"` or just verify sharp installed in package.json. + + +storage.ts exports directory management and file I/O functions. +thumbnails.ts exports generateThumbnail that handles EXIF rotation. + + + + + Task 3: API endpoints for serving images + + src/routes/api/images/[id]/+server.ts + src/routes/api/images/[id]/thumbnail/+server.ts + + +1. Create src/routes/api/images/[id]/+server.ts: + - GET handler that: + a. Gets id from params + b. Looks up image in imageRepository.getById(id) + c. Returns 404 error if not found + d. Reads file from getOriginalPath(id, image.ext) + e. Returns Response with: + - Content-Type based on ext (jpg -> image/jpeg, png -> image/png, etc.) + - Content-Length header + - Cache-Control: public, max-age=31536000, immutable (images are immutable by ID) + f. Catches file read errors, returns 404 + +2. Create src/routes/api/images/[id]/thumbnail/+server.ts: + - Same pattern but reads from getThumbnailPath(id) + - Content-Type always image/jpeg (thumbnails are always jpg) + +Import imageRepository and storage functions. Use readFile from 'node:fs/promises'. + + +Run `npm run check` - TypeScript compiles. +Verify endpoints exist: `ls -la src/routes/api/images/` + + +GET /api/images/[id] serves original images with proper Content-Type. +GET /api/images/[id]/thumbnail serves thumbnails. +Both return 404 for missing images. + + + + + + +1. `npm run check` passes with no TypeScript errors +2. `npm run dev` starts without errors +3. Sharp is in dependencies: `grep sharp package.json` +4. All new files exist: + - src/lib/server/db/schema.ts contains `images = sqliteTable` + - src/lib/server/db/repository.ts exports `imageRepository` + - src/lib/server/images/storage.ts exists + - src/lib/server/images/thumbnails.ts exists + - src/routes/api/images/[id]/+server.ts exists + - src/routes/api/images/[id]/thumbnail/+server.ts exists + + + +- images table can store image metadata with entry relationship +- imageRepository provides typed CRUD for images +- Thumbnail generation uses Sharp with EXIF auto-rotation +- API endpoints can serve images from filesystem +- All code compiles without TypeScript errors + + + +After completion, create `.planning/phases/03-images/03-01-SUMMARY.md` + diff --git a/.planning/phases/03-images/03-02-PLAN.md b/.planning/phases/03-images/03-02-PLAN.md new file mode 100644 index 0000000..83185bc --- /dev/null +++ b/.planning/phases/03-images/03-02-PLAN.md @@ -0,0 +1,284 @@ +--- +phase: 03-images +plan: 02 +type: execute +wave: 2 +depends_on: ["03-01"] +files_modified: + - src/routes/+page.server.ts + - src/lib/components/ImageUpload.svelte + - src/lib/components/QuickCapture.svelte +autonomous: true + +must_haves: + truths: + - "User can upload an image via file picker" + - "User can upload an image via drag-and-drop" + - "Uploaded image appears immediately (optimistic preview)" + - "Image is stored on server with thumbnail generated" + artifacts: + - path: "src/routes/+page.server.ts" + provides: "uploadImage form action" + contains: "uploadImage:" + - path: "src/lib/components/ImageUpload.svelte" + provides: "Upload button + drag-drop zone" + min_lines: 50 + key_links: + - from: "src/lib/components/ImageUpload.svelte" + to: "src/routes/+page.server.ts" + via: "fetch to ?/uploadImage" + pattern: "fetch.*\\?/uploadImage" + - from: "src/routes/+page.server.ts" + to: "src/lib/server/images/storage.ts" + via: "saveOriginal import" + pattern: "import.*storage" +--- + + +Implement file upload functionality with drag-and-drop support and optimistic preview. + +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 + + + +@/home/tho/.claude/get-shit-done/workflows/execute-plan.md +@/home/tho/.claude/get-shit-done/templates/summary.md + + + +@.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: + +1. 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' + +2. Add uploadImage action: + ```typescript + 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' }); + } + } + ``` + +3. 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.ts` + + +uploadImage action handles file upload, generates thumbnail, saves to database. +load function returns entries with their images attached. + + + + + Task 2: ImageUpload component with drag-drop + + src/lib/components/ImageUpload.svelte + + +Create ImageUpload.svelte component: + +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): + 1. Validate file.type starts with 'image/' + 2. Create optimistic preview: previewUrl = URL.createObjectURL(file) + 3. Call onUploadStart?.() + 4. Set isUploading true + 5. Create FormData with 'image' and 'entryId' + 6. fetch('?/uploadImage', { method: 'POST', body: formData }) + 7. Parse response, handle errors + 8. Call onUploadComplete or onUploadError + 9. Revoke preview URL, clear state + 10. 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.svelte` + + +ImageUpload component renders drag-drop zone with button fallback. +Shows optimistic preview during upload. +Calls form action and refreshes list on completion. + + + + + Task 3: Integrate upload into QuickCapture + + src/lib/components/QuickCapture.svelte + + +Update QuickCapture.svelte to include image upload option: + +1. Add state for pending image: + - pendingImage: File | null ($state) + - pendingPreviewUrl: string | null ($state) + +2. Add hidden file input with accept="image/*" after the form + +3. 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 + +4. When file selected: + - Store in pendingImage + - Create preview URL for display + - Show small thumbnail preview next to input + +5. 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 + +6. Add cancel button to remove pending image before submit + +7. 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. + + + + + + +1. `npm run check` passes +2. `npm run dev` - server starts +3. Test file upload: + - Create a new entry + - Expand entry and look for upload option (will be added in 03-04) + - OR test via QuickCapture image button +4. Check upload directories created: `ls data/uploads/` +5. Check image in database: `sqlite3 data/taskplaner.db "SELECT * FROM images LIMIT 5;"` + + + +- 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 + + + +After completion, create `.planning/phases/03-images/03-02-SUMMARY.md` + diff --git a/.planning/phases/03-images/03-03-PLAN.md b/.planning/phases/03-images/03-03-PLAN.md new file mode 100644 index 0000000..6d4aade --- /dev/null +++ b/.planning/phases/03-images/03-03-PLAN.md @@ -0,0 +1,263 @@ +--- +phase: 03-images +plan: 03 +type: execute +wave: 2 +depends_on: ["03-01"] +files_modified: + - src/lib/components/CameraCapture.svelte +autonomous: true + +must_haves: + truths: + - "User can open camera modal" + - "User can see live camera preview" + - "User can capture a photo" + - "User can preview captured photo before confirming" + - "User can retake if not satisfied" + - "Camera stream stops when modal closes" + artifacts: + - path: "src/lib/components/CameraCapture.svelte" + provides: "Camera capture modal with preview and confirm" + min_lines: 100 + key_links: + - from: "src/lib/components/CameraCapture.svelte" + to: "navigator.mediaDevices.getUserMedia" + via: "browser API call" + pattern: "getUserMedia" + - from: "src/lib/components/CameraCapture.svelte" + to: "canvas.toBlob" + via: "photo capture" + pattern: "toBlob" +--- + + +Implement camera capture component for mobile photo taking with preview and confirm flow. + +Purpose: Enables users to take photos directly in the app, critical for the core use case of photographing paper notes. Direct camera access feels more native than file picker on mobile. + +Output: +- CameraCapture modal component with live preview +- Capture, preview, retake, confirm flow +- Proper stream cleanup to prevent battery drain + + + +@/home/tho/.claude/get-shit-done/workflows/execute-plan.md +@/home/tho/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/phases/03-images/03-RESEARCH.md +@.planning/phases/03-images/03-01-SUMMARY.md + + + + + + Task 1: CameraCapture component with getUserMedia + + src/lib/components/CameraCapture.svelte + + +Create CameraCapture.svelte - a modal for camera capture: + +Props: +- entryId: string (required) +- onClose: () => void (required - parent controls open state) +- onCapture: (imageId: string) => void (optional callback after upload) + +State: +- stream: MediaStream | null ($state) +- capturedBlob: Blob | null ($state) +- capturedPreviewUrl: string | null ($state) +- error: string | null ($state) +- isUploading: boolean ($state) +- facingMode: 'environment' | 'user' ($state) - default 'environment' (back camera) + +Element refs: +- videoElement: HTMLVideoElement +- canvasElement: HTMLCanvasElement + +Functions: + +1. startCamera(): + - Check navigator.mediaDevices?.getUserMedia exists, set error if not + - Stop existing stream if any + - Try getUserMedia with: video: { facingMode, width: { ideal: 1920 }, height: { ideal: 1080 } }, audio: false + - Catch errors: + - NotAllowedError: "Camera permission denied" + - NotFoundError: "No camera found" + - Other: "Could not access camera" + - Set stream, assign to videoElement.srcObject, call play() + +2. stopCamera(): + - stream?.getTracks().forEach(track => track.stop()) + - stream = null + - videoElement.srcObject = null + +3. capturePhoto(): + - Get 2D context from canvas + - Set canvas dimensions to video dimensions + - drawImage(videoElement, 0, 0) + - canvas.toBlob() with 'image/jpeg', 0.9 quality + - Store blob, create preview URL + - Stop camera (save battery while reviewing) + +4. retake(): + - Revoke preview URL + - Clear capturedBlob and capturedPreviewUrl + - startCamera() again + +5. confirmAndUpload(): + - isUploading = true + - Create FormData with blob as 'image' (filename: 'camera-capture.jpg') + - Add entryId + - fetch('?/uploadImage', { method: 'POST', body: formData }) + - Handle response + - Call onCapture, onClose + - Revoke preview URL, clean up + +6. switchCamera(): + - Toggle facingMode + - Restart camera with new facing mode + +7. handleClose(): + - stopCamera() + - Revoke any preview URL + - onClose() + +Lifecycle: +- $effect: startCamera() on mount +- $effect: cleanup on unmount (stopCamera) + +Template structure: +``` + + + + X + Flip + + + + + {#if error} + {error} + Close + {:else if capturedPreviewUrl} + + + {:else} + + + {/if} + + + + + + + + {#if capturedPreviewUrl} + Retake + + {isUploading ? 'Uploading...' : 'Use Photo'} + + {:else if !error} + + + + {/if} + + +``` + +CRITICAL: Always stop camera stream when component closes or captures. Camera indicator should not stay on. + +Styling: +- Full screen black background (typical camera UI) +- Large touch targets for capture button (min 64px) +- Video fills available space while maintaining aspect ratio + + +Run `npm run check` - TypeScript compiles. +File exists and has key functions: `grep -c "getUserMedia\|capturePhoto\|stopCamera" src/lib/components/CameraCapture.svelte` + + +CameraCapture modal with live preview renders. +Capture creates blob from canvas. +Preview shows captured image with retake/confirm options. +Camera stream properly stopped on close and after capture. + + + + + Task 2: Feature detection and graceful fallback + + src/lib/components/CameraCapture.svelte + + +Enhance CameraCapture with proper feature detection and error handling: + +1. Add hasCameraSupport check: + ```typescript + const hasCameraSupport = typeof navigator !== 'undefined' && + !!navigator.mediaDevices?.getUserMedia; + ``` + +2. If no camera support, show friendly message immediately (don't try to call getUserMedia): + "Camera not supported in this browser. Use the file upload option instead." + +3. Add isSecureContext check: + - Camera API requires HTTPS (except localhost) + - Show message if not secure: "Camera requires HTTPS connection" + +4. Handle permission denied gracefully: + - Clear error message explaining how to enable camera + - "Camera access denied. Check browser settings to allow camera." + +5. Handle no camera found: + - "No camera found on this device. Use file upload instead." + +6. Add timeout for camera start: + - If camera doesn't start within 10 seconds, show error + - Prevents infinite loading state + +7. Test on desktop without webcam - should show "No camera found" not crash + +The error states should have a close button that calls onClose(). + + +Run `npm run check` - TypeScript compiles. +Grep for error handling: `grep -c "NotAllowedError\|NotFoundError\|hasCameraSupport" src/lib/components/CameraCapture.svelte` + + +CameraCapture gracefully handles missing camera, permission denied, insecure context. +Error messages are user-friendly with action guidance. + + + + + + +1. `npm run check` passes +2. `npm run dev` - server starts +3. Component file exists: `ls src/lib/components/CameraCapture.svelte` +4. Has required functions: grep for getUserMedia, stopCamera, capturePhoto, toBlob +5. Has error handling: grep for NotAllowedError, NotFoundError + + + +- CameraCapture opens with live camera preview +- Capture button takes photo and shows preview +- Retake returns to live camera +- Confirm uploads the photo +- Camera stream stops when modal closes +- Graceful error handling for permission denied, no camera, unsupported browser + + + +After completion, create `.planning/phases/03-images/03-03-SUMMARY.md` + diff --git a/.planning/phases/03-images/03-04-PLAN.md b/.planning/phases/03-images/03-04-PLAN.md new file mode 100644 index 0000000..de9efb5 --- /dev/null +++ b/.planning/phases/03-images/03-04-PLAN.md @@ -0,0 +1,414 @@ +--- +phase: 03-images +plan: 04 +type: execute +wave: 3 +depends_on: ["03-02", "03-03"] +files_modified: + - src/lib/components/EntryCard.svelte + - src/lib/components/ImageGallery.svelte + - src/lib/components/ImageLightbox.svelte + - src/routes/+page.server.ts + - src/routes/+page.svelte +autonomous: false + +must_haves: + truths: + - "User sees thumbnail on entry card when images are attached" + - "User sees horizontal scrolling gallery in expanded entry" + - "User can tap image to view fullscreen in lightbox" + - "User can delete images in edit mode" + - "User can add images to existing entries" + artifacts: + - path: "src/lib/components/EntryCard.svelte" + provides: "Entry card with image thumbnail and gallery" + contains: "ImageGallery" + - path: "src/lib/components/ImageGallery.svelte" + provides: "Horizontal scrolling image gallery" + min_lines: 40 + - path: "src/lib/components/ImageLightbox.svelte" + provides: "Fullscreen image viewer" + min_lines: 30 + - path: "src/routes/+page.server.ts" + provides: "deleteImage form action" + contains: "deleteImage:" + key_links: + - from: "src/lib/components/EntryCard.svelte" + to: "src/lib/components/ImageGallery.svelte" + via: "component import" + pattern: "import.*ImageGallery" + - from: "src/lib/components/ImageGallery.svelte" + to: "src/lib/components/ImageLightbox.svelte" + via: "lightbox trigger" + pattern: "ImageLightbox" +--- + + +Integrate image display and management into the entry card UI with gallery, lightbox, and delete functionality. + +Purpose: Completes the image feature by letting users see their images within entries, view them fullscreen, and manage (add/delete) them. + +Output: +- EntryCard shows thumbnail indicator when entry has images +- Expanded entry shows horizontal gallery with all images +- Clicking image opens fullscreen lightbox +- Edit mode shows delete buttons on images +- Upload controls available in expanded entry + + + +@/home/tho/.claude/get-shit-done/workflows/execute-plan.md +@/home/tho/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/phases/03-images/03-RESEARCH.md +@.planning/phases/03-images/03-02-SUMMARY.md +@.planning/phases/03-images/03-03-SUMMARY.md +@src/lib/components/EntryCard.svelte +@src/routes/+page.server.ts +@src/routes/+page.svelte + + + + + + Task 1: ImageGallery and ImageLightbox components + + src/lib/components/ImageGallery.svelte + src/lib/components/ImageLightbox.svelte + + +1. Install svelte-lightbox: `npm install svelte-lightbox` + +2. Create ImageGallery.svelte: + +Props: +- images: Array<{ id: string, ext: string, filename: string }> +- entryId: string +- editMode?: boolean (default false) +- onDelete?: (imageId: string) => void + +State: +- lightboxOpen: boolean ($state) +- lightboxIndex: number ($state) + +Template: +```svelte + + {#each images as image, i} + + { lightboxIndex = i; lightboxOpen = true; }} + class="block" + > + + + + {#if editMode} + onDelete?.(image.id)} + class="absolute -top-2 -right-2 w-6 h-6 bg-red-500 text-white rounded-full flex items-center justify-center" + aria-label="Delete image" + > + X + + {/if} + + {/each} + + +{#if lightboxOpen} + lightboxOpen = false} + /> +{/if} +``` + +3. Create ImageLightbox.svelte: + +Props: +- images: Array<{ id: string, ext: string, filename: string }> +- startIndex: number +- onClose: () => void + +Use svelte-lightbox or create simple custom lightbox: +- Full screen overlay (fixed inset-0 bg-black/90 z-50) +- Close button in corner +- Image centered, max-width/max-height to fit +- Left/right navigation if multiple images +- Keyboard support: Escape closes, Arrow keys navigate +- Click outside image closes +- Show image from /api/images/{id} (full size, not thumbnail) + +Keep it simple - a modal with the full-size image. Touch gestures (swipe, pinch-zoom) are nice-to-have, not required. + + +Run `npm run check` - TypeScript compiles. +Files exist: `ls src/lib/components/Image*.svelte` + + +ImageGallery renders horizontal scrolling thumbnails. +Edit mode shows delete buttons. +Clicking image opens ImageLightbox. +ImageLightbox shows full-size image with close/navigation. + + + + + Task 2: Integrate images into EntryCard + + src/lib/components/EntryCard.svelte + src/routes/+page.svelte + + +1. Update +page.svelte to pass images to EntryCard: + - The load function already returns entries with images attached (from 03-02) + - Ensure EntryCard receives entry.images + +2. Update EntryCard.svelte: + +Add to Props interface: +- entry should include images: Array<{ id: string, ext: string, filename: string }> + +Add imports: +- import ImageGallery from './ImageGallery.svelte' +- import ImageUpload from './ImageUpload.svelte' +- import CameraCapture from './CameraCapture.svelte' + +Add state: +- showCamera: boolean ($state) - controls camera modal +- editMode: boolean ($state) - toggle for showing delete buttons + +In collapsed view (next to title/content): +- If entry.images?.length > 0, show small image indicator +- Could be first thumbnail tiny (24x24) or just an image count badge + +In expanded view: +- Add ImageGallery component showing all images +- Add "Edit Images" toggle button to enable delete mode +- Add upload section with: + - ImageUpload component (drag-drop/button) + - Camera button that opens CameraCapture modal + +```svelte +{#if expanded} + + + + + {#if entry.images?.length > 0} + + + Images + editMode = !editMode} + class="text-sm text-blue-600" + > + {editMode ? 'Done' : 'Edit'} + + + + + {/if} + + + + Add Image + + + showCamera = true} + class="px-4 py-2 border rounded-lg flex items-center gap-2" + > + Camera + + + + + + +{/if} + +{#if showCamera} + showCamera = false} + onCapture={handleUploadComplete} + /> +{/if} +``` + +Add handler functions: +- handleDeleteImage(imageId: string): calls delete form action +- handleUploadComplete(): calls invalidateAll() to refresh + + +Run `npm run check` - TypeScript compiles. +Run `npm run dev` - verify EntryCard shows images when expanded. + + +EntryCard shows image indicator in collapsed view. +Expanded view shows ImageGallery with all images. +Edit mode reveals delete buttons on images. +Upload and camera buttons available for adding images. + + + + + Task 3: Delete image form action + + src/routes/+page.server.ts + + +Add deleteImage action to +page.server.ts: + +1. Import deleteImage from storage: + `import { deleteImage as deleteImageFile } from '$lib/server/images/storage'` + +2. Add action: +```typescript +deleteImage: async ({ request }) => { + const formData = await request.formData(); + const imageId = formData.get('imageId')?.toString(); + + if (!imageId) { + return fail(400, { error: 'Image ID required' }); + } + + // Get image to find extension for file deletion + const image = imageRepository.getById(imageId); + if (!image) { + return fail(404, { error: 'Image not found' }); + } + + // Delete files from filesystem + try { + await deleteImageFile(imageId, image.ext); + } catch (err) { + console.error('Failed to delete image files:', err); + // Continue to delete from database even if files missing + } + + // Delete from database + imageRepository.delete(imageId); + + return { success: true }; +} +``` + +3. Wire up the handler in EntryCard: +```typescript +async function handleDeleteImage(imageId: string) { + const formData = new FormData(); + formData.append('imageId', imageId); + + await fetch('?/deleteImage', { + method: 'POST', + body: formData + }); + + await invalidateAll(); +} +``` + + +Run `npm run check` - TypeScript compiles. +Verify action exists: `grep -n "deleteImage:" src/routes/+page.server.ts` + + +deleteImage action removes image from database and filesystem. +EntryCard can delete images via the gallery edit mode. + + + + + +Complete image attachment feature: +- File upload via drag-drop and button +- Camera capture on mobile +- Thumbnail display in entry cards +- Horizontal gallery in expanded view +- Fullscreen lightbox viewer +- Delete images in edit mode + + +1. Start dev server: `npm run dev` +2. Open http://localhost:5173 in browser + +Test file upload (desktop): +- Create a new entry +- Expand the entry +- Drag an image onto the upload zone OR click to select file +- Verify image appears in gallery after upload +- Verify thumbnail shows in collapsed entry view + +Test camera capture (use mobile device or browser with webcam): +- Expand an entry +- Click "Camera" button +- Grant camera permission when prompted +- Verify live camera preview appears +- Take a photo, verify preview shows +- Try "Retake" - should return to live camera +- Click "Use Photo" - verify image uploads and appears in gallery + +Test image viewing: +- Click on a thumbnail in the gallery +- Verify lightbox opens with full-size image +- Close lightbox (click X or outside image) + +Test image deletion: +- Expand entry with images +- Click "Edit" button above gallery +- Verify delete buttons appear on images +- Delete an image +- Verify image removed from gallery +- Verify files deleted: check data/uploads/ directory + +Test error cases: +- Try camera on desktop without webcam - should show friendly error +- Try uploading non-image file - should show error + + Type "approved" if all tests pass, or describe issues found + + + + + +1. `npm run check` passes +2. `npm run dev` - server starts +3. All components exist: + - src/lib/components/ImageGallery.svelte + - src/lib/components/ImageLightbox.svelte +4. EntryCard imports and uses image components +5. Human verification of complete image flow + + + +- Entry cards show image indicator when images attached +- Expanded entry shows horizontal scrolling gallery +- Clicking image opens fullscreen lightbox +- Edit mode allows deleting images +- Upload and camera available for adding images to existing entries +- Human verified: complete image flow works on desktop and mobile + + + +After completion, create `.planning/phases/03-images/03-04-SUMMARY.md` +
{error}