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:
284
.planning/phases/03-images/03-02-PLAN.md
Normal file
284
.planning/phases/03-images/03-02-PLAN.md
Normal file
@@ -0,0 +1,284 @@
|
||||
---
|
||||
phase: 03-images
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["03-01"]
|
||||
files_modified:
|
||||
- src/routes/+page.server.ts
|
||||
- src/lib/components/ImageUpload.svelte
|
||||
- src/lib/components/QuickCapture.svelte
|
||||
autonomous: true
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "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"
|
||||
artifacts:
|
||||
- path: "src/routes/+page.server.ts"
|
||||
provides: "uploadImage form action"
|
||||
contains: "uploadImage:"
|
||||
- path: "src/lib/components/ImageUpload.svelte"
|
||||
provides: "Upload button + drag-drop zone"
|
||||
min_lines: 50
|
||||
key_links:
|
||||
- from: "src/lib/components/ImageUpload.svelte"
|
||||
to: "src/routes/+page.server.ts"
|
||||
via: "fetch to ?/uploadImage"
|
||||
pattern: "fetch.*\\?/uploadImage"
|
||||
- from: "src/routes/+page.server.ts"
|
||||
to: "src/lib/server/images/storage.ts"
|
||||
via: "saveOriginal import"
|
||||
pattern: "import.*storage"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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
|
||||
</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-01-SUMMARY.md
|
||||
@src/routes/+page.server.ts
|
||||
@src/lib/components/QuickCapture.svelte
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Upload form action with thumbnail generation</name>
|
||||
<files>
|
||||
src/routes/+page.server.ts
|
||||
</files>
|
||||
<action>
|
||||
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:
|
||||
```typescript
|
||||
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)
|
||||
</action>
|
||||
<verify>
|
||||
Run `npm run check` - TypeScript compiles.
|
||||
Verify action exists: `grep -n "uploadImage:" src/routes/+page.server.ts`
|
||||
</verify>
|
||||
<done>
|
||||
uploadImage action handles file upload, generates thumbnail, saves to database.
|
||||
load function returns entries with their images attached.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: ImageUpload component with drag-drop</name>
|
||||
<files>
|
||||
src/lib/components/ImageUpload.svelte
|
||||
</files>
|
||||
<action>
|
||||
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])
|
||||
</action>
|
||||
<verify>
|
||||
Run `npm run check` - TypeScript compiles.
|
||||
File exists with core functions: `grep -c "handleDrop\|uploadFile" src/lib/components/ImageUpload.svelte`
|
||||
</verify>
|
||||
<done>
|
||||
ImageUpload component renders drag-drop zone with button fallback.
|
||||
Shows optimistic preview during upload.
|
||||
Calls form action and refreshes list on completion.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Integrate upload into QuickCapture</name>
|
||||
<files>
|
||||
src/lib/components/QuickCapture.svelte
|
||||
</files>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
Run `npm run check` - TypeScript compiles.
|
||||
Run `npm run dev` and verify QuickCapture shows image button.
|
||||
</verify>
|
||||
<done>
|
||||
QuickCapture has image attachment button.
|
||||
User can add image before submitting new entry.
|
||||
Image uploads after entry creation with the new entry ID.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
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;"`
|
||||
</verification>
|
||||
|
||||
<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>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/03-images/03-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user