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 && (
)}
{currentUser ? (
!(challenge.solves || []).includes(currentUser.id) ? (
) :
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 (
{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)} />}
);
};