import React, { useState, useMemo } from 'react'; import { X, CheckCircle2, Download, Globe, Sparkles, Heart, Clock, Bell } from 'lucide-react'; import { useCTF } from './CTFContext'; import { Challenge, Difficulty } from './types'; import { Button, CategoryIcon, Countdown } from './UIComponents'; import { calculateChallengeValue, getFirstBloodBonusFactor } from './services/scoring'; const ChallengeModal: React.FC<{ challenge: Challenge; onClose: () => void; }> = ({ challenge, onClose }) => { const { state, currentUser, submitFlag, refreshState } = useCTF(); const [flagInput, setFlagInput] = useState(''); const [message, setMessage] = useState<{ text: string, type: 'success' | 'error' } | null>(null); const handleFlagSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { const result = await submitFlag(challenge.id, flagInput); if (result) { setMessage({ text: 'ACCESS GRANTED ✨', type: 'success' }); setFlagInput(''); setTimeout(() => { onClose(); setMessage(null); refreshState(); }, 1500); } else setMessage({ text: 'RETRY SUGGESTED', type: 'error' }); } catch (err: any) { setMessage({ text: err.message || 'COMMUNICATION ERROR', type: 'error' }); } }; const connectionDetails = useMemo(() => { const port = Number(challenge.port); const ip = challenge.overrideIp || state.config.dockerIp || '127.0.0.1'; if (port > 0) return (challenge.connectionType || 'nc') === 'nc' ? `nc ${ip} ${port}` : `http://${ip}:${port}`; return null; }, [challenge.port, challenge.connectionType, challenge.overrideIp, state.config.dockerIp]); const basePoints = calculateChallengeValue( challenge.initialPoints, challenge.minimumPoints || 0, challenge.decaySolves || 1, (challenge.solves || []).length ); const difficultyColor = challenge.difficulty === 'Low' ? 'text-[#00ff00]' : challenge.difficulty === 'Medium' ? 'text-[#ffaa00]' : 'text-[#ff0000]'; return (

{challenge.title}

{challenge.category} | {challenge.difficulty} | {basePoints} PTS
{challenge.description}
{connectionDetails && (

Connect to:

{connectionDetails}
)} {challenge.files && challenge.files.length > 0 && (

Available Files

{challenge.files.map((file, idx) => ( {file.name} ))}
)} {currentUser ? ( !(challenge.solves || []).includes(currentUser.id) ? (
setFlagInput(e.target.value)} />
{message &&
{message.text}
}
) :
CHALLENGE_SOLVED ✨
) :

PLEASE_SIGN_IN_TO_SOLVE

}

SOLVER_LOG

{(challenge.solves || []).length > 0 ? ( state.solves.filter(s => s.challengeId === challenge.id).sort((a, b) => a.timestamp - b.timestamp).map((solve, idx) => { const team = state.teams.find(t => t.id === solve.teamId); const bonusFactor = getFirstBloodBonusFactor(idx); const bonus = Math.floor(challenge.initialPoints * bonusFactor); const totalGained = basePoints + bonus; return (
{idx + 1} {team?.name} {bonus > 0 && +{bonus} BONUS}
{totalGained} PTS {new Date(solve.timestamp).toLocaleTimeString()}
); }) ) :
No solutions discovered yet.
}
); }; export const ChallengeList: React.FC = () => { const { state, currentUser } = useCTF(); const [selectedChallenge, setSelectedChallenge] = useState(null); const [showRefreshPopup, setShowRefreshPopup] = useState(false); const difficultyWeight: Record = { 'Low': 1, 'Medium': 2, 'High': 3 }; const CATEGORIES = ['WEB', 'PWN', 'REV', 'CRY', 'MSC']; const now = Date.now(); const startTime = parseInt(state.config.eventStartTime || "0"); const endTime = parseInt(state.config.eventEndTime || (Date.now() + 86400000).toString()); const isStartedManual = state.isStarted; if (now < startTime && !currentUser?.isAdmin) { const utcStartTime = new Date(startTime).toUTCString(); return (

EVENT_STARTING_IN

setShowRefreshPopup(true)} />

Event will start {utcStartTime}

{showRefreshPopup && (

SYSTEM_READY

The event has officially commenced. Refresh your session to sync challenge data.

)}
); } if ((!isStartedManual || now > endTime) && !currentUser?.isAdmin) { return (

{now > endTime ? 'MISSION_COMPLETE' : 'BOARD_PAUSED'}

{now > endTime ? 'The operation timeframe has concluded.' : 'Waiting for HQ clearance...'}

); } return (
{CATEGORIES.map(category => { const categoryChallenges = (state.challenges || []) .filter(c => c.category === category) .sort((a, b) => difficultyWeight[a.difficulty] - difficultyWeight[b.difficulty]); if (categoryChallenges.length === 0) return null; return (

{category}

{categoryChallenges.map(c => { const isSolved = currentUser && (c.solves || []).includes(currentUser.id); const currentPoints = calculateChallengeValue( c.initialPoints, c.minimumPoints || 0, c.decaySolves || 1, (c.solves || []).length ); const diffColor = c.difficulty === 'Low' ? 'text-[#00ff00]' : c.difficulty === 'Medium' ? 'text-[#ffaa00]' : 'text-[#ff0000]'; return (
setSelectedChallenge(c)}>

{c.title}

{c.difficulty}
{isSolved && } {currentPoints}
{(c.solves || []).length}
); })}
); })}
{selectedChallenge && setSelectedChallenge(null)} />}
); };