diff --git a/App.tsx b/App.tsx index 24779b3..fd3b9d4 100644 --- a/App.tsx +++ b/App.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { HashRouter, Routes, Route, Link, Navigate } from 'react-router-dom'; -import { Terminal, Flag, Trophy, Newspaper, Shield, Settings, LogOut, X } from 'lucide-react'; +import { Terminal, Flag, Trophy, Newspaper, Shield, Settings, LogOut, X, History } from 'lucide-react'; import { CTFProvider, useCTF } from './CTFContext'; import { ProtectedRoute, Button, Countdown } from './UIComponents'; import { Home } from './Home'; @@ -10,6 +10,7 @@ import { Blog } from './Blog'; import { Scoreboard, ScoreMatrix } from './Scoreboard'; import { Admin } from './Admin'; import { Login, Register } from './Auth'; +import { Log } from './Log'; const ProfileSettingsModal: React.FC<{ onClose: () => void }> = ({ onClose }) => { const { updateProfile } = useCTF(); @@ -73,6 +74,7 @@ const LayoutShell: React.FC = () => { Challenges Blog Scoreboard + {currentUser ? Log : null} {currentUser?.isAdmin ? Admin : null} {isEventLive && ( @@ -98,6 +100,7 @@ const LayoutShell: React.FC = () => { } /> } /> } /> + } /> } /> {currentUser?.isAdmin ? : }} /> } /> diff --git a/Log.tsx b/Log.tsx new file mode 100644 index 0000000..11accd8 --- /dev/null +++ b/Log.tsx @@ -0,0 +1,76 @@ +import React, { useMemo } from 'react'; +import { Medal, CheckCircle2, History } from 'lucide-react'; +import { useCTF } from './CTFContext'; +import { calculateChallengeValue, getFirstBloodBonusFactor } from './services/scoring'; + +export const Log: React.FC = () => { + const { state } = useCTF(); + + const sortedSolves = useMemo(() => { + return [...state.solves].sort((a, b) => b.timestamp - a.timestamp); + }, [state.solves]); + + return ( +
+
+ +

Event Log

+
+
+ {sortedSolves.length === 0 ? ( +
+ No activities recorded yet. +
+ ) : ( + sortedSolves.map((solve, idx) => { + const team = state.teams.find(t => t.id === solve.teamId); + const challenge = state.challenges.find(c => c.id === solve.challengeId); + + if (!team || !challenge) return null; + + const challengeSolves = state.solves + .filter(s => s.challengeId === solve.challengeId) + .sort((a, b) => a.timestamp - b.timestamp); + + const rank = challengeSolves.findIndex(s => s.teamId === solve.teamId); + const bonusFactor = getFirstBloodBonusFactor(rank); + const basePoints = calculateChallengeValue( + challenge.initialPoints, + challenge.minimumPoints || 0, + challenge.decaySolves || 1, + challengeSolves.length + ); + const bonus = Math.floor(challenge.initialPoints * bonusFactor); + const pointsGained = basePoints + bonus; + + const getRankIcon = (rank: number) => { + if (rank === 0) return ; // Gold + if (rank === 1) return ; // Silver + if (rank === 2) return ; // Bronze + return ; // Checkmark + }; + + const difficultyColor = + challenge.difficulty === 'Low' ? 'text-[#00ff00]' : + challenge.difficulty === 'Medium' ? 'text-[#ffaa00]' : + 'text-[#ff0000]'; + + const dateStr = new Date(solve.timestamp).toLocaleString(); + + return ( +
+
+ {getRankIcon(rank)} +
+
+ [{dateStr}] + {team.name} solved {challenge.category} - {challenge.title} and gained {pointsGained} points. +
+
+ ); + }) + )} +
+
+ ); +};