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
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 (
);
}
// 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 */}
{sorted.length === 0 ? (
Geen sneakers gevonden met deze filters
) : (
sorted.map(item => (
))
)}
);
}