- 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
This commit is contained in:
146
App.tsx
146
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<HTMLDivElement>(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: <Terminal size={28} /> };
|
||||
if (path === '/challenges') return { text: 'CHALLENGES', icon: <Flag size={28} /> };
|
||||
if (path === '/blog') return { text: 'BLOG', icon: <Newspaper size={28} /> };
|
||||
if (path === '/scoreboard' || path === '/matrix') return { text: 'SCORES', icon: <Trophy size={28} /> };
|
||||
if (path === '/admin') return { text: 'ADMIN PANEL', icon: <Settings size={28} /> };
|
||||
if (path === '/login') return { text: 'LOGIN', icon: <User size={28} /> };
|
||||
if (path === '/register') return { text: 'REGISTER', icon: <User size={28} /> };
|
||||
return { text: '', icon: null };
|
||||
};
|
||||
|
||||
const pageTitle = getPageTitle();
|
||||
|
||||
return (
|
||||
<div style={bgStyles} className="min-h-screen text-white font-mono selection:bg-[#ff0000] selection:text-black relative overflow-x-hidden">
|
||||
{loadError && <div className="fixed top-0 left-0 right-0 bg-[#ff0000] text-black font-black text-center py-1 z-[999] text-[10px] italic">{loadError}</div>}
|
||||
@@ -70,35 +101,93 @@ const LayoutShell: React.FC = () => {
|
||||
filter: `brightness(${state.config.bgBrightness || '1.0'}) contrast(${state.config.bgContrast || '1.0'})`
|
||||
}} />
|
||||
)}
|
||||
<nav className="border-b-4 border-[#333] px-6 py-4 flex justify-between items-center sticky top-0 bg-black/80 backdrop-blur-md z-[100]">
|
||||
<Link to="/" className="flex items-center gap-3 group">
|
||||
{state.config.logoData ? <img src={state.config.logoData} className="w-8 h-8 object-contain" /> : <Terminal className="text-[#ff0000]" />}
|
||||
<span className="text-2xl font-black italic uppercase group-hover:text-[#ff0000] transition-colors tracking-tighter">{state.config.conferenceName}</span>
|
||||
</Link>
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="hidden md:flex gap-8 text-[10px] font-black uppercase tracking-widest">
|
||||
{currentUser ? <Link to="/challenges" className="hover:text-[#bf00ff] flex items-center gap-2 transition-colors"><Flag size={14}/> Challenges</Link> : null}
|
||||
<Link to="/blog" className="hover:text-[#bf00ff] flex items-center gap-2 transition-colors"><Newspaper size={14}/> Blog</Link>
|
||||
{currentUser ? <Link to="/scoreboard" className="hover:text-[#bf00ff] flex items-center gap-2 transition-colors"><Trophy size={14}/> Scoreboard</Link> : null}
|
||||
{currentUser ? <Link to="/log" className="hover:text-[#bf00ff] flex items-center gap-2 transition-colors"><History size={14}/> Log</Link> : null}
|
||||
{currentUser?.isAdmin ? <Link to="/admin" className="text-[#ff0000] hover:text-white transition-colors flex items-center gap-2"><Shield size={14}/> Admin</Link> : null}
|
||||
</div>
|
||||
{isEventLive && (
|
||||
<div className="px-4 py-2 hxp-border-purple bg-[#bf00ff]/10 hidden sm:block">
|
||||
<Countdown target={eventEndTime} label="Time Left" />
|
||||
<nav className="border-b-4 border-[#333] px-6 py-4 sticky top-0 bg-black/80 backdrop-blur-md z-[100]">
|
||||
<div className="grid grid-cols-3 items-center">
|
||||
{/* Left: Branding */}
|
||||
<Link to="/" className="flex items-center gap-3 group justify-self-start">
|
||||
{state.config.logoData ? <img src={state.config.logoData} className="w-8 h-8 object-contain" /> : <Terminal className="text-[#ff0000]" />}
|
||||
<span className="text-2xl font-black italic uppercase group-hover:text-[#ff0000] transition-colors tracking-tighter truncate max-w-[200px]">{state.config.conferenceName}</span>
|
||||
</Link>
|
||||
|
||||
{/* Middle: Dynamic Page Title */}
|
||||
<div className="flex justify-center text-center">
|
||||
<div className="flex items-center gap-3 text-white">
|
||||
{pageTitle.icon && <div className="text-[#bf00ff]">{pageTitle.icon}</div>}
|
||||
<span className="text-3xl font-black italic uppercase tracking-tighter truncate">
|
||||
{pageTitle.text}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-4">
|
||||
{currentUser ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<button onClick={() => setShowProfileModal(true)} title="Profile Settings" className="p-2 border-2 border-[#bf00ff] text-[#bf00ff] hover:bg-[#bf00ff] hover:text-black transition-all"><Settings size={18} /></button>
|
||||
<button onClick={logout} title="Sign Out" className="p-2 border-2 border-[#ff0000] text-[#ff0000] hover:bg-[#ff0000] hover:text-black transition-all"><LogOut size={18} /></button>
|
||||
</div>
|
||||
|
||||
{/* Right: Menu & User */}
|
||||
<div className="flex items-center gap-6 justify-self-end">
|
||||
<div className="hidden lg:flex gap-8 text-xs font-black uppercase tracking-widest">
|
||||
{currentUser ? <Link to="/challenges" className="hover:text-[#bf00ff] flex items-center gap-2 transition-colors"><Flag size={16}/> CHALLENGES</Link> : null}
|
||||
<Link to="/blog" className="hover:text-[#bf00ff] flex items-center gap-2 transition-colors"><Newspaper size={16}/> BLOG</Link>
|
||||
{currentUser ? <Link to="/scoreboard" className="hover:text-[#bf00ff] flex items-center gap-2 transition-colors"><Trophy size={16}/> SCORES</Link> : null}
|
||||
</div>
|
||||
|
||||
{isEventLive && (
|
||||
<div className="px-4 py-2 hxp-border-purple bg-[#bf00ff]/10 hidden sm:block">
|
||||
<Countdown target={eventEndTime} label="Time Left" />
|
||||
</div>
|
||||
) : <Link to="/login"><Button variant="secondary" className="text-[10px] py-1 px-4">SIGN_IN</Button></Link>}
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-4 relative">
|
||||
{currentUser ? (
|
||||
<div className="relative" ref={menuRef}>
|
||||
<button
|
||||
onClick={() => setShowUserMenu(!showUserMenu)}
|
||||
className={`flex items-center gap-2 px-3 py-2 border-2 transition-all ${showUserMenu ? 'bg-[#bf00ff] text-black border-[#bf00ff]' : 'border-[#333] text-white hover:border-[#bf00ff]'}`}
|
||||
>
|
||||
<User size={18} />
|
||||
<span className="text-xs font-black truncate max-w-[120px] hidden sm:inline">{currentUser.name}</span>
|
||||
<ChevronDown size={14} className={`transition-transform ${showUserMenu ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
|
||||
{showUserMenu && (
|
||||
<div className="absolute right-0 mt-2 w-64 bg-black border-2 border-[#bf00ff] shadow-[0_0_20px_rgba(191,0,255,0.2)] z-[50] animate-in fade-in slide-in-from-top-2 duration-200">
|
||||
<div className="p-4 border-b border-[#333] bg-[#bf00ff]/5">
|
||||
<p className="text-xs font-black text-slate-500 uppercase tracking-widest mb-1">PLAYER</p>
|
||||
<p className="text-lg font-black text-white truncate">{currentUser.name}</p>
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<span className="text-base font-black bg-[#bf00ff] text-black px-2 py-0.5">SCORE: {userScore} PTS</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-2 flex flex-col gap-1">
|
||||
{currentUser.isAdmin && (
|
||||
<Link
|
||||
to="/admin"
|
||||
onClick={() => setShowUserMenu(false)}
|
||||
className="flex items-center gap-3 px-3 py-2 text-xs font-black uppercase text-[#ff0000] hover:bg-[#ff0000]/10 transition-colors"
|
||||
>
|
||||
<Settings size={14} /> Admin Panel
|
||||
</Link>
|
||||
)}
|
||||
<button
|
||||
onClick={() => { setShowProfileModal(true); setShowUserMenu(false); }}
|
||||
className="flex items-center gap-3 px-3 py-2 text-xs font-black uppercase text-white hover:bg-white/10 transition-colors text-left"
|
||||
>
|
||||
<Settings size={14} /> Change Password
|
||||
</button>
|
||||
<div className="h-px bg-[#333] my-1" />
|
||||
<button
|
||||
onClick={() => { logout(); setShowUserMenu(false); }}
|
||||
className="flex items-center gap-3 px-3 py-2 text-xs font-black uppercase text-red-500 hover:bg-red-500/10 transition-colors text-left"
|
||||
>
|
||||
<LogOut size={14} /> LOGOUT
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : <Link to="/login"><Button variant="secondary" className="text-[10px] py-1 px-4">SIGN_IN</Button></Link>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main className="relative z-10">
|
||||
<main className="relative">
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
@@ -106,7 +195,6 @@ const LayoutShell: React.FC = () => {
|
||||
<Route path="/challenges" element={<ProtectedRoute><ChallengeList /></ProtectedRoute>} />
|
||||
<Route path="/blog" element={<Blog />} />
|
||||
<Route path="/scoreboard" element={<ProtectedRoute><Scoreboard /></ProtectedRoute>} />
|
||||
<Route path="/log" element={<ProtectedRoute><Log /></ProtectedRoute>} />
|
||||
<Route path="/matrix" element={<ProtectedRoute><ScoreMatrix /></ProtectedRoute>} />
|
||||
<Route path="/admin" element={<ProtectedRoute>{currentUser?.isAdmin ? <Admin /> : <Navigate to="/" />}</ProtectedRoute>} />
|
||||
<Route path="*" element={<Navigate to="/" />} />
|
||||
|
||||
Reference in New Issue
Block a user