Files
taskplaner/.planning/phases/03-images/03-02-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

8.8 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 02 execute 2
03-01
src/routes/+page.server.ts
src/lib/components/ImageUpload.svelte
src/lib/components/QuickCapture.svelte
true
truths artifacts key_links
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
path provides contains
src/routes/+page.server.ts uploadImage form action uploadImage:
path provides min_lines
src/lib/components/ImageUpload.svelte Upload button + drag-drop zone 50
from to via pattern
src/lib/components/ImageUpload.svelte src/routes/+page.server.ts fetch to ?/uploadImage fetch.*?/uploadImage
from to via pattern
src/routes/+page.server.ts src/lib/server/images/storage.ts saveOriginal import 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

<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-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:

    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;"`

<success_criteria>

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