Files
flashcards/packages/frontend/src/pages/Marketplace.tsx

98 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { motion } from 'framer-motion';
import { marketplaceApi, type MarketplaceListResponse } from '../api/marketplace.js';
import { lessonsApi } from '../api/lessons.js';
import { ApiClientError } from '../api/client.js';
export function MarketplacePage() {
const [data, setData] = useState<MarketplaceListResponse>({ rows: [], total: 0 });
const [q, setQ] = useState('');
const [curatedOnly, setCuratedOnly] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const navigate = useNavigate();
async function refresh() {
setBusy(true); setError(null);
try {
const r = await marketplaceApi.list({ q: q.trim() || undefined, curated: curatedOnly ? true : undefined });
setData(r);
} catch (e) {
setError(e instanceof ApiClientError ? e.message : 'Kon marketplace niet laden.');
} finally { setBusy(false); }
}
useEffect(() => { refresh(); }, [curatedOnly]);
async function subscribe(id: number) {
try { await lessonsApi.subscribe(id); await refresh(); }
catch (e) { alert(e instanceof ApiClientError ? e.message : 'Abonneren mislukt'); }
}
async function fork(id: number) {
try {
const f = await lessonsApi.fork(id);
navigate(`/lessons/${f.id}`);
}
catch (e) { alert(e instanceof ApiClientError ? e.message : 'Forken mislukt'); }
}
return (
<div className="space-y-6">
<header>
<h1 className="font-display text-3xl font-bold">Marketplace 🛍</h1>
<p className="text-sm text-slate-500">Vind trainingen van andere gebruikers en officiële beheerderscontent.</p>
</header>
<div className="surface flex flex-col gap-2 p-4 sm:flex-row">
<input
className="input-field flex-1"
placeholder="Zoek op naam of beschrijving…"
value={q}
onChange={(e) => setQ(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && refresh()}
/>
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" className="h-4 w-4 rounded accent-brand-600" checked={curatedOnly} onChange={(e) => setCuratedOnly(e.target.checked)} />
Alleen officieel
</label>
<button className="btn-primary shrink-0" onClick={refresh} disabled={busy}>Zoek</button>
</div>
{error && <p className="text-sm text-danger-700">{error}</p>}
{data.rows.length === 0 && !busy ? (
<div className="surface p-12 text-center text-slate-500">Geen lessen gevonden.</div>
) : (
<ul className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{data.rows.map((l, i) => (
<motion.li
key={l.id}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.02 }}
className="surface flex flex-col p-5"
>
<div className="mb-2 flex items-center gap-2">
{l.isCurated && <span className="rounded-full bg-yellow-100 px-2 py-0.5 text-xs font-semibold text-yellow-800"> Curated</span>}
{l.isFork && <span className="rounded-full bg-slate-100 px-2 py-0.5 text-xs text-slate-600 dark:bg-slate-800 dark:text-slate-300">🍴 Fork</span>}
</div>
<h2 className="font-display text-lg font-bold">{l.name}</h2>
<p className="mt-1 line-clamp-2 text-sm text-slate-500">{l.description ?? <span className="italic">geen beschrijving</span>}</p>
<div className="mt-3 flex items-center justify-between text-xs text-slate-500">
<span>door {l.ownerDisplayName}</span>
<span>{l.totalCards} kaarten · {l.subscribersCount} abonnees</span>
</div>
<div className="mt-4 flex gap-2">
<button className="btn-primary flex-1" onClick={() => subscribe(l.id)}>Abonneer</button>
<button className="btn-ghost flex-1" onClick={() => fork(l.id)}>🍴 Fork</button>
</div>
</motion.li>
))}
</ul>
)}
</div>
);
}