279 lines
13 KiB
Markdown
279 lines
13 KiB
Markdown
---
|
|
phase: 06-category-icons
|
|
plan: 01
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- src/db/schema.ts
|
|
- src/shared/schemas.ts
|
|
- src/shared/types.ts
|
|
- src/db/seed.ts
|
|
- src/server/services/category.service.ts
|
|
- src/server/services/item.service.ts
|
|
- src/server/services/thread.service.ts
|
|
- src/server/services/setup.service.ts
|
|
- src/server/services/totals.service.ts
|
|
- tests/helpers/db.ts
|
|
- src/client/lib/iconData.ts
|
|
- package.json
|
|
autonomous: true
|
|
requirements: [CAT-03]
|
|
|
|
must_haves:
|
|
truths:
|
|
- "Database schema uses 'icon' column (not 'emoji') on categories table with default 'package'"
|
|
- "Zod schemas validate 'icon' field as a string (Lucide icon name) instead of 'emoji'"
|
|
- "All server services reference categories.icon instead of categories.emoji"
|
|
- "Curated icon data with ~80-120 gear-relevant Lucide icons is available for the picker"
|
|
- "A LucideIcon render component exists for displaying icons by name string"
|
|
- "Existing emoji data in the database is migrated to equivalent Lucide icon names"
|
|
artifacts:
|
|
- path: "src/db/schema.ts"
|
|
provides: "Categories table with icon column"
|
|
contains: "icon.*text.*default.*package"
|
|
- path: "src/shared/schemas.ts"
|
|
provides: "Category Zod schemas with icon field"
|
|
contains: "icon.*z.string"
|
|
- path: "src/client/lib/iconData.ts"
|
|
provides: "Curated icon groups and LucideIcon component"
|
|
exports: ["iconGroups", "LucideIcon", "EMOJI_TO_ICON_MAP"]
|
|
- path: "tests/helpers/db.ts"
|
|
provides: "Test helper with icon column"
|
|
contains: "icon TEXT NOT NULL DEFAULT"
|
|
key_links:
|
|
- from: "src/db/schema.ts"
|
|
to: "src/shared/types.ts"
|
|
via: "Drizzle type inference"
|
|
pattern: "categories\\.\\$inferSelect"
|
|
- from: "src/shared/schemas.ts"
|
|
to: "src/server/routes/categories.ts"
|
|
via: "Zod validation"
|
|
pattern: "createCategorySchema"
|
|
- from: "src/client/lib/iconData.ts"
|
|
to: "downstream icon picker and display components"
|
|
via: "import"
|
|
pattern: "iconGroups|LucideIcon"
|
|
---
|
|
|
|
<objective>
|
|
Migrate the category data layer from emoji to Lucide icons and create the icon data infrastructure.
|
|
|
|
Purpose: Establish the foundation (schema, types, icon data, render helper) that all UI components will consume. Without this, no component can display or select Lucide icons.
|
|
Output: Updated DB schema with `icon` column, Zod schemas with `icon` field, all services updated, curated icon data file with render component, Drizzle migration generated, lucide-react installed.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@/home/jean-luc-makiola/.claude/get-shit-done/workflows/execute-plan.md
|
|
@/home/jean-luc-makiola/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/06-category-icons/06-CONTEXT.md
|
|
|
|
<interfaces>
|
|
<!-- Key types and contracts the executor needs -->
|
|
|
|
From src/db/schema.ts (CURRENT - will be modified):
|
|
```typescript
|
|
export const categories = sqliteTable("categories", {
|
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
name: text("name").notNull().unique(),
|
|
emoji: text("emoji").notNull().default("\u{1F4E6}"), // RENAME to icon, default "package"
|
|
createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => new Date()),
|
|
});
|
|
```
|
|
|
|
From src/shared/schemas.ts (CURRENT - will be modified):
|
|
```typescript
|
|
export const createCategorySchema = z.object({
|
|
name: z.string().min(1, "Category name is required"),
|
|
emoji: z.string().min(1).max(4).default("\u{1F4E6}"), // RENAME to icon, change validation
|
|
});
|
|
export const updateCategorySchema = z.object({
|
|
id: z.number().int().positive(),
|
|
name: z.string().min(1).optional(),
|
|
emoji: z.string().min(1).max(4).optional(), // RENAME to icon
|
|
});
|
|
```
|
|
|
|
From src/server/services/*.ts (all reference categories.emoji):
|
|
```typescript
|
|
// item.service.ts line 22, thread.service.ts lines 25+70, setup.service.ts line 60, totals.service.ts line 12
|
|
categoryEmoji: categories.emoji, // RENAME to categoryIcon: categories.icon
|
|
```
|
|
|
|
From src/server/services/category.service.ts:
|
|
```typescript
|
|
export function createCategory(db, data: { name: string; emoji?: string }) { ... }
|
|
export function updateCategory(db, id, data: { name?: string; emoji?: string }) { ... }
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Migrate schema, Zod schemas, services, test helper, and seed to icon field</name>
|
|
<files>
|
|
src/db/schema.ts,
|
|
src/shared/schemas.ts,
|
|
src/server/services/category.service.ts,
|
|
src/server/services/item.service.ts,
|
|
src/server/services/thread.service.ts,
|
|
src/server/services/setup.service.ts,
|
|
src/server/services/totals.service.ts,
|
|
src/db/seed.ts,
|
|
tests/helpers/db.ts
|
|
</files>
|
|
<action>
|
|
1. In `src/db/schema.ts`: Rename the `emoji` column on `categories` to `icon` with `text("icon").notNull().default("package")`. The column name in the database changes from `emoji` to `icon`.
|
|
|
|
2. In `src/shared/schemas.ts`:
|
|
- `createCategorySchema`: Replace `emoji: z.string().min(1).max(4).default("📦")` with `icon: z.string().min(1).max(50).default("package")`. The max is 50 to allow Lucide icon names like "mountain-snow".
|
|
- `updateCategorySchema`: Replace `emoji: z.string().min(1).max(4).optional()` with `icon: z.string().min(1).max(50).optional()`.
|
|
|
|
3. In `src/server/services/category.service.ts`:
|
|
- `createCategory`: Change function parameter type from `{ name: string; emoji?: string }` to `{ name: string; icon?: string }`. Update the spread to use `data.icon` and `{ icon: data.icon }`.
|
|
- `updateCategory`: Change parameter type from `{ name?: string; emoji?: string }` to `{ name?: string; icon?: string }`.
|
|
|
|
4. In `src/server/services/item.service.ts`: Change `categoryEmoji: categories.emoji` to `categoryIcon: categories.icon` in the select.
|
|
|
|
5. In `src/server/services/thread.service.ts`: Same rename — `categoryEmoji: categories.emoji` to `categoryIcon: categories.icon` in both `getAllThreads` and `getThreadById` functions.
|
|
|
|
6. In `src/server/services/setup.service.ts`: Same rename — `categoryEmoji` to `categoryIcon`.
|
|
|
|
7. In `src/server/services/totals.service.ts`: Same rename — `categoryEmoji` to `categoryIcon`.
|
|
|
|
8. In `src/db/seed.ts`: Change `emoji: "\u{1F4E6}"` to `icon: "package"`.
|
|
|
|
9. In `tests/helpers/db.ts`: Change the CREATE TABLE statement for categories to use `icon TEXT NOT NULL DEFAULT 'package'` instead of `emoji TEXT NOT NULL DEFAULT '📦'`. Update the seed insert to use `icon: "package"` instead of `emoji: "\u{1F4E6}"`.
|
|
|
|
10. Generate the Drizzle migration: Run `bun run db:generate` to create the migration SQL. The migration needs to handle renaming the column AND converting existing emoji values to icon names. After generation, inspect the migration file and add data conversion SQL if Drizzle doesn't handle it automatically. The emoji-to-icon mapping for migration:
|
|
- 📦 -> "package"
|
|
- 🏕️/⛺ -> "tent"
|
|
- 🚲 -> "bike"
|
|
- 📷 -> "camera"
|
|
- 🎒 -> "backpack"
|
|
- 👕 -> "shirt"
|
|
- 🔧 -> "wrench"
|
|
- 🍳 -> "cooking-pot"
|
|
- Any unmapped emoji -> "package" (fallback)
|
|
|
|
NOTE: Since SQLite doesn't support ALTER TABLE RENAME COLUMN in all versions, the migration may need to recreate the table. Check the generated migration and ensure it works. If `bun run db:generate` produces a column rename, verify it. If it produces a drop+recreate, ensure data is preserved. You may need to manually write migration SQL that: (a) creates a new column `icon`, (b) updates it from `emoji` with the mapping, (c) drops the `emoji` column. Test with `bun run db:push`.
|
|
</action>
|
|
<verify>
|
|
<automated>bun test tests/services/category.service.test.ts -t "create" 2>&1 | head -20; echo "---"; bun run db:push 2>&1 | tail -5</automated>
|
|
</verify>
|
|
<done>
|
|
- categories table has `icon` column (text, default "package") instead of `emoji`
|
|
- All Zod schemas use `icon` field
|
|
- All services reference `categories.icon` and return `categoryIcon`
|
|
- Test helper creates table with `icon` column
|
|
- `bun run db:push` applies migration without errors
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Install lucide-react and create icon data file with LucideIcon component</name>
|
|
<files>
|
|
package.json,
|
|
src/client/lib/iconData.ts
|
|
</files>
|
|
<action>
|
|
1. Install lucide-react: `bun add lucide-react`
|
|
|
|
2. Create `src/client/lib/iconData.ts` with:
|
|
|
|
a) An `EMOJI_TO_ICON_MAP` constant (Record<string, string>) mapping emoji characters to Lucide icon names. Cover at minimum:
|
|
- 📦 -> "package", 🏕️ -> "tent", ⛺ -> "tent", 🚲 -> "bike", 📷 -> "camera"
|
|
- 🎒 -> "backpack", 👕 -> "shirt", 🔧 -> "wrench", 🍳 -> "cooking-pot"
|
|
- 🎮 -> "gamepad-2", 💻 -> "laptop", 🏔️ -> "mountain-snow", ⛰️ -> "mountain"
|
|
- 🏖️ -> "umbrella-off", 🧭 -> "compass", 🔦 -> "flashlight", 🔋 -> "battery"
|
|
- 📱 -> "smartphone", 🎧 -> "headphones", 🧤 -> "hand", 🧣 -> "scarf"
|
|
- 👟 -> "footprints", 🥾 -> "footprints", 🧢 -> "hard-hat", 🕶️ -> "glasses"
|
|
- Plus any other reasonable gear-related emoji from the old emojiData.ts
|
|
|
|
b) An `IconGroup` interface and `iconGroups` array with ~80-120 curated gear-relevant Lucide icons organized into groups:
|
|
```typescript
|
|
interface IconEntry { name: string; keywords: string[] }
|
|
interface IconGroup { name: string; icon: string; icons: IconEntry[] }
|
|
```
|
|
Groups (matching picker tabs):
|
|
- **Outdoor**: tent, campfire, mountain, mountain-snow, compass, map, map-pin, binoculars, tree-pine, trees, sun, cloud-rain, snowflake, wind, flame, leaf, flower-2, sunrise, sunset, moon, star, thermometer
|
|
- **Travel**: backpack, luggage, plane, car, bike, ship, train-front, map-pinned, globe, ticket, route, navigation, milestone, fuel, parking-meter
|
|
- **Sports**: dumbbell, trophy, medal, timer, heart-pulse, footprints, gauge, target, flag, swords, shield, zap
|
|
- **Electronics**: laptop, smartphone, tablet-smartphone, headphones, camera, battery, bluetooth, wifi, usb, monitor, keyboard, mouse, gamepad-2, speaker, radio, tv, plug, cable, cpu, hard-drive
|
|
- **Clothing**: shirt, glasses, watch, gem, scissors, ruler, palette
|
|
- **Cooking**: cooking-pot, utensils, cup-soda, coffee, beef, fish, apple, wheat, flame-kindling, refrigerator, microwave
|
|
- **Tools**: wrench, hammer, screwdriver, drill, ruler, tape-measure, flashlight, pocket-knife, axe, shovel, paintbrush, scissors, cog, nut
|
|
- **General**: package, box, tag, bookmark, archive, folder, grid-3x3, list, layers, circle-dot, square, hexagon, triangle, heart, star, plus, check, x
|
|
|
|
Each icon entry has `name` (the Lucide icon name) and `keywords` (array of search terms for filtering).
|
|
|
|
c) A `LucideIcon` React component that renders a Lucide icon by name string:
|
|
```typescript
|
|
import { icons } from "lucide-react";
|
|
|
|
interface LucideIconProps {
|
|
name: string;
|
|
size?: number;
|
|
className?: string;
|
|
}
|
|
|
|
export function LucideIcon({ name, size = 20, className = "" }: LucideIconProps) {
|
|
const IconComponent = icons[name as keyof typeof icons];
|
|
if (!IconComponent) {
|
|
const FallbackIcon = icons["Package"];
|
|
return <FallbackIcon size={size} className={className} />;
|
|
}
|
|
return <IconComponent size={size} className={className} />;
|
|
}
|
|
```
|
|
|
|
IMPORTANT: Lucide icon names in the `icons` map use PascalCase (e.g., "Package", "MountainSnow"). The `name` prop should accept kebab-case (matching Lucide convention) and convert to PascalCase for lookup. Add a conversion helper:
|
|
```typescript
|
|
function toPascalCase(str: string): string {
|
|
return str.split("-").map(s => s.charAt(0).toUpperCase() + s.slice(1)).join("");
|
|
}
|
|
```
|
|
Use `icons[toPascalCase(name)]` for lookup.
|
|
|
|
NOTE: This approach imports the entire lucide-react icons object for dynamic lookup by name. This is intentional — the icon picker needs access to all icons by name string. Tree-shaking won't help here since we need runtime lookup. The bundle impact is acceptable for this single-user app.
|
|
</action>
|
|
<verify>
|
|
<automated>bun run build 2>&1 | tail -5; echo "---"; grep -c "lucide-react" package.json</automated>
|
|
</verify>
|
|
<done>
|
|
- lucide-react is installed as a dependency
|
|
- `src/client/lib/iconData.ts` exports `iconGroups`, `LucideIcon`, and `EMOJI_TO_ICON_MAP`
|
|
- `LucideIcon` renders any Lucide icon by kebab-case name string with fallback to Package icon
|
|
- Icon groups contain ~80-120 curated gear-relevant icons across 8 groups
|
|
- `bun run build` succeeds without errors
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `bun test` passes (all existing tests work with icon field)
|
|
- `bun run build` succeeds
|
|
- Database migration applies cleanly via `bun run db:push`
|
|
- `src/client/lib/iconData.ts` exports are importable
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- Categories table uses `icon` text column with "package" default
|
|
- All Zod schemas, services, types reference `icon` not `emoji`
|
|
- lucide-react installed
|
|
- Icon data file with curated groups and LucideIcon render component exists
|
|
- All tests pass, build succeeds
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/06-category-icons/06-01-SUMMARY.md`
|
|
</output>
|