Files

8.8 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
07-weight-unit-selection 01 tdd 1
src/client/lib/formatters.ts
src/client/hooks/useWeightUnit.ts
tests/lib/formatters.test.ts
true
UNIT-02
UNIT-03
truths artifacts key_links
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'
path provides exports contains
src/client/lib/formatters.ts WeightUnit type export and parameterized formatWeight function
WeightUnit
formatWeight
formatPrice
WeightUnit
path provides exports
src/client/hooks/useWeightUnit.ts Convenience hook wrapping useSetting for weight unit
useWeightUnit
path provides min_lines
tests/lib/formatters.test.ts Unit tests for formatWeight with all 4 units and edge cases 30
from to via pattern
src/client/hooks/useWeightUnit.ts src/client/hooks/useSettings.ts useSetting('weightUnit') useSetting.*weightUnit
from to via pattern
src/client/hooks/useWeightUnit.ts src/client/lib/formatters.ts imports WeightUnit type 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.

<execution_context> @/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md @/home/jlmak/.claude/get-shit-done/templates/summary.md </execution_context>

@.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):

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:

export function useSetting(key: string) {
  return useQuery({
    queryKey: ["settings", key],
    queryFn: async () => {
      try {
        const result = await apiGet<Setting>(`/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<Setting>(`/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)

<success_criteria>

  • 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) </success_criteria>
After completion, create `.planning/phases/07-weight-unit-selection/07-01-SUMMARY.md`