122 lines
7.1 KiB
TypeScript
122 lines
7.1 KiB
TypeScript
|
|
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 { CTFProvider, useCTF } from './CTFContext';
|
|
import { ProtectedRoute, Button, Countdown } from './UIComponents';
|
|
import { Home } from './Home';
|
|
import { ChallengeList } from './Challenges';
|
|
import { Blog } from './Blog';
|
|
import { Scoreboard, ScoreMatrix } from './Scoreboard';
|
|
import { Admin } from './Admin';
|
|
import { Login, Register } from './Auth';
|
|
|
|
const ProfileSettingsModal: React.FC<{ onClose: () => void }> = ({ onClose }) => {
|
|
const { updateProfile } = useCTF();
|
|
const [password, setPassword] = useState('');
|
|
const [confirm, setConfirm] = useState('');
|
|
const [error, setError] = useState('');
|
|
const [success, setSuccess] = useState(false);
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (password !== confirm) return setError('PASSWORD_MISMATCH');
|
|
try { await updateProfile(password); setSuccess(true); setTimeout(onClose, 2000); } catch (err: any) { setError('FAILED'); }
|
|
};
|
|
return (
|
|
<div className="fixed inset-0 bg-black/95 z-[500] flex items-center justify-center p-4">
|
|
<div className="hxp-border border-4 max-md w-full max-w-md bg-black p-8 relative">
|
|
<button onClick={onClose} className="absolute top-4 right-4 text-white"><X size={24}/></button>
|
|
<h3 className="text-3xl font-black italic text-white mb-8">IDENTITY_SETTINGS</h3>
|
|
{success ? <div className="p-8 border-[#00ff00] text-[#00ff00] font-black text-center">UPDATED_SUCCESSFULLY</div> : (
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
<input type="password" placeholder="NEW KEY" className="w-full bg-black hxp-border-purple p-4 text-white font-black" value={password} onChange={e => setPassword(e.target.value)} required />
|
|
<input type="password" placeholder="CONFIRM" className="w-full bg-black hxp-border-purple p-4 text-white font-black" value={confirm} onChange={e => setConfirm(e.target.value)} required />
|
|
{error && <p className="text-red-500 font-black italic">{error}</p>}<Button type="submit" className="w-full py-4 text-xl">Confirm Update</Button>
|
|
</form>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const LayoutShell: React.FC = () => {
|
|
const { state, currentUser, logout, loading, loadError } = useCTF();
|
|
const [showProfileModal, setShowProfileModal] = useState(false);
|
|
|
|
if (loading) return <div className="min-h-screen bg-black flex items-center justify-center text-[#ff0000] font-black italic text-4xl animate-pulse tracking-tighter uppercase">INITIALIZING_SESSION...</div>;
|
|
|
|
const now = Date.now();
|
|
const eventStartTime = parseInt(state.config.eventStartTime || "0");
|
|
const eventEndTime = parseInt(state.config.eventEndTime || (Date.now() + 86400000).toString());
|
|
const isEventLive = now >= eventStartTime && now <= eventEndTime && state.isStarted;
|
|
|
|
const bgStyles: React.CSSProperties = { backgroundColor: state.config.bgType === 'color' ? (state.config.bgColor || '#000000') : '#000000' };
|
|
|
|
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>}
|
|
{state.config.bgType === 'image' && state.config.bgImageData && (
|
|
<div className="fixed inset-0 pointer-events-none z-0" style={{
|
|
backgroundImage: `url(${state.config.bgImageData})`,
|
|
backgroundSize: 'cover', backgroundPosition: 'center', backgroundAttachment: 'fixed',
|
|
opacity: parseFloat(state.config.bgOpacity || '0.5'),
|
|
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">
|
|
<Link to="/challenges" className="hover:text-[#bf00ff] flex items-center gap-2 transition-colors"><Flag size={14}/> Challenges</Link>
|
|
<Link to="/blog" className="hover:text-[#bf00ff] flex items-center gap-2 transition-colors"><Newspaper size={14}/> Blog</Link>
|
|
<Link to="/scoreboard" className="hover:text-[#bf00ff] flex items-center gap-2 transition-colors"><Trophy size={14}/> Scoreboard</Link>
|
|
{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" />
|
|
</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>
|
|
) : <Link to="/login"><Button variant="secondary" className="text-[10px] py-1 px-4">SIGN_IN</Button></Link>}
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
<main className="relative z-10">
|
|
<Routes>
|
|
<Route path="/" element={<Home />} />
|
|
<Route path="/login" element={<Login />} />
|
|
<Route path="/register" element={<Register />} />
|
|
<Route path="/challenges" element={<ProtectedRoute><ChallengeList /></ProtectedRoute>} />
|
|
<Route path="/blog" element={<Blog />} />
|
|
<Route path="/scoreboard" element={<Scoreboard />} />
|
|
<Route path="/matrix" element={<ScoreMatrix />} />
|
|
<Route path="/admin" element={<ProtectedRoute>{currentUser?.isAdmin ? <Admin /> : <Navigate to="/" />}</ProtectedRoute>} />
|
|
<Route path="*" element={<Navigate to="/" />} />
|
|
</Routes>
|
|
</main>
|
|
{showProfileModal && <ProfileSettingsModal onClose={() => setShowProfileModal(false)} />}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const App: React.FC = () => {
|
|
return (
|
|
<CTFProvider>
|
|
<HashRouter>
|
|
<LayoutShell />
|
|
</HashRouter>
|
|
</CTFProvider>
|
|
);
|
|
};
|
|
|
|
export default App;
|