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

12 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 04 execute 3
03-02
03-03
src/lib/components/EntryCard.svelte
src/lib/components/ImageGallery.svelte
src/lib/components/ImageLightbox.svelte
src/routes/+page.server.ts
src/routes/+page.svelte
false
truths artifacts key_links
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
path provides contains
src/lib/components/EntryCard.svelte Entry card with image thumbnail and gallery ImageGallery
path provides min_lines
src/lib/components/ImageGallery.svelte Horizontal scrolling image gallery 40
path provides min_lines
src/lib/components/ImageLightbox.svelte Fullscreen image viewer 30
path provides contains
src/routes/+page.server.ts deleteImage form action deleteImage:
from to via pattern
src/lib/components/EntryCard.svelte src/lib/components/ImageGallery.svelte component import import.*ImageGallery
from to via pattern
src/lib/components/ImageGallery.svelte src/lib/components/ImageLightbox.svelte lightbox trigger 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

<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-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`
  1. 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:

<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}
  1. 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
  1. 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
{#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 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:

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 };
}
  1. Wire up the handler in EntryCard:
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

<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>
After completion, create `.planning/phases/03-images/03-04-SUMMARY.md`