Files
taskplaner/.planning/phases/03-images/03-04-PLAN.md
Thomas Richter 04c2742e73 docs(03): create phase plan
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>
2026-01-29 15:15:30 +01:00

415 lines
12 KiB
Markdown

---
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"
---
<objective>
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
</objective>
<execution_context>
@/home/tho/.claude/get-shit-done/workflows/execute-plan.md
@/home/tho/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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
</context>
<tasks>
<task type="auto">
<name>Task 1: ImageGallery and ImageLightbox components</name>
<files>
src/lib/components/ImageGallery.svelte
src/lib/components/ImageLightbox.svelte
</files>
<action>
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
<div class="flex gap-2 overflow-x-auto py-2 -mx-2 px-2">
{#each images as image, i}
<div class="relative flex-shrink-0">
<button
type="button"
onclick={() => { lightboxIndex = i; lightboxOpen = true; }}
class="block"
>
<img
src="/api/images/{image.id}/thumbnail"
alt={image.filename}
class="w-20 h-20 object-cover rounded-lg"
/>
</button>
{#if editMode}
<button
type="button"
onclick={() => 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
</button>
{/if}
</div>
{/each}
</div>
{#if lightboxOpen}
<ImageLightbox
images={images}
startIndex={lightboxIndex}
onClose={() => 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.
</action>
<verify>
Run `npm run check` - TypeScript compiles.
Files exist: `ls src/lib/components/Image*.svelte`
</verify>
<done>
ImageGallery renders horizontal scrolling thumbnails.
Edit mode shows delete buttons.
Clicking image opens ImageLightbox.
ImageLightbox shows full-size image with close/navigation.
</done>
</task>
<task type="auto">
<name>Task 2: Integrate images into EntryCard</name>
<files>
src/lib/components/EntryCard.svelte
src/routes/+page.svelte
</files>
<action>
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}
<div class="mt-4 pl-9 space-y-3">
<!-- Existing edit fields... -->
<!-- Images section -->
{#if entry.images?.length > 0}
<div>
<div class="flex items-center justify-between mb-2">
<label class="text-sm font-medium text-gray-700">Images</label>
<button
type="button"
onclick={() => editMode = !editMode}
class="text-sm text-blue-600"
>
{editMode ? 'Done' : 'Edit'}
</button>
</div>
<ImageGallery
images={entry.images}
entryId={entry.id}
editMode={editMode}
onDelete={handleDeleteImage}
/>
</div>
{/if}
<!-- Add image section -->
<div>
<label class="text-sm font-medium text-gray-700 mb-2 block">Add Image</label>
<div class="flex gap-2">
<ImageUpload entryId={entry.id} onUploadComplete={handleUploadComplete} />
<button
type="button"
onclick={() => showCamera = true}
class="px-4 py-2 border rounded-lg flex items-center gap-2"
>
Camera
</button>
</div>
</div>
<!-- Existing type selector and delete button... -->
</div>
{/if}
{#if showCamera}
<CameraCapture
entryId={entry.id}
onClose={() => showCamera = false}
onCapture={handleUploadComplete}
/>
{/if}
```
Add handler functions:
- handleDeleteImage(imageId: string): calls delete form action
- handleUploadComplete(): calls invalidateAll() to refresh
</action>
<verify>
Run `npm run check` - TypeScript compiles.
Run `npm run dev` - verify EntryCard shows images when expanded.
</verify>
<done>
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.
</done>
</task>
<task type="auto">
<name>Task 3: Delete image form action</name>
<files>
src/routes/+page.server.ts
</files>
<action>
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();
}
```
</action>
<verify>
Run `npm run check` - TypeScript compiles.
Verify action exists: `grep -n "deleteImage:" src/routes/+page.server.ts`
</verify>
<done>
deleteImage action removes image from database and filesystem.
EntryCard can delete images via the gallery edit mode.
</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>
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
</what-built>
<how-to-verify>
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
</how-to-verify>
<resume-signal>Type "approved" if all tests pass, or describe issues found</resume-signal>
</task>
</tasks>
<verification>
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
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
After completion, create `.planning/phases/03-images/03-04-SUMMARY.md`
</output>