fix: address Chip's review — secure auth, wire TanStack Query, fix UX issues

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 <html>)
- 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 <noreply@paperclip.ing>
This commit is contained in:
Frontend Frankie
2026-03-18 11:54:06 +00:00
parent 034f12d0aa
commit fe5b0e87bd
14 changed files with 307 additions and 62 deletions
+19 -17
View File
@@ -1,24 +1,22 @@
import { useState, useMemo } from 'react'
import { useState } from 'react'
import { Link } from 'react-router-dom'
import { mockProducts } from '../lib/mock-data.ts'
import { useProducts } from '../hooks/useApi.ts'
export function Products() {
const [search, setSearch] = useState('')
const { data: products = [], isLoading, error } = useProducts(search || undefined)
const filtered = useMemo(() => {
if (!search.trim()) return mockProducts
const q = search.toLowerCase()
return mockProducts.filter(
(p) =>
p.name.toLowerCase().includes(q) ||
p.brand.toLowerCase().includes(q) ||
p.category.toLowerCase().includes(q),
)
}, [search])
const lowestPrice = (product: typeof mockProducts[0]) =>
const lowestPrice = (product: typeof products[0]) =>
Math.min(...product.prices.map((p) => p.price))
if (error) {
return (
<div className="py-8 text-center">
<p className="text-sm text-red-600">Failed to load products.</p>
</div>
)
}
return (
<div>
<h1 className="text-2xl font-bold text-gray-900">Products</h1>
@@ -36,12 +34,16 @@ export function Products() {
{/* Product list */}
<div className="mt-4 space-y-3">
{filtered.length === 0 ? (
{isLoading ? (
[1, 2, 3].map((i) => (
<div key={i} className="h-24 animate-pulse rounded-xl bg-gray-200" />
))
) : products.length === 0 ? (
<div className="rounded-xl bg-white p-6 text-center shadow-sm">
<p className="text-sm text-gray-500">No products match "{search}".</p>
<p className="text-sm text-gray-500">No products match &ldquo;{search}&rdquo;.</p>
</div>
) : (
filtered.map((product) => {
products.map((product) => {
const low = lowestPrice(product)
const cheapest = product.prices.find((p) => p.price === low)
return (