From 8c30a7ab09d345fb87b31385c80749d9ff0c6d31 Mon Sep 17 00:00:00 2001 From: null Date: Fri, 3 Jul 2026 22:01:30 -0500 Subject: [PATCH] refactor(ts): convert small hooks + version to TypeScript (TS8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useAutoSave.ts — generic useAutoSave; AutoSaveStatus union; timer ref typed ReturnType; pending ref T|null narrows on != null. - useSearchPanelPreference.ts — typed [boolean, setter] tuple return. - version.ts — ReleaseNotes/ReleaseHighlight shapes; the Vite-injected __APP_VERSION__ global declared inline (declare const). No behavior change. useAutoSave.test.jsx imports the .ts transparently and passes. typecheck 0, lint 0 errors (47 warns), build green, 48 client tests. Co-Authored-By: Claude Opus 4.8 --- HISTORY.md | 2 +- .../hooks/{useAutoSave.js => useAutoSave.ts} | 24 +++++++++++++------ ...ference.js => useSearchPanelPreference.ts} | 6 ++--- client/lib/{version.js => version.ts} | 18 ++++++++++++-- 4 files changed, 37 insertions(+), 13 deletions(-) rename client/hooks/{useAutoSave.js => useAutoSave.ts} (70%) rename client/hooks/{useSearchPanelPreference.js => useSearchPanelPreference.ts} (79%) rename client/lib/{version.js => version.ts} (86%) diff --git a/HISTORY.md b/HISTORY.md index e0203a7..b3e14c2 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -5,7 +5,7 @@ - **[Client] Adopted TypeScript incrementally, starting with the money boundary** — the move off plain JS. Upgraded `jsconfig` → `tsconfig` with full `strict` + `noUncheckedIndexedAccess`, but `allowJs` + `checkJs:false` so every existing `.js`/`.jsx` keeps resolving and running untouched — only `.ts`/`.tsx` are type-checked, so it's a file-by-file migration with nothing breaking in between. Added `npm run typecheck` (`tsc --noEmit`) and wired it into `npm run ci`. Installed TypeScript 6 + `@types/react`/`react-dom` 19. - **[Client] Branded `Cents`/`Dollars` types** — converted `client/lib/money.js` → `money.ts` with `type Cents = number & { __unit: 'cents' }` / `type Dollars = number & { __unit: 'dollars' }` plus `asCents`/`asDollars`/`centsToDollars`/`dollarsToCents`. The formatters now require the correct branded unit (a bare number won't type-check), so a typed caller **physically cannot format cents as dollars** (the 100×-too-big bug — cf. QA-B9-01) or vice-versa. A never-imported `money.type-test.ts` guard uses `@ts-expect-error` to assert each unit mixup is a real compile error, so `typecheck` fails loudly if the branding ever regresses (verified: removing a guard makes tsc error `Argument of type 1234 is not assignable to DollarsInput`). Existing `.jsx` callers are unaffected (checkJs off). Full CI (lint + typecheck + server 181 + client 48 + build) green. -- **[Client] ESLint now type-aware, and the migration is proceeding file-by-file** — extended the flat config with a `client/**/*.{ts,tsx}` block (typescript-eslint parser, same react-hooks/react-refresh enforcement, TS-aware unused-vars) so the `.ts` files get the same correctness linting as the `.jsx` ones. Converted, each its own commit and verified green (typecheck + lint + build + 48 client tests): `client/lib/utils.ts` (the app-wide `cn`/`fmt`/date/format helpers — `fmt` now inherits `formatUSD`'s branded-dollars input), `client/hooks/usePaymentActions.ts` (the shared quick-pay/toggle-paid `useMutation` hooks — typed payloads + `Dollars` on the money amounts so the brand flows to callers, strict-catch errors narrowed via an `errMessage(unknown)` helper), `client/lib/trackerUtils.ts` (the row/status/sort helpers — introduces a `TrackerRow` domain interface and a `TrackerStatus` union; row money fields stay `number` until the API is typed), and a batch of pure leaf utilities — `reorder.ts` (generic `moveInArray`), `billingSchedule.ts` (a `Schedule` union), `billDrafts.ts` (`SourceBill`/`Template`/`Category` shapes for `makeBillDraft`), `trackerTableColumns.ts`, and `cashflowUtils.ts` (typed SVG timeline geometry for the Safe-to-Spend card). The `.test.js` suites that import these resolve the `.ts` transparently and still pass (48 client tests). +- **[Client] ESLint now type-aware, and the migration is proceeding file-by-file** — extended the flat config with a `client/**/*.{ts,tsx}` block (typescript-eslint parser, same react-hooks/react-refresh enforcement, TS-aware unused-vars) so the `.ts` files get the same correctness linting as the `.jsx` ones. Converted, each its own commit and verified green (typecheck + lint + build + 48 client tests): `client/lib/utils.ts` (the app-wide `cn`/`fmt`/date/format helpers — `fmt` now inherits `formatUSD`'s branded-dollars input), `client/hooks/usePaymentActions.ts` (the shared quick-pay/toggle-paid `useMutation` hooks — typed payloads + `Dollars` on the money amounts so the brand flows to callers, strict-catch errors narrowed via an `errMessage(unknown)` helper), `client/lib/trackerUtils.ts` (the row/status/sort helpers — introduces a `TrackerRow` domain interface and a `TrackerStatus` union; row money fields stay `number` until the API is typed), and a batch of pure leaf utilities — `reorder.ts` (generic `moveInArray`), `billingSchedule.ts` (a `Schedule` union), `billDrafts.ts` (`SourceBill`/`Template`/`Category` shapes for `makeBillDraft`), `trackerTableColumns.ts`, and `cashflowUtils.ts` (typed SVG timeline geometry for the Safe-to-Spend card). The `.test.js` suites that import these resolve the `.ts` transparently and still pass (48 client tests). Also converted two small hooks — `useAutoSave.ts` (a generic `useAutoSave` debounced-save hook with an `AutoSaveStatus` union) and `useSearchPanelPreference.ts` (a typed `[boolean, setter]` tuple) — and `version.ts` (release-notes shapes; the Vite-injected `__APP_VERSION__` is declared inline). ### 🧪 Money-service test coverage diff --git a/client/hooks/useAutoSave.js b/client/hooks/useAutoSave.ts similarity index 70% rename from client/hooks/useAutoSave.js rename to client/hooks/useAutoSave.ts index 8e944b8..dc4b2d1 100644 --- a/client/hooks/useAutoSave.js +++ b/client/hooks/useAutoSave.ts @@ -1,5 +1,15 @@ import { useCallback, useEffect, useRef, useState } from 'react'; +export type AutoSaveStatus = 'idle' | 'saving' | 'saved' | 'error'; + +type SaveFn = (payload: T) => unknown; + +export interface UseAutoSave { + status: AutoSaveStatus; + schedule: (payload: T, delay?: number) => void; + flush: () => void; +} + /** * Debounced auto-save with a status the UI can render. * @@ -10,14 +20,14 @@ import { useCallback, useEffect, useRef, useState } from 'react'; * * Pending changes are flushed on unmount so navigating away never loses edits. */ -export function useAutoSave(saveFn, defaultDelay = 400) { - const [status, setStatus] = useState('idle'); - const timer = useRef(null); - const pending = useRef(null); - const saveRef = useRef(saveFn); +export function useAutoSave(saveFn: SaveFn, defaultDelay = 400): UseAutoSave { + const [status, setStatus] = useState('idle'); + const timer = useRef | undefined>(undefined); + const pending = useRef(null); + const saveRef = useRef>(saveFn); saveRef.current = saveFn; - const run = useCallback(async (payload) => { + const run = useCallback(async (payload: T) => { pending.current = null; setStatus('saving'); try { @@ -28,7 +38,7 @@ export function useAutoSave(saveFn, defaultDelay = 400) { } }, []); - const schedule = useCallback((payload, delay = defaultDelay) => { + const schedule = useCallback((payload: T, delay = defaultDelay) => { pending.current = payload; clearTimeout(timer.current); timer.current = setTimeout(() => run(payload), delay); diff --git a/client/hooks/useSearchPanelPreference.js b/client/hooks/useSearchPanelPreference.ts similarity index 79% rename from client/hooks/useSearchPanelPreference.js rename to client/hooks/useSearchPanelPreference.ts index 6438841..dbbc0fd 100644 --- a/client/hooks/useSearchPanelPreference.js +++ b/client/hooks/useSearchPanelPreference.ts @@ -3,11 +3,11 @@ import { api } from '@/api'; const SETTING_KEY = 'search_bars_collapsed'; -function settingToBool(value) { +function settingToBool(value: unknown): boolean { return value === true || value === 'true' || value === '1' || value === 1; } -export function useSearchPanelPreference() { +export function useSearchPanelPreference(): [boolean, (nextCollapsed: boolean) => void] { const [collapsed, setCollapsed] = useState(false); useEffect(() => { @@ -23,7 +23,7 @@ export function useSearchPanelPreference() { return () => { mounted = false; }; }, []); - const saveCollapsed = useCallback((nextCollapsed) => { + const saveCollapsed = useCallback((nextCollapsed: boolean) => { setCollapsed(nextCollapsed); api.saveSettings({ [SETTING_KEY]: nextCollapsed ? 'true' : 'false' }).catch(() => {}); }, []); diff --git a/client/lib/version.js b/client/lib/version.ts similarity index 86% rename from client/lib/version.js rename to client/lib/version.ts index 182f6b2..720e454 100644 --- a/client/lib/version.js +++ b/client/lib/version.ts @@ -1,10 +1,24 @@ // __APP_VERSION__ is injected by Vite at build time from package.json. // Do not hardcode a version string here — update package.json instead. -/* global __APP_VERSION__ */ +declare const __APP_VERSION__: string; + export const APP_VERSION = __APP_VERSION__; export const APP_NAME = 'BillTracker'; -export const RELEASE_NOTES = { +export interface ReleaseHighlight { + icon: string; + title: string; + desc: string; +} + +export interface ReleaseNotes { + version: string; + date: string; + highlights: ReleaseHighlight[]; + image: { src: string; alt: string }; +} + +export const RELEASE_NOTES: ReleaseNotes = { version: APP_VERSION, date: '2026-05-31', highlights: [