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>
This commit is contained in:
414
.planning/phases/03-images/03-04-PLAN.md
Normal file
414
.planning/phases/03-images/03-04-PLAN.md
Normal file
@@ -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"
|
||||
---
|
||||
|
||||
<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>
|
||||
Reference in New Issue
Block a user