import { useState, useEffect, type FormEvent } from 'react'; import WatchlistItemCard, { type WatchlistItem } from './WatchlistItemCard'; import ForYouSection from './ForYouSection'; function LoginForm({ onLoggedIn }: { onLoggedIn: () => void }) { const [email, setEmail] = useState(''); const [loading, setLoading] = useState(false); const [message, setMessage] = useState(''); const handleSubmit = async (e: FormEvent) => { e.preventDefault(); setLoading(true); setMessage(''); try { const res = await fetch('/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email }), }); const data = await res.json(); if (data.ok) { setMessage('Check je email voor de bevestigingslink!'); } else if (data.error?.includes('al geregistreerd')) { const loginRes = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email }), }); const loginData = await loginRes.json(); setMessage(loginData.message || 'Check je email voor de inloglink!'); } else { setMessage(data.error || 'Er ging iets mis'); } } catch { setMessage('Er ging iets mis'); } setLoading(false); }; return (

Watchlist & Prijsalerts

Houd je favoriete sneakers in de gaten en ontvang een melding zodra de prijs daalt.

Inloggen met Google
of via e-mail
setEmail(e.target.value)} placeholder="je@email.nl" required className="mb-3 w-full rounded-lg border border-border/30 bg-bg-elevated px-4 py-3 text-text placeholder-text-muted focus:border-accent/50 focus:outline-none" /> {message && (

{message}

)}

Via e-mail ontvang je een magic link — geen wachtwoord nodig.

); } type SortOption = 'newest' | 'price_low' | 'price_high' | 'discount' | 'price_drop'; function urlBase64ToUint8Array(base64String: string) { const padding = '='.repeat((4 - (base64String.length % 4)) % 4); const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); const rawData = atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) outputArray[i] = rawData.charCodeAt(i); return outputArray; } export default function WatchlistPageApp() { const [items, setItems] = useState([]); const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [sortBy, setSortBy] = useState('newest'); const [filterBrand, setFilterBrand] = useState('all'); const [filterDealsOnly, setFilterDealsOnly] = useState(false); const [pushEnabled, setPushEnabled] = useState(false); const [pushSubscribing, setPushSubscribing] = useState(false); useEffect(() => { Promise.all([ fetch('/api/auth/session').then(r => r.json()), fetch('/api/watchlist').then(r => r.json()).catch(() => ({ items: [] })), ]).then(([session, watchlist]) => { setUser(session.user); setItems(watchlist.items || []); setLoading(false); }); // Check push status from localStorage setPushEnabled(localStorage.getItem('sp_push_subscribed') === '1'); }, []); const enablePush = async () => { if (!('PushManager' in window) || !('serviceWorker' in navigator)) { alert('Je browser ondersteunt geen push notificaties.'); return; } setPushSubscribing(true); try { const reg = await navigator.serviceWorker.ready; const vapidKey = (import.meta as any).env?.PUBLIC_VAPID_KEY; const sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(vapidKey), }); await fetch('/api/push/subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(sub.toJSON()), }); localStorage.setItem('sp_push_subscribed', '1'); localStorage.removeItem('sp_push_dismissed'); setPushEnabled(true); } catch { alert('Push notificaties konden niet worden ingeschakeld. Controleer je browserinstellingen.'); } setPushSubscribing(false); }; const removeItem = async (productId: number) => { await fetch('/api/watchlist', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ product_id: productId }), }); setItems(items.filter(i => i.product_id !== productId)); }; const updateTargetPrice = async (productId: number, price: number | null) => { await fetch('/api/watchlist', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ product_id: productId, target_price: price }), }); setItems(items.map(i => i.product_id === productId ? { ...i, target_price: price } : i)); }; const toggleNotification = async (productId: number, field: 'notify_price_drop' | 'notify_back_in_stock' | 'notify_push_price_drop' | 'notify_push_back_in_stock', value: boolean) => { await fetch('/api/watchlist', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ product_id: productId, [field]: value }), }); setItems(items.map(i => i.product_id === productId ? { ...i, [field]: value } : i)); }; if (loading) { return
Laden...
; } if (!user) { return window.location.reload()} />; } if (items.length === 0) { return (

👟

Je watchlist is nog leeg

Ontdek sneakers
); } // Derive unique brands from items const brands = [...new Set(items.map(i => i.brand))].sort(); // Filter let filtered = items; if (filterBrand !== 'all') { filtered = filtered.filter(i => i.brand === filterBrand); } if (filterDealsOnly) { filtered = filtered.filter(i => { const lp = i.lowest_price ? parseFloat(String(i.lowest_price)) : null; const tp = i.target_price ? parseFloat(String(i.target_price)) : null; return lp && tp && lp <= tp; }); } // Sort const sorted = [...filtered].sort((a, b) => { const aPrice = a.lowest_price ? parseFloat(String(a.lowest_price)) : Infinity; const bPrice = b.lowest_price ? parseFloat(String(b.lowest_price)) : Infinity; const aOrig = a.original_price ? parseFloat(String(a.original_price)) : aPrice; const bOrig = b.original_price ? parseFloat(String(b.original_price)) : bPrice; const aDiscount = aOrig > 0 ? (aOrig - aPrice) / aOrig : 0; const bDiscount = bOrig > 0 ? (bOrig - bPrice) / bOrig : 0; switch (sortBy) { case 'price_low': return aPrice - bPrice; case 'price_high': return bPrice - aPrice; case 'discount': return bDiscount - aDiscount; case 'price_drop': { const aDelta = (a.price_7d_ago && a.lowest_price) ? parseFloat(String(a.lowest_price)) - parseFloat(String(a.price_7d_ago)) : 0; const bDelta = (b.price_7d_ago && b.lowest_price) ? parseFloat(String(b.lowest_price)) - parseFloat(String(b.price_7d_ago)) : 0; return aDelta - bDelta; // Most negative (biggest drop) first } default: return 0; // 'newest' keeps API order (created_at DESC) } }); return (
{/* Push activation banner */} {!pushEnabled && 'PushManager' in window && (

Ontvang direct een browsermelding bij een prijsdaling — geen e-mail nodig.

)} {/* Sort & Filter controls */}

{filtered.length} sneaker{filtered.length !== 1 ? 's' : ''}

{brands.length > 1 && ( )}
{sorted.length === 0 ? (

Geen sneakers gevonden met deze filters

) : ( sorted.map(item => ( )) )}
); }