feat: implement knowledge testing system with leaderboard, quiz generation, and PocketBase integration
This commit is contained in:
@@ -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"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user