feat(frontend): lesson badges + marketplace nav link

This commit is contained in:
2026-05-21 00:34:31 +02:00
parent 01f7df3735
commit 7eabd667ce
2 changed files with 55 additions and 50 deletions

View File

@@ -6,6 +6,7 @@ import { UserMenu } from './UserMenu.js';
const navItems = [ const navItems = [
{ to: '/', label: 'Dashboard', end: true }, { to: '/', label: 'Dashboard', end: true },
{ to: '/admin', label: 'Lessen' }, { to: '/admin', label: 'Lessen' },
{ to: '/marketplace', label: 'Marketplace 🛍️' },
{ to: '/stats', label: 'Stats' }, { to: '/stats', label: 'Stats' },
]; ];

View File

@@ -3,9 +3,11 @@ import { Link } from 'react-router-dom';
import type { LessonTreeNode } from '@flashcard/shared'; import type { LessonTreeNode } from '@flashcard/shared';
import { lessonsApi } from '../api/lessons.js'; import { lessonsApi } from '../api/lessons.js';
import { useLessons } from '../stores/lessonsStore.js'; import { useLessons } from '../stores/lessonsStore.js';
import { useAuth } from '../stores/authStore.js';
export function LessonTree({ nodes, depth = 0 }: { nodes: LessonTreeNode[]; depth?: number }) { export function LessonTree({ nodes, depth = 0 }: { nodes: LessonTreeNode[]; depth?: number }) {
const refresh = useLessons((s) => s.refresh); const refresh = useLessons((s) => s.refresh);
const currentUserId = useAuth((s) => s.user?.id);
const [addingTo, setAddingTo] = useState<number | null>(null); const [addingTo, setAddingTo] = useState<number | null>(null);
const [name, setName] = useState(''); const [name, setName] = useState('');
@@ -32,57 +34,59 @@ export function LessonTree({ nodes, depth = 0 }: { nodes: LessonTreeNode[]; dept
return ( return (
<ul className="space-y-1"> <ul className="space-y-1">
{nodes.map((n) => ( {nodes.map((n) => {
<li key={n.id} style={{ paddingLeft: depth * 20 }}> const isOwner = n.ownerId === currentUserId;
<div className="group flex items-center gap-2 rounded-2xl px-3 py-2 transition hover:bg-brand-50/70 dark:hover:bg-slate-800/60"> const visibilityBadge =
<span className={`h-2 w-2 rounded-full ${depth === 0 ? 'bg-brand-500' : 'bg-brand-300'}`} /> n.isCurated ? '⭐ Curated'
<Link to={`/admin/lessons/${n.id}`} className="flex-1 truncate font-medium text-slate-800 dark:text-slate-100"> : n.visibility === 'shared' ? '🌍 Gedeeld'
{n.name} : '🔒 Privé';
<span className="ml-2 rounded-full bg-brand-100 px-2 py-0.5 text-xs font-semibold text-brand-700 dark:bg-brand-900/30 dark:text-brand-200"> return (
{n.cardCount} <li key={n.id} style={{ paddingLeft: depth * 20 }}>
</span> <div className="group flex items-center gap-2 rounded-2xl px-3 py-2 transition hover:bg-brand-50/70 dark:hover:bg-slate-800/60">
</Link> <span className={`h-2 w-2 rounded-full ${depth === 0 ? 'bg-brand-500' : 'bg-brand-300'}`} />
<div className="flex items-center gap-1 opacity-0 transition group-hover:opacity-100"> <Link to={`/admin/lessons/${n.id}`} className="flex-1 truncate font-medium text-slate-800 dark:text-slate-100">
<button {n.name}
className="rounded-lg px-2 py-1 text-xs font-medium text-brand-700 hover:bg-brand-100 dark:text-brand-200 dark:hover:bg-brand-900/40" <span className="ml-2 rounded-full bg-brand-100 px-2 py-0.5 text-xs font-semibold text-brand-700 dark:bg-brand-900/30 dark:text-brand-200">
onClick={() => setAddingTo(n.id)} {n.cardCount}
> </span>
+ subles <span className="ml-2 rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-semibold text-slate-600 dark:bg-slate-800 dark:text-slate-300">
</button> {visibilityBadge}
<button </span>
className="rounded-lg px-2 py-1 text-xs font-medium text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800" {!isOwner && (
onClick={() => rename(n.id, n.name)} <span className="ml-1 rounded-full bg-amber-50 px-2 py-0.5 text-[10px] font-semibold text-amber-700 dark:bg-amber-900/30 dark:text-amber-200">
> 📥 Geabonneerd
rename </span>
</button> )}
<button </Link>
className="rounded-lg px-2 py-1 text-xs font-medium text-danger-600 hover:bg-danger-50 dark:hover:bg-danger-400/10" {isOwner && (
onClick={() => remove(n.id)} <div className="flex items-center gap-1 opacity-0 transition group-hover:opacity-100">
> <button className="rounded-lg px-2 py-1 text-xs font-medium text-brand-700 hover:bg-brand-100 dark:text-brand-200 dark:hover:bg-brand-900/40" onClick={() => setAddingTo(n.id)}>+ subles</button>
delete <button className="rounded-lg px-2 py-1 text-xs font-medium text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800" onClick={() => rename(n.id, n.name)}>rename</button>
</button> <button className="rounded-lg px-2 py-1 text-xs font-medium text-danger-600 hover:bg-danger-50 dark:hover:bg-danger-400/10" onClick={() => remove(n.id)}>delete</button>
</div>
)}
</div> </div>
</div> {addingTo === n.id && (
{addingTo === n.id && ( <div className="ml-6 mt-1 flex gap-2">
<div className="ml-6 mt-1 flex gap-2"> <input
<input autoFocus
autoFocus className="input-field flex-1"
className="input-field flex-1" value={name}
value={name} onChange={(e) => setName(e.target.value)}
onChange={(e) => setName(e.target.value)} onKeyDown={(e) => {
onKeyDown={(e) => { if (e.key === 'Enter') addChild(n.id);
if (e.key === 'Enter') addChild(n.id); if (e.key === 'Escape') { setAddingTo(null); setName(''); }
if (e.key === 'Escape') { setAddingTo(null); setName(''); } }}
}} placeholder="Naam van subles"
placeholder="Naam van subles" />
/> <button className="btn-primary px-3" onClick={() => addChild(n.id)}>Toevoegen</button>
<button className="btn-primary px-3" onClick={() => addChild(n.id)}>Toevoegen</button> <button className="btn-ghost px-3" onClick={() => { setAddingTo(null); setName(''); }}>Annuleren</button>
<button className="btn-ghost px-3" onClick={() => { setAddingTo(null); setName(''); }}>Annuleren</button> </div>
</div> )}
)} {n.children.length > 0 && <LessonTree nodes={n.children} depth={depth + 1} />}
{n.children.length > 0 && <LessonTree nodes={n.children} depth={depth + 1} />} </li>
</li> );
))} })}
</ul> </ul>
); );
} }