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 5563b5f145
commit d0185fd93d
14 changed files with 307 additions and 62 deletions
+35 -5
View File
@@ -1,17 +1,47 @@
import { useState } from 'react'
import { Link } from 'react-router-dom'
import { mockAlerts } from '../lib/mock-data.ts'
import { usePriceAlerts } from '../hooks/useApi.ts'
import type { PriceAlert } from '../types/api.ts'
export function Alerts() {
const [alerts, setAlerts] = useState<PriceAlert[]>(mockAlerts)
const { data: fetchedAlerts = [], isLoading, error } = usePriceAlerts()
const [localAlerts, setLocalAlerts] = useState<PriceAlert[]>([])
const [deletedIds, setDeletedIds] = useState<Set<string>>(new Set())
const [showCreate, setShowCreate] = useState(false)
// Merge fetched + locally created, minus deleted
const alerts = [
...localAlerts,
...fetchedAlerts.filter((a) => !deletedIds.has(a.id)),
]
const triggered = alerts.filter((a) => a.triggered)
const watching = alerts.filter((a) => !a.triggered)
function handleDelete(id: string) {
setAlerts((prev) => prev.filter((a) => a.id !== id))
setLocalAlerts((prev) => prev.filter((a) => a.id !== id))
setDeletedIds((prev) => new Set(prev).add(id))
}
if (isLoading) {
return (
<div className="animate-pulse">
<div className="h-8 w-32 rounded bg-gray-200" />
<div className="mt-6 space-y-3">
{[1, 2, 3].map((i) => (
<div key={i} className="h-24 rounded-xl bg-gray-200" />
))}
</div>
</div>
)
}
if (error) {
return (
<div className="py-8 text-center">
<p className="text-sm text-red-600">Failed to load price alerts.</p>
</div>
)
}
return (
@@ -28,7 +58,7 @@ export function Alerts() {
{/* Create alert form */}
{showCreate && <CreateAlertForm onClose={() => setShowCreate(false)} onCreated={(a) => {
setAlerts((prev) => [a, ...prev])
setLocalAlerts((prev) => [a, ...prev])
setShowCreate(false)
}} />}
@@ -115,7 +145,7 @@ function AlertCard({
)}
<button
onClick={() => onDelete(alert.id)}
className="min-h-10 min-w-10 rounded-lg p-2 text-gray-400 active:bg-gray-100"
className="min-h-12 min-w-12 rounded-lg p-2 text-gray-400 active:bg-gray-100"
aria-label="Delete alert"
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>