From 4fe198eb0aa2e96def3758904311d0c4de2b96da Mon Sep 17 00:00:00 2001 From: Thomas Richter Date: Sat, 31 Jan 2026 12:49:53 +0100 Subject: [PATCH] docs(04): create phase plan Phase 04: Tags & Organization - 3 plan(s) in 2 wave(s) - Wave 1: 04-01 (tags schema), 04-02 (pin/due date) - parallel - Wave 2: 04-03 (tag UI) - depends on 04-01 - Ready for execution Co-Authored-By: Claude Opus 4.5 --- .planning/ROADMAP.md | 8 +- .planning/phases/04-tags/04-01-PLAN.md | 183 +++++++++++++ .planning/phases/04-tags/04-02-PLAN.md | 284 ++++++++++++++++++++ .planning/phases/04-tags/04-03-PLAN.md | 347 +++++++++++++++++++++++++ 4 files changed, 818 insertions(+), 4 deletions(-) create mode 100644 .planning/phases/04-tags/04-01-PLAN.md create mode 100644 .planning/phases/04-tags/04-02-PLAN.md create mode 100644 .planning/phases/04-tags/04-03-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index da84988..f87364c 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -88,12 +88,12 @@ Plans: 5. User can pin/favorite an entry for quick access 6. User can set a due date on a task 7. Pinned entries appear in a dedicated section at top of list -**Plans**: TBD +**Plans**: 3 plans Plans: -- [ ] 04-01: TBD -- [ ] 04-02: TBD -- [ ] 04-03: TBD +- [ ] 04-01-PLAN.md — Tags schema with case-insensitive index and tagRepository +- [ ] 04-02-PLAN.md — Pin/favorite and due date UI (uses existing schema columns) +- [ ] 04-03-PLAN.md — Tag input component with Svelecte autocomplete ### Phase 5: Search **Goal**: Users can find entries through search and filtering diff --git a/.planning/phases/04-tags/04-01-PLAN.md b/.planning/phases/04-tags/04-01-PLAN.md new file mode 100644 index 0000000..8d24e6a --- /dev/null +++ b/.planning/phases/04-tags/04-01-PLAN.md @@ -0,0 +1,183 @@ +--- +phase: 04-tags +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - src/lib/server/db/schema.ts + - src/lib/server/db/repository.ts +autonomous: true + +must_haves: + truths: + - "Tags table exists with case-insensitive unique constraint" + - "Entry-tag associations stored in junction table" + - "Finding tag by name is case-insensitive" + - "Updating entry tags replaces entire tag set atomically" + artifacts: + - path: "src/lib/server/db/schema.ts" + provides: "tags and entryTags table definitions with relations" + contains: "tags = sqliteTable" + - path: "src/lib/server/db/repository.ts" + provides: "tagRepository singleton with CRUD operations" + contains: "tagRepository" + key_links: + - from: "entryTags.entryId" + to: "entries.id" + via: "foreign key with cascade delete" + pattern: "onDelete.*cascade" + - from: "entryTags.tagId" + to: "tags.id" + via: "foreign key with cascade delete" + pattern: "onDelete.*cascade" +--- + + +Add database schema and repository layer for many-to-many tag relationships between entries and tags. + +Purpose: Foundation for TAG-01, TAG-02, TAG-03, TAG-04 requirements - enables tag storage and querying +Output: tags table, entry_tags junction table, tagRepository with findOrCreate/getAll/updateEntryTags methods + + + +@/home/tho/.claude/get-shit-done/workflows/execute-plan.md +@/home/tho/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/04-tags/04-RESEARCH.md + +@src/lib/server/db/schema.ts +@src/lib/server/db/repository.ts + + + + + + Task 1: Add tags schema with case-insensitive unique index + src/lib/server/db/schema.ts + +Add tags table and entry_tags junction table to schema.ts: + +1. Create `lower()` SQL helper function for case-insensitive indexing: +```typescript +import { sql, SQL } from 'drizzle-orm'; +import { AnySQLiteColumn } from 'drizzle-orm/sqlite-core'; + +export function lower(column: AnySQLiteColumn): SQL { + return sql`lower(${column})`; +} +``` + +2. Add tags table: +```typescript +export const tags = sqliteTable('tags', { + id: text('id').primaryKey(), + name: text('name').notNull(), + createdAt: text('created_at').notNull().$defaultFn(() => new Date().toISOString()), +}, (table) => [ + uniqueIndex('tagNameUniqueIndex').on(lower(table.name)), +]); +``` + +3. Add entry_tags junction table with composite primary key: +```typescript +export const entryTags = sqliteTable('entry_tags', { + entryId: text('entry_id').notNull().references(() => entries.id, { onDelete: 'cascade' }), + tagId: text('tag_id').notNull().references(() => tags.id, { onDelete: 'cascade' }), +}, (t) => [ + primaryKey({ columns: [t.entryId, t.tagId] }), +]); +``` + +4. Add type exports: +```typescript +export type Tag = typeof tags.$inferSelect; +export type NewTag = typeof tags.$inferInsert; +export type EntryTag = typeof entryTags.$inferSelect; +``` + +Import requirements: Add `uniqueIndex`, `primaryKey` to drizzle-orm/sqlite-core imports. + + Run `npm run check` - TypeScript compiles without errors + Schema file contains tags table with case-insensitive unique index and entry_tags junction table with cascade deletes + + + + Task 2: Create tagRepository with tag operations + src/lib/server/db/repository.ts + +Add TagRepository interface and implementation after ImageRepository: + +1. Import new schema tables: +```typescript +import { entries, images, tags, entryTags, lower, ... } from './schema'; +``` + +2. Define TagRepository interface: +```typescript +export interface TagRepository { + findOrCreate(name: string): Tag; + getAll(): Tag[]; + getById(id: string): Tag | undefined; + getByEntryId(entryId: string): Tag[]; + updateEntryTags(entryId: string, tagNames: string[]): void; +} +``` + +3. Implement SQLiteTagRepository: +- `findOrCreate(name)`: + - Normalize: `name.trim()` + - Query with `sql\`lower(${tags.name}) = lower(${normalizedName})\`` + - If exists, return it + - If not, insert with nanoid() and return + +- `getAll()`: + - Select all from tags ordered by name ASC + +- `getById(id)`: + - Simple select where eq(tags.id, id) + +- `getByEntryId(entryId)`: + - Join entry_tags with tags where entryId matches + - Return Tag[] (the tag objects, not junction records) + +- `updateEntryTags(entryId, tagNames)`: + - Delete all from entry_tags where entryId matches + - For each tagName: findOrCreate tag, then insert into entry_tags + +4. Export singleton: +```typescript +export const tagRepository: TagRepository = new SQLiteTagRepository(); +``` + +Import `sql` from 'drizzle-orm' for case-insensitive queries. + + Run `npm run check` - TypeScript compiles. Manually test in dev console or add temporary test route if needed. + tagRepository exported with findOrCreate, getAll, getById, getByEntryId, and updateEntryTags methods working correctly + + + + + +1. `npm run check` passes +2. Schema includes tags table with unique case-insensitive index on name +3. Schema includes entry_tags with composite PK and cascade deletes +4. tagRepository singleton exported with all five methods +5. Run `npm run db:push` to sync schema to database + + + +- Tags table created with case-insensitive unique constraint +- Entry-tag junction table links entries to tags with cascade delete +- tagRepository.findOrCreate returns same tag for "work", "Work", "WORK" +- tagRepository.updateEntryTags atomically replaces all tags for an entry + + + +After completion, create `.planning/phases/04-tags/04-01-SUMMARY.md` + diff --git a/.planning/phases/04-tags/04-02-PLAN.md b/.planning/phases/04-tags/04-02-PLAN.md new file mode 100644 index 0000000..99cf943 --- /dev/null +++ b/.planning/phases/04-tags/04-02-PLAN.md @@ -0,0 +1,284 @@ +--- +phase: 04-tags +plan: 02 +type: execute +wave: 1 +depends_on: [] +files_modified: + - src/routes/+page.server.ts + - src/routes/+page.svelte + - src/lib/components/EntryCard.svelte + - src/lib/components/EntryList.svelte +autonomous: true + +must_haves: + truths: + - "User can click a pin button to toggle pinned state" + - "User can set a due date on any entry" + - "Pinned entries appear in a dedicated section at top of list" + - "Due date shows on entry card when set" + artifacts: + - path: "src/routes/+page.server.ts" + provides: "togglePin and updateDueDate form actions" + contains: "togglePin" + - path: "src/lib/components/EntryCard.svelte" + provides: "Pin button and due date picker in expanded view" + contains: "togglePin" + - path: "src/lib/components/EntryList.svelte" + provides: "Pinned section above regular entries" + contains: "pinned" + key_links: + - from: "EntryCard pin button" + to: "?/togglePin action" + via: "form submit or fetch" + pattern: "togglePin" + - from: "EntryCard due date input" + to: "?/updateDueDate action" + via: "form submit or fetch" + pattern: "updateDueDate" +--- + + +Add pin/favorite and due date features using existing schema columns (pinned, dueDate already in entries table). + +Purpose: Enables ORG-01, ORG-02, ORG-03 requirements - quick access to important entries and task scheduling +Output: Toggle pin button, due date picker, pinned entries section at top of list + + + +@/home/tho/.claude/get-shit-done/workflows/execute-plan.md +@/home/tho/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md + +@src/routes/+page.server.ts +@src/routes/+page.svelte +@src/lib/components/EntryCard.svelte +@src/lib/components/EntryList.svelte +@src/lib/server/db/schema.ts + + + + + + Task 1: Add togglePin and updateDueDate form actions + src/routes/+page.server.ts + +Add two new form actions to the existing actions object: + +1. `togglePin` action: +```typescript +togglePin: async ({ request }) => { + const formData = await request.formData(); + const id = formData.get('id')?.toString(); + + if (!id) { + return fail(400, { error: 'Entry ID is required' }); + } + + const existing = entryRepository.getById(id); + if (!existing) { + return fail(404, { error: 'Entry not found' }); + } + + // Toggle pinned state + entryRepository.update(id, { pinned: !existing.pinned }); + + return { success: true }; +} +``` + +2. `updateDueDate` action: +```typescript +updateDueDate: async ({ request }) => { + const formData = await request.formData(); + const id = formData.get('id')?.toString(); + const dueDate = formData.get('dueDate')?.toString() || null; + + if (!id) { + return fail(400, { error: 'Entry ID is required' }); + } + + const existing = entryRepository.getById(id); + if (!existing) { + return fail(404, { error: 'Entry not found' }); + } + + // Update due date (empty string becomes null) + entryRepository.update(id, { dueDate: dueDate || null }); + + return { success: true }; +} +``` + +Note: The dueDate is stored as ISO date string (YYYY-MM-DD format from HTML5 date input). + + Run `npm run check` - TypeScript compiles. Start dev server and test actions via form submission. + togglePin and updateDueDate form actions work correctly, entries.pinned toggles and entries.dueDate updates + + + + Task 2: Add pin button, due date picker, and pinned section UI + src/lib/components/EntryCard.svelte, src/lib/components/EntryList.svelte + +**EntryCard.svelte changes:** + +1. Add state for due date editing: +```typescript +let editDueDate = $state(entry.dueDate || ''); +``` + +2. Sync dueDate with entry on collapse (add to existing $effect): +```typescript +$effect(() => { + if (!expanded) { + // ... existing syncs + editDueDate = entry.dueDate || ''; + } +}); +``` + +3. Add async function for pin toggle (similar to debouncedSave pattern): +```typescript +async function handleTogglePin() { + const formData = new FormData(); + formData.append('id', entry.id); + + await fetch('?/togglePin', { + method: 'POST', + body: formData + }); + + await invalidateAll(); +} +``` + +4. Add async function for due date change: +```typescript +async function handleDueDateChange(e: Event) { + const input = e.target as HTMLInputElement; + editDueDate = input.value; + + const formData = new FormData(); + formData.append('id', entry.id); + formData.append('dueDate', input.value); + + await fetch('?/updateDueDate', { + method: 'POST', + body: formData + }); + + await invalidateAll(); +} +``` + +5. In collapsed view header (after the expand arrow), add pin indicator: +```svelte +{#if entry.pinned} + + + +{/if} +``` + +6. In collapsed view, show due date if set (near type badge): +```svelte +{#if entry.dueDate} + {entry.dueDate} +{/if} +``` + +7. In expanded view, add pin button and due date picker (in the flex row with type and delete): +```svelte +
+ + +
+ + +
+
+``` + +**EntryList.svelte changes:** + +1. Separate entries into pinned and unpinned: +```typescript +let pinnedEntries = $derived(entries.filter(e => e.pinned)); +let unpinnedEntries = $derived(entries.filter(e => !e.pinned)); +``` + +2. Render pinned section first if there are any: +```svelte +{#if pinnedEntries.length > 0} +
+

Pinned

+
+ {#each pinnedEntries as entry (entry.id)} + + {/each} +
+
+{/if} + +{#if unpinnedEntries.length > 0} +
+ {#each unpinnedEntries as entry (entry.id)} + + {/each} +
+{/if} + +{#if entries.length === 0} + +{/if} +``` +
+ Run `npm run dev`, test: +1. Click pin button in expanded view - entry moves to pinned section +2. Click again - entry moves back to regular list +3. Set due date - shows on collapsed card +4. Clear due date - disappears from card + Pin toggle works, due date picker works, pinned entries appear in dedicated section at top +
+ +
+ + +1. `npm run check` passes +2. Pin button toggles entry.pinned state +3. Due date picker updates entry.dueDate +4. Pinned section appears at top of list with pinned entries +5. Pin icon visible on collapsed pinned entries +6. Due date visible on collapsed entries when set + + + +- User can toggle pin status with visual feedback (ORG-01) +- User can set/clear due date using native date picker (ORG-02) +- Pinned entries appear in labeled section at top of list (ORG-03) +- Due date displays on entry card in collapsed view + + + +After completion, create `.planning/phases/04-tags/04-02-SUMMARY.md` + diff --git a/.planning/phases/04-tags/04-03-PLAN.md b/.planning/phases/04-tags/04-03-PLAN.md new file mode 100644 index 0000000..3ef6a92 --- /dev/null +++ b/.planning/phases/04-tags/04-03-PLAN.md @@ -0,0 +1,347 @@ +--- +phase: 04-tags +plan: 03 +type: execute +wave: 2 +depends_on: ["04-01"] +files_modified: + - package.json + - src/lib/components/TagInput.svelte + - src/lib/components/EntryCard.svelte + - src/routes/+page.server.ts + - src/routes/+page.svelte +autonomous: true + +must_haves: + truths: + - "User can add multiple tags to an entry" + - "User can remove tags from an entry" + - "Tag input shows autocomplete suggestions from existing tags" + - "Tags are case-insensitive (work = Work = WORK)" + - "New tags can be created inline during entry editing" + artifacts: + - path: "src/lib/components/TagInput.svelte" + provides: "Multi-select tag input with autocomplete and creation" + contains: "Svelecte" + - path: "src/lib/components/EntryCard.svelte" + provides: "Tag display in collapsed view, TagInput in expanded view" + contains: "TagInput" + - path: "src/routes/+page.server.ts" + provides: "updateTags form action and tags in load data" + contains: "updateTags" + key_links: + - from: "TagInput component" + to: "availableTags prop" + via: "passed from page data" + pattern: "availableTags" + - from: "EntryCard save" + to: "?/updateTags action" + via: "fetch call" + pattern: "updateTags" + - from: "+page.server.ts load" + to: "tagRepository.getByEntryId" + via: "attach tags to entries" + pattern: "getByEntryId" +--- + + +Add tag input UI with Svelecte for autocomplete and inline tag creation, integrated into EntryCard. + +Purpose: Enables TAG-01, TAG-02, TAG-03, TAG-04 requirements - full tag management with user-friendly input +Output: TagInput component with Svelecte, tag display on entries, updateTags form action + + + +@/home/tho/.claude/get-shit-done/workflows/execute-plan.md +@/home/tho/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/04-tags/04-RESEARCH.md +@.planning/phases/04-tags/04-01-SUMMARY.md + +@src/lib/server/db/schema.ts +@src/lib/server/db/repository.ts +@src/lib/components/EntryCard.svelte +@src/routes/+page.server.ts +@src/routes/+page.svelte + + + + + + Task 1: Install Svelecte and update load/actions for tags + package.json, src/routes/+page.server.ts + +**Install Svelecte:** +```bash +npm install svelecte +``` + +**Update +page.server.ts:** + +1. Import tagRepository: +```typescript +import { entryRepository, imageRepository, tagRepository } from '$lib/server/db/repository'; +``` + +2. Update load function to include tags on entries and all available tags: +```typescript +export const load: PageServerLoad = async ({ url }) => { + const showCompleted = url.searchParams.get('showCompleted') === 'true'; + const entries = entryRepository.getOrdered({ showCompleted }); + + // Attach images AND tags to each entry + const entriesWithData = entries.map((entry) => ({ + ...entry, + images: imageRepository.getByEntryId(entry.id), + tags: tagRepository.getByEntryId(entry.id) + })); + + // Get all tags for autocomplete + const allTags = tagRepository.getAll(); + + return { + entries: entriesWithData, + allTags, + showCompleted + }; +}; +``` + +3. Add updateTags action: +```typescript +updateTags: async ({ request }) => { + const formData = await request.formData(); + const id = formData.get('id')?.toString(); + const tagsJson = formData.get('tags')?.toString() || '[]'; + + if (!id) { + return fail(400, { error: 'Entry ID is required' }); + } + + const existing = entryRepository.getById(id); + if (!existing) { + return fail(404, { error: 'Entry not found' }); + } + + try { + const tagNames = JSON.parse(tagsJson) as string[]; + tagRepository.updateEntryTags(id, tagNames); + return { success: true }; + } catch (e) { + return fail(400, { error: 'Invalid tags format' }); + } +} +``` + + Run `npm run check` - TypeScript compiles. Run `npm run dev` and check console for load data including allTags. + Svelecte installed, load returns entries with tags and allTags array, updateTags action processes tag changes + + + + Task 2: Create TagInput component and integrate into EntryCard + src/lib/components/TagInput.svelte, src/lib/components/EntryCard.svelte, src/routes/+page.svelte + +**Create TagInput.svelte:** +```svelte + + + + + +``` + +**Update EntryCard.svelte:** + +1. Import TagInput and Tag type: +```typescript +import TagInput from './TagInput.svelte'; +import type { Tag } from '$lib/server/db/schema'; +``` + +2. Add availableTags prop to Props interface: +```typescript +interface Props { + entry: EntryWithImages & { tags: Tag[] }; + availableTags: Tag[]; +} + +let { entry, availableTags }: Props = $props(); +``` + +3. Add state for tag editing: +```typescript +let editTags = $state(entry.tags || []); +``` + +4. Sync tags with entry on collapse: +```typescript +$effect(() => { + if (!expanded) { + // ... existing syncs + editTags = entry.tags || []; + } +}); +``` + +5. Add function to handle tag changes: +```typescript +async function handleTagsChange(newTags: Tag[]) { + editTags = newTags; + + // Save immediately (no debounce for tags) + const formData = new FormData(); + formData.append('id', entry.id); + formData.append('tags', JSON.stringify(newTags.map(t => t.name))); + + await fetch('?/updateTags', { + method: 'POST', + body: formData + }); + + await invalidateAll(); +} +``` + +6. Display tags in collapsed view (after type badge): +```svelte +{#if entry.tags?.length > 0} +
+ {#each entry.tags.slice(0, 3) as tag} + {tag.name} + {/each} + {#if entry.tags.length > 3} + +{entry.tags.length - 3} + {/if} +
+{/if} +``` + +7. Add TagInput in expanded view (after type selector, before delete button): +```svelte +
+ + +
+``` + +**Update +page.svelte:** + +Pass allTags to EntryList, then from EntryList to EntryCard. Or simpler - pass directly: + +1. Update EntryList.svelte to accept and pass availableTags: +```typescript +interface Props { + entries: EntryWithImages[]; + availableTags: Tag[]; +} +let { entries, availableTags }: Props = $props(); +``` + +And pass to each EntryCard: +```svelte + +``` + +2. Update +page.svelte to pass allTags: +```svelte + +``` + +Import Tag type if needed for type checking. +
+ Run `npm run dev`, test: +1. Expand an entry - TagInput appears +2. Type a tag name - autocomplete shows matching existing tags +3. Select existing tag - appears as chip +4. Type new tag and press Enter - tag created +5. Click X on tag chip - tag removed +6. Collapse and expand - tags persist + TagInput component with Svelecte works, shows autocomplete, allows creation, displays tags on collapsed entries +
+ +
+ + +1. `npm run check` passes +2. Svelecte package installed and loads correctly +3. Tag autocomplete shows all existing tags +4. User can add tags (existing or new) to entries +5. User can remove tags by clicking X +6. Tags display on collapsed entry cards +7. Case-insensitive: typing "Work" matches existing "work" tag + + + +- User can add multiple tags to entry via Svelecte input (TAG-01) +- User can remove tags by clicking X on chips (TAG-02) +- Autocomplete shows matching existing tags as user types (TAG-03) +- Existing tags matched case-insensitively (TAG-04) +- Tags visible on collapsed entry cards (max 3 with +N indicator) + + + +After completion, create `.planning/phases/04-tags/04-03-SUMMARY.md` +