diff --git a/src/App.jsx b/src/App.jsx index 0a3abe4..06f1378 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -9,9 +9,7 @@ import Dashboard from './pages/Dashboard' import Admin from './pages/Admin' import Leren from './pages/Leren' import Testen from './pages/Testen' - -// Placeholder components for routing structure -const Leaderboard = () =>

Leaderboard

See who is on top!

+import Leaderboard from './pages/Leaderboard' // Protected Route Wrapper const ProtectedRoute = ({ children, requireAdmin }) => { diff --git a/src/pages/Leaderboard.jsx b/src/pages/Leaderboard.jsx new file mode 100644 index 0000000..2940aac --- /dev/null +++ b/src/pages/Leaderboard.jsx @@ -0,0 +1,170 @@ +import React, { useState, useEffect } from 'react'; +import { Trophy, Medal, Award, TrendingUp, Star } from 'lucide-react'; +import { motion } from 'framer-motion'; +import Card from '../components/ui/Card'; +import Tag from '../components/ui/Tag'; +import { useApp } from '../store/AppContext'; +import { storage } from '../lib/storage'; + +const BADGE_RULES = [ + { id: 'first_test', icon: Star, label: 'First Steps', condition: (u) => u.testsCompleted > 0 }, + { id: 'perfectionist', icon: Award, label: 'Perfectionist', condition: (u) => u.perfectScores > 0 }, + { id: 'veteran', icon: Medal, label: 'Veteran', condition: (u) => u.testsCompleted >= 5 }, +]; + +const Leaderboard = () => { + const { state } = useApp(); + const [board, setBoard] = useState([]); + + useEffect(() => { + // Re-evaluate badges before displaying + let data = storage.get('leaderboard:current', []); + + // Supplement with users that haven't scored yet + const allUsers = storage.get('users:registry', []); + const userMap = new Map(data.map(u => [u.userId, u])); + + allUsers.forEach(user => { + if (!userMap.has(user.id) && user.role !== 'admin') { + data.push({ + userId: user.id, + name: user.name, + points: 0, + testsCompleted: 0, + }); + } + }); + + // Check for perfect scores by peeking at test results + data = data.map(entry => { + let perfectScores = 0; + // Loop over all possible weeks + for (let w = 1; w <= state.weekNumber; w++) { + const result = storage.get(`quiz:result:${entry.userId}:week:${w}`); + if (result && result.percentage === 100) perfectScores++; + } + return { ...entry, perfectScores }; + }); + + data.sort((a, b) => b.points - a.points); + setBoard(data); + }, [state.weekNumber]); + + return ( +
+
+ +

Company Leaderboard

+

Compete, learn, and earn badges based on your weekly knowledge tests.

+
+ + {/* Top 3 Podium */} + {board.length >= 3 && ( +
+ {/* 2nd Place */} + +
+
2
+
+
+

{board[1].name}

+

{board[1].points} pts

+
+
+ + {/* 1st Place */} + +
+
1
+
+ +
+

{board[0].name}

+

{board[0].points} pts

+
+
+ + {/* 3rd Place */} + +
+
3
+
+
+

{board[2].name}

+

{board[2].points} pts

+
+
+
+ )} + + {/* Full List */} + +
+ {board.map((user, index) => { + const earnedBadges = BADGE_RULES.filter(r => r.condition(user)); + const isMe = user.userId === state.currentUser?.id; + + return ( + + {/* Rank & Name */} +
+ + #{index + 1} + +
+

+ {user.name} + {isMe && You} +

+

+ {user.testsCompleted || 0} tests completed +

+
+
+ + {/* Points */} +
+
+ + {user.points} pts +
+
+ + {/* Badges */} +
+ {earnedBadges.length > 0 ? ( + earnedBadges.map(b => ( +
+ +
+ )) + ) : ( + No badges yet + )} +
+
+ ); + })} + + {board.length === 0 && ( +
+ No one is on the leaderboard yet. Be the first to complete a test! +
+ )} +
+
+
+ ); +}; + +export default Leaderboard;