feat: implement AI-driven learning content generation service and interactive leaderboard functionality
This commit is contained in:
@@ -5,10 +5,38 @@ import Card from '../components/ui/Card';
|
||||
import Button from '../components/ui/Button';
|
||||
import Tag from '../components/ui/Tag';
|
||||
|
||||
import { storage } from '../lib/storage';
|
||||
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 leaderboard = storage.get('leaderboard:current', []).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;
|
||||
|
||||
// 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 });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 md:p-10 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<header>
|
||||
@@ -23,11 +51,13 @@ const Dashboard = () => {
|
||||
<h3 className="text-xl">Learning</h3>
|
||||
<p className="text-fg-muted text-sm mt-1">Your topic this week:</p>
|
||||
</div>
|
||||
<Tag variant="accent">To Do</Tag>
|
||||
{learnDone ? <Tag variant="success">Completed</Tag> : <Tag variant="accent">To Do</Tag>}
|
||||
</div>
|
||||
<h2 className="text-2xl mt-2 mb-6">The Role of the Product Owner</h2>
|
||||
<Link to="/learn">
|
||||
<Button className="mt-auto w-full">Start Learning Session</Button>
|
||||
<h2 className="text-2xl mt-2 mb-6">{topic ? topic.label : 'No topic assigned'}</h2>
|
||||
<Link to="/learn" className="mt-auto">
|
||||
<Button className="w-full" variant={learnDone ? 'outline' : 'primary'}>
|
||||
{learnDone ? 'Review Learning Material' : 'Start Learning Session'}
|
||||
</Button>
|
||||
</Link>
|
||||
</Card>
|
||||
|
||||
@@ -37,12 +67,20 @@ const Dashboard = () => {
|
||||
<h3 className="text-xl">Testing</h3>
|
||||
<p className="text-fg-muted text-sm mt-1">Weekly test — 10 questions</p>
|
||||
</div>
|
||||
<Tag variant="default">To Do</Tag>
|
||||
{testResult ? <Tag variant="success">Score: {testResult.percentage}%</Tag> : <Tag variant="default">To Do</Tag>}
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-center py-6 text-fg-subtle">
|
||||
Complete your learning session first
|
||||
{!learnDone ? 'Complete your learning session first' : testResult ? 'Test completed for this week' : 'Ready to test your knowledge'}
|
||||
</div>
|
||||
<Button variant="outline" className="mt-auto" disabled>Start Test</Button>
|
||||
{learnDone && !testResult ? (
|
||||
<Link to="/test" className="mt-auto">
|
||||
<Button className="w-full">Start Test</Button>
|
||||
</Link>
|
||||
) : (
|
||||
<Link to="/test" className="mt-auto">
|
||||
<Button variant="outline" className="w-full">{testResult ? 'View Results' : 'Start Test'}</Button>
|
||||
</Link>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -51,26 +89,26 @@ const Dashboard = () => {
|
||||
<h3 className="text-2xl mb-4">Recent Activity</h3>
|
||||
<Card className="p-0 overflow-hidden border border-bg-warm">
|
||||
<div className="divide-y divide-bg-warm">
|
||||
<div className="p-4 flex items-center gap-4 hover:bg-bg-warm transition-colors">
|
||||
<div className="w-10 h-10 rounded-[var(--r-org)] bg-accent-soft flex items-center justify-center text-purple-700 font-bold">
|
||||
T
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">Test completed: Information Security</p>
|
||||
<p className="text-sm text-fg-muted">Last week · Score: 90%</p>
|
||||
</div>
|
||||
<div className="ml-auto text-teal font-bold">+15 pts</div>
|
||||
</div>
|
||||
<div className="p-4 flex items-center gap-4 hover:bg-bg-warm transition-colors">
|
||||
<div className="w-10 h-10 rounded-[var(--r-org)] bg-sage flex items-center justify-center text-teal-900 font-bold">
|
||||
L
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">Learning session: Information Security</p>
|
||||
<p className="text-sm text-fg-muted">Last week</p>
|
||||
</div>
|
||||
<div className="ml-auto text-teal font-bold">+5 pts</div>
|
||||
</div>
|
||||
{activity.length === 0 ? (
|
||||
<div className="p-8 text-center text-fg-muted">No recent activity.</div>
|
||||
) : (
|
||||
activity.slice(0, 5).map((act, i) => (
|
||||
<div key={i} className="p-4 flex items-center gap-4 hover:bg-bg-warm transition-colors">
|
||||
<div className={`w-10 h-10 rounded-[var(--r-org)] flex items-center justify-center font-bold ${
|
||||
act.type === 'test' ? 'bg-accent-soft text-purple-700' : 'bg-sage text-teal-900'
|
||||
}`}>
|
||||
{act.type === 'test' ? 'T' : 'L'}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">{act.type === 'test' ? 'Test completed' : 'Learning session'}: {act.topic}</p>
|
||||
<p className="text-sm text-fg-muted">
|
||||
Week {act.week} {act.type === 'test' && `· Score: ${act.score}%`}
|
||||
</p>
|
||||
</div>
|
||||
{act.points > 0 && <div className="ml-auto text-teal font-bold">+{act.points} pts</div>}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -79,20 +117,28 @@ const Dashboard = () => {
|
||||
<h3 className="text-2xl mb-4">Mini Leaderboard</h3>
|
||||
<Card className="border border-bg-warm">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-mono font-bold text-accent">1.</span>
|
||||
<span className="font-medium">Admin</span>
|
||||
{top3.length === 0 ? (
|
||||
<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 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>
|
||||
</div>
|
||||
<Tag variant="dark">{u.points} pts</Tag>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
{myRank > 3 && (
|
||||
<div className="flex items-center justify-between pt-4 border-t border-bg-warm">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-mono font-bold text-fg-muted">{myRank}.</span>
|
||||
<span className="font-medium text-teal">You</span>
|
||||
</div>
|
||||
<Tag variant="default">{myPoints} pts</Tag>
|
||||
</div>
|
||||
<Tag variant="dark">120 pts</Tag>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-mono font-bold text-teal">2.</span>
|
||||
<span className="font-medium">{currentUser?.name}</span>
|
||||
</div>
|
||||
<Tag variant="default">85 pts</Tag>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Link to="/leaderboard">
|
||||
<Button variant="ghost" className="w-full mt-4 text-sm">View full leaderboard</Button>
|
||||
|
||||
Reference in New Issue
Block a user