feat: implement knowledge testing system with leaderboard, quiz generation, and PocketBase integration

This commit is contained in:
RaymondVerhoef
2026-05-14 16:53:10 +02:00
parent 42d7209773
commit 74ba5d3dc0
15 changed files with 590 additions and 512 deletions

View File

@@ -4,7 +4,8 @@ import Card from '../ui/Card';
import Button from '../ui/Button';
import Input from '../ui/Input';
import Tag from '../ui/Tag';
import { storage } from '../../lib/storage';
import * as db from '../../lib/db';
import { pb } from '../../lib/pb';
import { useApp } from '../../store/AppContext';
const TeamManager = () => {
@@ -12,44 +13,32 @@ const TeamManager = () => {
const [users, setUsers] = useState([]);
const [isEditing, setIsEditing] = useState(false);
const [editingId, setEditingId] = useState(null);
const [formData, setFormData] = useState({ name: '', role: 'user', pin: '' });
const [message, setMessage] = useState('');
const loadUsers = () => {
setUsers(storage.get('users:registry', []));
const loadUsers = async () => {
const members = await db.getTeamMembers();
setUsers(members);
};
useEffect(() => { loadUsers(); }, []);
const handleSave = (e) => {
const handleSave = async (e) => {
e.preventDefault();
if (!formData.name || !formData.pin) return;
let updatedUsers = [...users];
if (editingId) {
updatedUsers = updatedUsers.map(u => u.id === editingId ? { ...u, ...formData } : u);
await db.updateTeamMember(editingId, { name: formData.name, role: formData.role, pin: formData.pin });
setMessage('User updated successfully.');
} else {
const newUser = {
id: `u_${Date.now()}`,
name: formData.name,
role: formData.role,
pin: formData.pin,
registeredAt: new Date().toISOString()
};
updatedUsers.push(newUser);
await db.addTeamMember({ name: formData.name, role: formData.role, pin: formData.pin });
setMessage('User added successfully.');
}
storage.set('users:registry', updatedUsers);
setUsers(updatedUsers);
await loadUsers();
setFormData({ name: '', role: 'user', pin: '' });
setIsEditing(false);
setEditingId(null);
// In a real app we'd trigger a global state update, but here the user has to refresh or re-login if they edit themselves.
setTimeout(() => setMessage(''), 3000);
};
@@ -59,20 +48,21 @@ const TeamManager = () => {
setEditingId(user.id);
};
const handleDelete = (id) => {
const handleDelete = async (id) => {
if (id === state.currentUser?.id) {
alert("You cannot delete yourself.");
return;
}
if (confirm("Are you sure you want to delete this user?")) {
const updated = users.filter(u => u.id !== id);
storage.set('users:registry', updated);
setUsers(updated);
// Also remove them from leaderboard
const lb = storage.get('leaderboard:current', []);
storage.set('leaderboard:current', lb.filter(e => e.userId !== id));
await db.deleteTeamMember(id);
// Also remove from leaderboard
try {
const entry = await pb.collection('leaderboard').getFirstListItem(`user_id="${id}"`);
await pb.collection('leaderboard').delete(entry.id);
} catch { /* no leaderboard entry, nothing to do */ }
await loadUsers();
setMessage('User deleted.');
setTimeout(() => setMessage(''), 3000);
}
@@ -92,27 +82,26 @@ const TeamManager = () => {
</div>
)}
{/* Form */}
<Card className="border border-bg-warm">
<h2 className="text-xl font-bold flex items-center gap-2 mb-4">
{isEditing ? <Edit2 size={20} className="text-teal" /> : <UserPlus size={20} className="text-teal" />}
{isEditing ? 'Edit Team Member' : 'Add New Team Member'}
</h2>
<form onSubmit={handleSave} className="flex flex-col sm:flex-row gap-4 items-end">
<div className="flex-1 w-full">
<Input
label="Full Name"
placeholder="e.g. Jane Doe"
value={formData.name}
onChange={e => setFormData({...formData, name: e.target.value})}
required
<Input
label="Full Name"
placeholder="e.g. Jane Doe"
value={formData.name}
onChange={e => setFormData({...formData, name: e.target.value})}
required
/>
</div>
<div className="flex-1 w-full">
<label className="block text-sm font-medium mb-1.5 text-fg">Role</label>
<select
value={formData.role}
<select
value={formData.role}
onChange={e => setFormData({...formData, role: e.target.value})}
className="w-full h-11 px-3 rounded-[var(--r-sm)] border border-bg-warm bg-paper text-sm focus:outline-none focus:ring-2 focus:ring-teal/30 focus:border-teal"
>
@@ -121,13 +110,13 @@ const TeamManager = () => {
</select>
</div>
<div className="flex-1 w-full">
<Input
label="Login PIN"
type="text"
placeholder="e.g. 1234"
value={formData.pin}
onChange={e => setFormData({...formData, pin: e.target.value})}
required
<Input
label="Login PIN"
type="text"
placeholder="e.g. 1234"
value={formData.pin}
onChange={e => setFormData({...formData, pin: e.target.value})}
required
/>
</div>
<div className="flex gap-2 w-full sm:w-auto">
@@ -143,7 +132,6 @@ const TeamManager = () => {
</form>
</Card>
{/* List */}
<Card className="p-0 border border-bg-warm overflow-hidden">
<div className="divide-y divide-bg-warm">
{users.map(user => (
@@ -160,14 +148,14 @@ const TeamManager = () => {
<p className="text-sm text-fg-muted">PIN: {user.pin}</p>
</div>
</div>
<div className="flex items-center gap-3">
<Tag variant={user.role === 'admin' ? 'dark' : 'default'} className="hidden sm:inline-flex">{user.role}</Tag>
<button onClick={() => handleEdit(user)} className="p-2 text-fg-muted hover:text-teal transition-colors">
<Edit2 size={16} />
</button>
<button
onClick={() => handleDelete(user.id)}
<button
onClick={() => handleDelete(user.id)}
disabled={user.id === state.currentUser?.id}
className="p-2 text-fg-muted hover:text-red-500 disabled:opacity-30 transition-colors"
>