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:
+19
-17
@@ -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 “{search}”.</p>
|
||||
</div>
|
||||
) : (
|
||||
filtered.map((product) => {
|
||||
products.map((product) => {
|
||||
const low = lowestPrice(product)
|
||||
const cheapest = product.prices.find((p) => p.price === low)
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user