From 0d0726478873c39cd6a69625717753bd52181b4e Mon Sep 17 00:00:00 2001 From: m0rph3us1987 Date: Wed, 11 Mar 2026 17:47:46 +0100 Subject: [PATCH] - Fixed Challenge Modal overlap issue by adjusting the main stacking context in App.tsx - Implemented "click-outside-to-close" functionality for both the Challenge Modal and User Dropdown - Added protocol-specific action buttons for challenges: "Open in new tab" for HTTP and "Copy to clipboard" for NC - Enhanced Scoreboard rankings with significantly larger, consistent font sizes (text-2xl) for better readability - Rebranded "TEAM_IDENTIFIER" to "PLAYER" and "TOTAL_POINTS" to "POINTS" across the platform (Scoreboard, Matrix, User Menu) - Updated navigation: Renamed "SCOREBOARD" to "SCORES" in the nav bar and dynamic page titles - Modernized User Dropdown menu with a dedicated "PLAYER" header and "LOGOUT" action - Improved Score Matrix and Score Graph titles for consistency with the new "Player" terminology - Added CAPTCHA human verification (svg-captcha) to Login and Registration flows for enhanced security - Optimized frontend assets by migrating Tailwind and JetBrains Mono to local hosting - Refactored Admin panel: Renamed "Operators" to "Users" and improved layout alignment --- Admin.tsx | 19 +- App.tsx | 146 +++++++++--- Auth.tsx | 124 +++++++--- Blog.tsx | 3 +- CTFContext.tsx | 24 +- Challenges.tsx | 43 +++- Home.tsx | 8 +- Log.tsx | 233 ------------------- Scoreboard.tsx | 427 ++++++++++++++++++++++------------ changelog.txt | 13 ++ index.html | 5 +- package-lock.json | 33 ++- package.json | 3 +- public/jetbrains-mono-400.ttf | Bin 0 -> 112172 bytes public/jetbrains-mono-700.ttf | Bin 0 -> 112092 bytes public/jetbrains-mono-800.ttf | Bin 0 -> 112088 bytes public/jetbrains-mono.css | 21 ++ public/tailwind.js | 83 +++++++ server.js | 44 +++- services/api.ts | 13 +- 20 files changed, 753 insertions(+), 489 deletions(-) delete mode 100644 Log.tsx create mode 100644 public/jetbrains-mono-400.ttf create mode 100644 public/jetbrains-mono-700.ttf create mode 100644 public/jetbrains-mono-800.ttf create mode 100644 public/jetbrains-mono.css create mode 100644 public/tailwind.js diff --git a/Admin.tsx b/Admin.tsx index a465c36..c6a1f30 100644 --- a/Admin.tsx +++ b/Admin.tsx @@ -23,7 +23,7 @@ export const Admin: React.FC = () => { return [...state.challenges].sort((a, b) => a.title.localeCompare(b.title)); }, [state.challenges]); - const sortedOperators = useMemo(() => { + const sortedUsers = useMemo(() => { return [...state.teams] .filter(t => t.id !== 'admin-0') .sort((a, b) => (a.name || '').localeCompare(b.name || '', undefined, { sensitivity: 'base' })); @@ -107,8 +107,7 @@ export const Admin: React.FC = () => { return (
-
-

ADMIN_CONSOLE

+
{isBeforeStart && (
@@ -256,19 +255,19 @@ export const Admin: React.FC = () => {
-

OPERATORS

+

USERS

- + - {sortedOperators.map(team => ( + {sortedUsers.map(team => ( ))} - {sortedOperators.length === 0 && ( + {sortedUsers.length === 0 && ( - + )} @@ -451,7 +450,7 @@ export const Admin: React.FC = () => {
-

OPERATOR_PROFILE

+

USER_PROFILE

{ e.preventDefault(); await updateTeam(editingTeam.id as string, { name: editingTeam.name, isDisabled: editingTeam.isDisabled, isAdmin: editingTeam.isAdmin, password: editingTeam.newPassword }); setEditingTeam(null); }} className="space-y-4"> setEditingTeam({...editingTeam, name: e.target.value})} required /> setEditingTeam({...editingTeam, newPassword: e.target.value})} /> diff --git a/App.tsx b/App.tsx index c57bcb3..6468034 100644 --- a/App.tsx +++ b/App.tsx @@ -1,7 +1,7 @@ -import React, { useState, useEffect } from 'react'; -import { HashRouter, Routes, Route, Link, Navigate } from 'react-router-dom'; -import { Terminal, Flag, Trophy, Newspaper, Shield, Settings, LogOut, X, History } from 'lucide-react'; +import React, { useState, useEffect, useRef } from 'react'; +import { HashRouter, Routes, Route, Link, Navigate, useLocation } from 'react-router-dom'; +import { Terminal, Flag, Trophy, Newspaper, Settings, LogOut, X, History, User, ChevronDown } from 'lucide-react'; import { CTFProvider, useCTF } from './CTFContext'; import { ProtectedRoute, Button, Countdown } from './UIComponents'; import { Home } from './Home'; @@ -10,7 +10,7 @@ import { Blog } from './Blog'; import { Scoreboard, ScoreMatrix } from './Scoreboard'; import { Admin } from './Admin'; import { Login, Register } from './Auth'; -import { Log } from './Log'; +import { calculateTeamTotalScore } from './services/scoring'; const ProfileSettingsModal: React.FC<{ onClose: () => void }> = ({ onClose }) => { const { updateProfile } = useCTF(); @@ -43,6 +43,21 @@ const ProfileSettingsModal: React.FC<{ onClose: () => void }> = ({ onClose }) => const LayoutShell: React.FC = () => { const { state, currentUser, logout, loading, loadError } = useCTF(); const [showProfileModal, setShowProfileModal] = useState(false); + const [showUserMenu, setShowUserMenu] = useState(false); + const menuRef = useRef(null); + const location = useLocation(); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setShowUserMenu(false); + } + }; + if (showUserMenu) { + document.body.addEventListener('mousedown', handleClickOutside); + } + return () => document.body.removeEventListener('mousedown', handleClickOutside); + }, [showUserMenu]); useEffect(() => { if (state.config && state.config.conferenceName) { @@ -59,6 +74,22 @@ const LayoutShell: React.FC = () => { const bgStyles: React.CSSProperties = { backgroundColor: state.config.bgType === 'color' ? (state.config.bgColor || '#000000') : '#000000' }; + const userScore = currentUser ? calculateTeamTotalScore(currentUser.id, state.challenges, state.solves) : 0; + + const getPageTitle = () => { + const path = location.pathname; + if (path === '/') return { text: 'HOME', icon: }; + if (path === '/challenges') return { text: 'CHALLENGES', icon: }; + if (path === '/blog') return { text: 'BLOG', icon: }; + if (path === '/scoreboard' || path === '/matrix') return { text: 'SCORES', icon: }; + if (path === '/admin') return { text: 'ADMIN PANEL', icon: }; + if (path === '/login') return { text: 'LOGIN', icon: }; + if (path === '/register') return { text: 'REGISTER', icon: }; + return { text: '', icon: null }; + }; + + const pageTitle = getPageTitle(); + return (
{loadError &&
{loadError}
} @@ -70,35 +101,93 @@ const LayoutShell: React.FC = () => { filter: `brightness(${state.config.bgBrightness || '1.0'}) contrast(${state.config.bgContrast || '1.0'})` }} /> )} -
TEAM_IDENTIFIERUSER_IDENTIFIER ROLE STATUS ACTIONS
@@ -283,7 +282,7 @@ export const Admin: React.FC = () => { title={team.isAdmin ? "Revoke Admin Privileges" : "Grant Admin Privileges"} > {team.isAdmin ? : } - {team.isAdmin ? 'ADMIN' : 'OPERATOR'} + {team.isAdmin ? 'ADMIN' : 'USER'}
@@ -304,9 +303,9 @@ export const Admin: React.FC = () => {
No registered operators detected.No registered users detected.
+ + + + + + + + + + {rankings.map((team, idx) => ( + + + + + + + ))} + +
RANKPLAYERSOLVESPOINTS
+
{idx + 1}
+
{team.name}{team.solveCount}{team.score}
+
+ ); + case 'matrix': + if (state.solves.length === 0) { + return ( +
+ +

No solves yet!

+

The matrix is currently empty. Be the first to solve a challenge!

+
+ ); + } + return ( +
+
+ + + + + {sortedChallenges.map(c => { + const diffColor = c.difficulty === 'Low' ? 'text-[#00ff00]' : c.difficulty === 'Medium' ? 'text-[#ffaa00]' : 'text-[#ff0000]'; + return ( + + ); + })} + + + + {rankings.map(team => ( + + + {sortedChallenges.map(c => { + const solve = state.solves.find(s => s.challengeId === c.id && s.teamId === team.id); + if (!solve) { + return ( + + ); + } + const challengeSolves = state.solves + .filter(s => s.challengeId === c.id) + .sort((a, b) => a.timestamp - b.timestamp); + const rank = challengeSolves.findIndex(s => s.teamId === team.id); + const baseValue = calculateChallengeValue( + c.initialPoints, + c.minimumPoints || 0, + c.decaySolves || 1, + (c.solves || []).length + ); + const bonus = Math.floor(c.initialPoints * getFirstBloodBonusFactor(rank)); + const totalPoints = baseValue + bonus; + + return ( + + ); + })} + + ))} + +
Player / Challenge +
+ + {c.title} + +
+
+ {(c.solves || []).length} SOLVES +
+
+ {team.name} ({team.score}) + +
+
+
+
+ {rank === 0 ? : rank === 1 ? : rank === 2 ? : } +
+ {totalPoints} +
+
+
+
+ ); + case 'log': + return ( +
+ {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 basePoints = calculateChallengeValue(challenge.initialPoints, challenge.minimumPoints || 0, challenge.decaySolves || 1, challengeSolves.length); + const bonus = Math.floor(challenge.initialPoints * getFirstBloodBonusFactor(rank)); + const pointsGained = basePoints + bonus; + const difficultyColor = challenge.difficulty === 'Low' ? 'text-[#00ff00]' : challenge.difficulty === 'Medium' ? 'text-[#ffaa00]' : 'text-[#ff0000]'; + + return ( +
+
+ {rank === 0 ? : rank === 1 ? : rank === 2 ? : } +
+
+ [{new Date(solve.timestamp).toLocaleString()}] + {team.name} solved {challenge.category} - {challenge.title} and gained {pointsGained} points. +
+
+ ); + }) + )} +
+ ); + case 'graph': + return ( +
+

Top 10 Players - Score Progression

+ {graphData.length === 0 ? ( +
No data to display.
+ ) : ( + + + + + + -item.value} + /> + + {top10Teams.map((team, idx) => ( + + ))} + + + )} +
+ ); + } + }; + return ( -
-
-
-

SCOREBOARD

+
+
+
+ +
); }; export const ScoreMatrix: React.FC = () => { - const { state } = useCTF(); - - const sortedTeams = useMemo(() => { - return state.teams - .filter(t => !t.isAdmin && !t.isDisabled) - .map(t => ({ ...t, score: calculateTeamTotalScore(t.id, state.challenges, state.solves) })) - .sort((a, b) => b.score - a.score); - }, [state]); - - const sortedChallenges = useMemo(() => { - return [...state.challenges].sort((a, b) => (b.solves?.length || 0) - (a.solves?.length || 0)); - }, [state.challenges]); - - if (state.solves.length === 0) { - return ( -
-
-

SCORE_MATRIX

- View_Rankings -
-
- -

No solves yet!

-

The matrix is currently empty. Be the first to solve a challenge!

- - - -
-
- ); - } - - return ( -
-
-

SCORE_MATRIX

- View_Rankings -
-
- - - - - {sortedChallenges.map(c => { - const diffColor = c.difficulty === 'Low' ? 'text-[#00ff00]' : c.difficulty === 'Medium' ? 'text-[#ffaa00]' : 'text-[#ff0000]'; - return ( - - ); - })} - - - - {sortedTeams.map(team => ( - - - {sortedChallenges.map(c => { - const solve = state.solves.find(s => s.challengeId === c.id && s.teamId === team.id); - if (!solve) { - return ( - - ); - } - - // Find rank for the medal and bonus - const challengeSolves = state.solves - .filter(s => s.challengeId === c.id) - .sort((a, b) => a.timestamp - b.timestamp); - const rank = challengeSolves.findIndex(s => s.teamId === team.id); - - // Calculate point gain for this specific solve - const baseValue = calculateChallengeValue( - c.initialPoints, - c.minimumPoints || 0, - c.decaySolves || 1, - (c.solves || []).length - ); - const bonus = Math.floor(c.initialPoints * getFirstBloodBonusFactor(rank)); - const totalPoints = baseValue + bonus; - - return ( - - ); - })} - - ))} - -
Team / Challenge - {/* Vertical text container using writing-mode so it stretches cell height automatically */} -
- - {c.title} - -
- {/* Solve count tag at the bottom of the header cell */} -
- {(c.solves || []).length} SOLVES -
-
- {team.name} ({team.score}) - -
-
-
-
- {rank === 0 ? ( - - ) : rank === 1 ? ( - - ) : rank === 2 ? ( - - ) : ( - - )} -
- {totalPoints} -
-
-
-
- ); + // Keeping this for compatibility but it's now integrated into Scoreboard main component + return ; }; diff --git a/changelog.txt b/changelog.txt index adb02c0..f3ca4c3 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,16 @@ +2026-03-11 +- Fixed Challenge Modal overlap issue by adjusting the main stacking context in App.tsx +- Implemented "click-outside-to-close" functionality for both the Challenge Modal and User Dropdown +- Added protocol-specific action buttons for challenges: "Open in new tab" for HTTP and "Copy to clipboard" for NC +- Enhanced Scoreboard rankings with significantly larger, consistent font sizes (text-2xl) for better readability +- Rebranded "TEAM_IDENTIFIER" to "PLAYER" and "TOTAL_POINTS" to "POINTS" across the platform (Scoreboard, Matrix, User Menu) +- Updated navigation: Renamed "SCOREBOARD" to "SCORES" in the nav bar and dynamic page titles +- Modernized User Dropdown menu with a dedicated "PLAYER" header and "LOGOUT" action +- Improved Score Matrix and Score Graph titles for consistency with the new "Player" terminology +- Added CAPTCHA human verification (svg-captcha) to Login and Registration flows for enhanced security +- Optimized frontend assets by migrating Tailwind and JetBrains Mono to local hosting +- Refactored Admin panel: Renamed "Operators" to "Users" and improved layout alignment + 2026-03-10 - Added "HW" (Hardware) category to the platform with a dedicated icon and color - Updated challenge grid to 6 columns on desktop to accommodate the new category diff --git a/index.html b/index.html index 9c19251..ac528a1 100644 --- a/index.html +++ b/index.html @@ -5,8 +5,8 @@ HIPCTF - - + +