feat: implement AI-driven learning content generation service and interactive leaderboard functionality

This commit is contained in:
RaymondVerhoef
2026-05-11 20:16:56 +02:00
parent 0cf5758742
commit 2597dc751a
9 changed files with 727 additions and 176 deletions

View File

@@ -0,0 +1,185 @@
import React, { useState, useEffect } from 'react';
import { Users, UserPlus, Trash2, Edit2, Shield, User, CheckCircle } from 'lucide-react';
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 { useApp } from '../../store/AppContext';
const TeamManager = () => {
const { state } = useApp();
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', []));
};
useEffect(() => { loadUsers(); }, []);
const handleSave = (e) => {
e.preventDefault();
if (!formData.name || !formData.pin) return;
let updatedUsers = [...users];
if (editingId) {
updatedUsers = updatedUsers.map(u => u.id === editingId ? { ...u, ...formData } : u);
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);
setMessage('User added successfully.');
}
storage.set('users:registry', updatedUsers);
setUsers(updatedUsers);
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);
};
const handleEdit = (user) => {
setFormData({ name: user.name, role: user.role, pin: user.pin });
setIsEditing(true);
setEditingId(user.id);
};
const handleDelete = (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));
setMessage('User deleted.');
setTimeout(() => setMessage(''), 3000);
}
};
const cancelEdit = () => {
setIsEditing(false);
setEditingId(null);
setFormData({ name: '', role: 'user', pin: '' });
};
return (
<div className="space-y-8">
{message && (
<div className="bg-teal-50 text-teal-800 p-3 rounded-[var(--r-sm)] flex items-center gap-2 text-sm">
<CheckCircle size={16} /> {message}
</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
/>
</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}
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"
>
<option value="user">User</option>
<option value="admin">Admin</option>
</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
/>
</div>
<div className="flex gap-2 w-full sm:w-auto">
<Button type="submit" className="w-full sm:w-auto h-11 px-6">
{isEditing ? 'Update' : 'Add'}
</Button>
{isEditing && (
<Button type="button" variant="outline" onClick={cancelEdit} className="h-11 px-4">
Cancel
</Button>
)}
</div>
</form>
</Card>
{/* List */}
<Card className="p-0 border border-bg-warm overflow-hidden">
<div className="divide-y divide-bg-warm">
{users.map(user => (
<div key={user.id} className="p-4 flex items-center justify-between hover:bg-bg-warm/30 transition-colors">
<div className="flex items-center gap-4">
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${user.role === 'admin' ? 'bg-purple-100 text-purple-700' : 'bg-teal/10 text-teal'}`}>
{user.role === 'admin' ? <Shield size={18} /> : <User size={18} />}
</div>
<div>
<p className="font-medium flex items-center gap-2">
{user.name}
{user.id === state.currentUser?.id && <Tag variant="accent" className="text-[10px] py-0">You</Tag>}
</p>
<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)}
disabled={user.id === state.currentUser?.id}
className="p-2 text-fg-muted hover:text-red-500 disabled:opacity-30 transition-colors"
>
<Trash2 size={16} />
</button>
</div>
</div>
))}
</div>
</Card>
</div>
);
};
export default TeamManager;