feat: implement knowledge testing system with leaderboard, quiz generation, and PocketBase integration
This commit is contained in:
@@ -1,47 +1,62 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useApp } from '../store/AppContext';
|
||||
import Card from '../components/ui/Card';
|
||||
import Button from '../components/ui/Button';
|
||||
import Tag from '../components/ui/Tag';
|
||||
|
||||
import { storage } from '../lib/storage';
|
||||
import * as db from '../lib/db';
|
||||
import { getAssignedTopic } from '../lib/learningService';
|
||||
import { getTestResult } from '../lib/testService';
|
||||
|
||||
const Dashboard = () => {
|
||||
const { state } = useApp();
|
||||
const { currentUser, weekNumber } = state;
|
||||
|
||||
const topic = getAssignedTopic(currentUser?.id, weekNumber);
|
||||
const learnDone = storage.get(`user:${currentUser?.id}:week:${weekNumber}:learn_done`, false);
|
||||
const testResult = getTestResult(currentUser?.id, weekNumber);
|
||||
|
||||
const allUsers = storage.get('users:registry', []);
|
||||
const nonAdminIds = allUsers.filter(u => u.role !== 'admin').map(u => u.id);
|
||||
|
||||
const leaderboard = storage.get('leaderboard:current', [])
|
||||
.filter(u => nonAdminIds.includes(u.userId))
|
||||
.sort((a, b) => b.points - a.points);
|
||||
|
||||
const top3 = leaderboard.slice(0, 3);
|
||||
const myRank = leaderboard.findIndex(u => u.userId === currentUser?.id) + 1;
|
||||
const myPoints = leaderboard.find(u => u.userId === currentUser?.id)?.points || 0;
|
||||
const [dashData, setDashData] = useState({
|
||||
topic: null,
|
||||
learnDone: false,
|
||||
testResult: null,
|
||||
top3: [],
|
||||
myRank: 0,
|
||||
myPoints: 0,
|
||||
activity: [],
|
||||
});
|
||||
|
||||
// Gather recent activity from past weeks
|
||||
const activity = [];
|
||||
for (let w = weekNumber; w >= Math.max(1, weekNumber - 3); w--) {
|
||||
const pastLearn = storage.get(`user:${currentUser?.id}:week:${w}:learn_done`, false);
|
||||
const pastTest = getTestResult(currentUser?.id, w);
|
||||
const pastTopic = getAssignedTopic(currentUser?.id, w);
|
||||
|
||||
if (pastTest) {
|
||||
activity.push({ type: 'test', week: w, topic: pastTopic?.label, score: pastTest.percentage, points: pastTest.pointsEarned });
|
||||
}
|
||||
if (pastLearn) {
|
||||
activity.push({ type: 'learn', week: w, topic: pastTopic?.label });
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!currentUser) return;
|
||||
|
||||
const load = async () => {
|
||||
const [topic, learnDone, testResult, allUsers, leaderboard] = await Promise.all([
|
||||
getAssignedTopic(currentUser.id, weekNumber),
|
||||
db.getLearnDone(currentUser.id, weekNumber),
|
||||
db.getQuizResult(currentUser.id, weekNumber),
|
||||
db.getTeamMembers(),
|
||||
db.getLeaderboard(),
|
||||
]);
|
||||
|
||||
const nonAdminIds = allUsers.filter(u => u.role !== 'admin').map(u => u.id);
|
||||
const filtered = leaderboard.filter(u => nonAdminIds.includes(u.user_id));
|
||||
const top3 = filtered.slice(0, 3);
|
||||
const myRank = filtered.findIndex(u => u.user_id === currentUser.id) + 1;
|
||||
const myPoints = filtered.find(u => u.user_id === currentUser.id)?.points || 0;
|
||||
|
||||
const activity = [];
|
||||
for (let w = weekNumber; w >= Math.max(1, weekNumber - 3); w--) {
|
||||
const [pastLearn, pastTest, pastTopic] = await Promise.all([
|
||||
db.getLearnDone(currentUser.id, w),
|
||||
db.getQuizResult(currentUser.id, w),
|
||||
getAssignedTopic(currentUser.id, w),
|
||||
]);
|
||||
if (pastTest) activity.push({ type: 'test', week: w, topic: pastTopic?.label, score: pastTest.percentage, points: pastTest.points_earned });
|
||||
if (pastLearn) activity.push({ type: 'learn', week: w, topic: pastTopic?.label });
|
||||
}
|
||||
|
||||
setDashData({ topic, learnDone, testResult, top3, myRank, myPoints, activity });
|
||||
};
|
||||
|
||||
load();
|
||||
}, [currentUser, weekNumber]);
|
||||
|
||||
const { topic, learnDone, testResult, top3, myRank, myPoints, activity } = dashData;
|
||||
|
||||
return (
|
||||
<div className="p-6 md:p-10 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
@@ -118,7 +133,7 @@ const Dashboard = () => {
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<h3 className="text-2xl mb-4">Mini Leaderboard</h3>
|
||||
<Card className="border border-bg-warm">
|
||||
@@ -127,10 +142,10 @@ const Dashboard = () => {
|
||||
<div className="text-center text-fg-muted py-4">No points yet</div>
|
||||
) : (
|
||||
top3.map((u, i) => (
|
||||
<div key={u.userId} className="flex items-center justify-between">
|
||||
<div key={u.user_id} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`font-mono font-bold ${i === 0 ? 'text-yellow-500' : 'text-teal'}`}>{i + 1}.</span>
|
||||
<span className="font-medium">{u.name} {u.userId === currentUser?.id && '(You)'}</span>
|
||||
<span className="font-medium">{u.name} {u.user_id === currentUser?.id && '(You)'}</span>
|
||||
</div>
|
||||
<Tag variant="dark">{u.points} pts</Tag>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user