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>
415 lines
12 KiB
Markdown
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>
|