--- phase: 07-weight-unit-selection plan: 01 type: tdd wave: 1 depends_on: [] files_modified: - src/client/lib/formatters.ts - src/client/hooks/useWeightUnit.ts - tests/lib/formatters.test.ts autonomous: true requirements: - UNIT-02 - UNIT-03 must_haves: truths: - "formatWeight converts grams to g, oz, lb, kg with correct precision" - "formatWeight defaults to grams when no unit is specified (backward compatible)" - "formatWeight handles null/undefined input for all units" - "useWeightUnit hook returns a valid WeightUnit from settings, defaulting to 'g'" artifacts: - path: "src/client/lib/formatters.ts" provides: "WeightUnit type export and parameterized formatWeight function" exports: ["WeightUnit", "formatWeight", "formatPrice"] contains: "WeightUnit" - path: "src/client/hooks/useWeightUnit.ts" provides: "Convenience hook wrapping useSetting for weight unit" exports: ["useWeightUnit"] - path: "tests/lib/formatters.test.ts" provides: "Unit tests for formatWeight with all 4 units and edge cases" min_lines: 30 key_links: - from: "src/client/hooks/useWeightUnit.ts" to: "src/client/hooks/useSettings.ts" via: "useSetting('weightUnit')" pattern: "useSetting.*weightUnit" - from: "src/client/hooks/useWeightUnit.ts" to: "src/client/lib/formatters.ts" via: "imports WeightUnit type" pattern: "import.*WeightUnit.*formatters" --- Create the weight unit conversion core: a parameterized `formatWeight` function with a `WeightUnit` type and a `useWeightUnit` convenience hook, all backed by tests. Purpose: Establish the conversion contracts (type, function, hook) that Plan 02 will wire into every component. TDD approach ensures the conversion math is correct before any UI work. Output: Working `formatWeight(grams, unit)` with tests green, `useWeightUnit()` hook ready for consumption. @/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md @/home/jlmak/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/07-weight-unit-selection/07-RESEARCH.md @src/client/lib/formatters.ts @src/client/hooks/useSettings.ts From src/client/lib/formatters.ts (current): ```typescript export function formatWeight(grams: number | null | undefined): string { if (grams == null) return "--"; return `${Math.round(grams)}g`; } export function formatPrice(cents: number | null | undefined): string { if (cents == null) return "--"; return `$${(cents / 100).toFixed(2)}`; } ``` From src/client/hooks/useSettings.ts: ```typescript export function useSetting(key: string) { return useQuery({ queryKey: ["settings", key], queryFn: async () => { try { const result = await apiGet(`/api/settings/${key}`); return result.value; } catch (err: any) { if (err?.status === 404) return null; throw err; } }, }); } export function useUpdateSetting() { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ key, value }: { key: string; value: string }) => apiPut(`/api/settings/${key}`, { value }), onSuccess: (_data, variables) => { queryClient.invalidateQueries({ queryKey: ["settings", variables.key] }); }, }); } ``` formatWeight unit conversion src/client/lib/formatters.ts, tests/lib/formatters.test.ts Conversion constants: 1 oz = 28.3495g, 1 lb = 453.592g, 1 kg = 1000g - formatWeight(100, "g") -> "100g" - formatWeight(100, "oz") -> "3.5 oz" - formatWeight(1000, "lb") -> "2.20 lb" - formatWeight(1500, "kg") -> "1.50 kg" - formatWeight(null, "oz") -> "--" - formatWeight(undefined, "kg") -> "--" - formatWeight(100) -> "100g" (default unit, backward compatible) - formatWeight(0, "oz") -> "0.0 oz" - formatWeight(5, "lb") -> "0.01 lb" (small weight precision) - formatWeight(50000, "kg") -> "50.00 kg" (large weight) 1. Add `WeightUnit` type export: `"g" | "oz" | "lb" | "kg"` 2. Add conversion constants as module-level consts (not exported) 3. Modify `formatWeight` signature to `(grams: number | null | undefined, unit: WeightUnit = "g"): string` 4. Keep the null guard as-is at the top 5. Add switch statement for unit-specific formatting: - g: `Math.round(grams)` + "g" (0 decimals, current behavior) - oz: `.toFixed(1)` + " oz" (1 decimal) - lb: `.toFixed(2)` + " lb" (2 decimals) - kg: `.toFixed(2)` + " kg" (2 decimals) 6. Do NOT modify `formatPrice` — leave it untouched Task 1: TDD formatWeight with unit parameter src/client/lib/formatters.ts, tests/lib/formatters.test.ts - formatWeight(100, "g") returns "100g" - formatWeight(100, "oz") returns "3.5 oz" - formatWeight(1000, "lb") returns "2.20 lb" - formatWeight(1500, "kg") returns "1.50 kg" - formatWeight(null) returns "--" for all units - formatWeight(undefined, "kg") returns "--" - formatWeight(100) returns "100g" (backward compatible, no second arg) - formatWeight(0, "oz") returns "0.0 oz" RED: Create `tests/lib/formatters.test.ts`. Import `formatWeight` from `@/client/lib/formatters`. Write tests for: - All 4 units with a known gram value (e.g., 1000g = "1000g", "35.3 oz", "2.20 lb", "1.00 kg") - Null and undefined input returning "--" for each unit - Default parameter (no second arg) producing current "g" behavior - Zero grams producing "0g", "0.0 oz", "0.00 lb", "0.00 kg" - Precision edge cases (small values like 5g in lb = "0.01 lb") Run tests — they should fail because formatWeight does not accept a unit parameter yet. GREEN: Modify `src/client/lib/formatters.ts`: - Export `type WeightUnit = "g" | "oz" | "lb" | "kg"` - Add constants: `GRAMS_PER_OZ = 28.3495`, `GRAMS_PER_LB = 453.592`, `GRAMS_PER_KG = 1000` - Change signature to `formatWeight(grams: number | null | undefined, unit: WeightUnit = "g")` - Add switch statement after the null guard for unit-specific conversion and formatting - Leave `formatPrice` completely untouched Run tests — all should pass. REFACTOR: None expected — the code is already minimal. bun test tests/lib/formatters.test.ts formatWeight handles all 4 units with correct precision, null handling, and backward-compatible default. WeightUnit type is exported. All tests pass. Task 2: Create useWeightUnit convenience hook src/client/hooks/useWeightUnit.ts Create `src/client/hooks/useWeightUnit.ts`: ```typescript import { useSetting } from "./useSettings"; import type { WeightUnit } from "../lib/formatters"; const VALID_UNITS: WeightUnit[] = ["g", "oz", "lb", "kg"]; export function useWeightUnit(): WeightUnit { const { data } = useSetting("weightUnit"); if (data && VALID_UNITS.includes(data as WeightUnit)) { return data as WeightUnit; } return "g"; } ``` This hook: - Wraps `useSetting("weightUnit")` for a typed return value - Validates the stored value is a known unit (protects against bad data) - Defaults to "g" when no setting exists (backward compatible — UNIT-03 persistence works via existing settings API) - Returns `WeightUnit` type so components can pass directly to `formatWeight` bun run lint useWeightUnit hook exists, imports from useSettings and formatters, returns typed WeightUnit with "g" default. Lint passes. - `bun test tests/lib/formatters.test.ts` passes with all unit conversion tests green - `bun run lint` passes with no errors - `src/client/lib/formatters.ts` exports `WeightUnit` type and updated `formatWeight` function - `src/client/hooks/useWeightUnit.ts` exists and exports `useWeightUnit` - Existing tests still pass: `bun test` (full suite) - formatWeight("g") produces identical output to the old function (backward compatible) - formatWeight with oz/lb/kg produces correct conversions with appropriate decimal precision - WeightUnit type is exported for use by Plan 02 components - useWeightUnit hook is ready for components to consume - All existing tests remain green (no regressions) After completion, create `.planning/phases/07-weight-unit-selection/07-01-SUMMARY.md`