From d0185fd93d7f0d7eec9263bcd9ddc74eb682f262 Mon Sep 17 00:00:00 2001 From: Frontend Frankie Date: Wed, 18 Mar 2026 11:54:06 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20address=20Chip's=20review=20=E2=80=94=20?= =?UTF-8?q?secure=20auth,=20wire=20TanStack=20Query,=20fix=20UX=20issues?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Must-fix: - Exclude JWT token from Zustand persist (partialize) to prevent localStorage XSS exfiltration — token now lives in memory only - Wire all pages through TanStack Query hooks (usePurchases, useProduct, useProducts, usePriceHistory, useCoupons, usePriceAlerts) with proper loading skeletons and error states - Add mock interceptor in api.ts (VITE_MOCK_API=true) so mock data flows through the same fetch path — single flag to switch to live API Should-fix: - Wire theme toggle to DOM (dark class on ) - Fix AccountLinking form inputs (controlled with value/onChange) - Remove unused err in catch blocks (Login, Register) - Bump remaining min-h-10 touch targets to min-h-12 (48px) Build: 128KB initial JS, Recharts 498KB lazy chunk. 5/5 tests pass. Co-Authored-By: Paperclip --- src/lib/api.ts | 64 +++++++++++++++++++++++++++++++++++ src/pages/AccountLinking.tsx | 18 ++++++---- src/pages/Alerts.tsx | 40 +++++++++++++++++++--- src/pages/Coupons.tsx | 28 +++++++++++++-- src/pages/Dashboard.tsx | 55 ++++++++++++++++++++++++------ src/pages/Login.tsx | 2 +- src/pages/ProductDetail.tsx | 16 +++++++-- src/pages/Products.tsx | 36 ++++++++++---------- src/pages/PurchaseDetail.tsx | 20 +++++++++-- src/pages/Purchases.tsx | 44 ++++++++++++++++++++---- src/pages/Register.tsx | 2 +- src/pages/StoreComparison.tsx | 18 ++++++++-- src/stores/auth.ts | 5 ++- src/stores/theme.ts | 21 ++++++++++-- 14 files changed, 307 insertions(+), 62 deletions(-) diff --git a/src/lib/api.ts b/src/lib/api.ts index f36fa79..beaced7 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,8 +1,72 @@ import { useAuthStore } from '../stores/auth.ts' +import { + mockPurchases, + mockProducts, + mockCoupons, + mockAlerts, + getMockPriceHistory, +} from './mock-data.ts' const API_BASE = import.meta.env.VITE_API_URL ?? '/api/v1' +const USE_MOCK = import.meta.env.VITE_MOCK_API === 'true' + +// Mock response lookup table +const mockRoutes: Record unknown> = { + '/purchases': () => mockPurchases, + '/products': () => mockProducts, + '/coupons': () => mockCoupons, + '/price-alerts': () => mockAlerts, +} + +function matchMockRoute(path: string): T | null { + // Exact match + if (mockRoutes[path]) return mockRoutes[path](path) as T + + // /purchases/:id + const purchaseMatch = path.match(/^\/purchases\/(.+)$/) + if (purchaseMatch) { + const purchase = mockPurchases.find((p) => p.id === purchaseMatch[1]) + return (purchase ?? null) as T + } + + // /products/:id/price-history + const priceHistoryMatch = path.match(/^\/products\/(.+)\/price-history$/) + if (priceHistoryMatch) { + return getMockPriceHistory(priceHistoryMatch[1]) as T + } + + // /products?q=search or /products/:id + const productMatch = path.match(/^\/products\/(.+)$/) + if (productMatch) { + const product = mockProducts.find((p) => p.id === productMatch[1]) + return (product ?? null) as T + } + + const productsSearch = path.match(/^\/products\?q=(.+)$/) + if (productsSearch) { + const q = decodeURIComponent(productsSearch[1]).toLowerCase() + return mockProducts.filter( + (p) => + p.name.toLowerCase().includes(q) || + p.brand.toLowerCase().includes(q) || + p.category.toLowerCase().includes(q), + ) as T + } + + return null +} async function apiFetch(path: string, options?: RequestInit): Promise { + // Mock interceptor: return mock data without hitting the network + if (USE_MOCK && (!options?.method || options.method === 'GET')) { + const mockResult = matchMockRoute(path) + if (mockResult !== null) { + // Simulate network delay for realistic loading states + await new Promise((r) => setTimeout(r, 300)) + return mockResult + } + } + const token = useAuthStore.getState().token const res = await fetch(`${API_BASE}${path}`, { diff --git a/src/pages/AccountLinking.tsx b/src/pages/AccountLinking.tsx index 96aa15f..3b4d8b2 100644 --- a/src/pages/AccountLinking.tsx +++ b/src/pages/AccountLinking.tsx @@ -44,9 +44,9 @@ export function AccountLinking() { const [connected, setConnected] = useState(['meijer', 'kroger']) const [status, setStatus] = useState<'idle' | 'connecting' | 'success' | 'error'>('idle') - function handleConnect(storeId: string) { + function handleConnect(storeId: string, _fields: Record) { setStatus('connecting') - // Simulate connection + // Simulate connection — fields will be sent to API when available setTimeout(() => { setConnected((prev) => [...prev, storeId]) setStatus('success') @@ -100,7 +100,7 @@ export function AccountLinking() { {isConnected && !isLinking && ( @@ -119,7 +119,7 @@ export function AccountLinking() { handleConnect(store.id)} + onSubmit={(fields) => handleConnect(store.id, fields)} onCancel={() => { setLinking(null) setStatus('idle') @@ -149,9 +149,13 @@ function LinkForm({ }: { store: StoreConfig status: string - onSubmit: () => void + onSubmit: (fields: Record) => void onCancel: () => void }) { + const [values, setValues] = useState>(() => + Object.fromEntries(store.fields.map((f) => [f.key, ''])), + ) + return (
{store.fields.map((field) => ( @@ -159,6 +163,8 @@ function LinkForm({ key={field.key} type={field.type} placeholder={field.label} + value={values[field.key] ?? ''} + onChange={(e) => setValues((prev) => ({ ...prev, [field.key]: e.target.value }))} autoComplete={field.type === 'password' ? 'current-password' : field.type} className="min-h-12 w-full rounded-xl border border-gray-200 px-4 text-base focus:border-brand-blue focus:outline-none focus:ring-1 focus:ring-brand-blue" /> @@ -186,7 +192,7 @@ function LinkForm({ {status === 'idle' && (