import React, { useState, useEffect } from 'react'; import { Trophy, Users, Target, Clock, Plus, Edit3, Save, X, Medal } from 'lucide-react'; const BoulderingCompetitionSystem = () => { const [competitors, setCompetitors] = useState([]); const [categories, setCategories] = useState([ { id: 1, name: 'Men Open', ageMin: 18, ageMax: 99 }, { id: 2, name: 'Women Open', ageMin: 18, ageMax: 99 }, { id: 3, name: 'Men Youth A', ageMin: 16, ageMax: 17 }, { id: 4, name: 'Women Youth A', ageMin: 16, ageMax: 17 }, ]); const [currentRound, setCurrentRound] = useState('Qualifiers'); const [activeTab, setActiveTab] = useState('scoreboard'); const [editingCompetitor, setEditingCompetitor] = useState(null); const [newCompetitor, setNewCompetitor] = useState({ name: '', categoryId: 1, bibNumber: '' }); const rounds = ['Qualifiers', 'Semi-Finals', 'Finals']; const problems = { 'Qualifiers': [ { id: 'Q1', points: 1000, zone: 500 }, { id: 'Q2', points: 1000, zone: 500 }, { id: 'Q3', points: 1000, zone: 500 }, { id: 'Q4', points: 1000, zone: 500 }, { id: 'Q5', points: 1000, zone: 500 } ], 'Semi-Finals': [ { id: 'SF1', points: 1000, zone: 500 }, { id: 'SF2', points: 1000, zone: 500 }, { id: 'SF3', points: 1000, zone: 500 }, { id: 'SF4', points: 1000, zone: 500 } ], 'Finals': [ { id: 'F1', points: 1000, zone: 500 }, { id: 'F2', points: 1000, zone: 500 }, { id: 'F3', points: 1000, zone: 500 }, { id: 'F4', points: 1000, zone: 500 } ] }; const addCompetitor = () => { if (newCompetitor.name && newCompetitor.bibNumber) { const competitor = { id: Date.now(), name: newCompetitor.name, categoryId: parseInt(newCompetitor.categoryId), bibNumber: newCompetitor.bibNumber, scores: initializeScores() }; setCompetitors([...competitors, competitor]); setNewCompetitor({ name: '', categoryId: 1, bibNumber: '' }); } }; const initializeScores = () => { const scores = {}; rounds.forEach(round => { scores[round] = {}; problems[round].forEach(problem => { scores[round][problem.id] = { top: false, zone: false, attempts: 0 }; }); }); return scores; }; const updateScore = (competitorId, round, problemId, field, value) => { setCompetitors(competitors.map(competitor => { if (competitor.id === competitorId) { const newScores = { ...competitor.scores }; newScores[round][problemId] = { ...newScores[round][problemId], [field]: value }; // Auto-set zone to true if top is achieved if (field === 'top' && value) { newScores[round][problemId].zone = true; } return { ...competitor, scores: newScores }; } return competitor; })); }; const calculateRoundScore = (competitor, round) => { if (!competitor.scores[round]) return { total: 0, tops: 0, zones: 0 }; let total = 0; let tops = 0; let zones = 0; problems[round].forEach(problem => { const score = competitor.scores[round][problem.id]; if (score.top) { total += problem.points; tops++; zones++; // Top includes zone } else if (score.zone) { total += problem.zone; zones++; } }); return { total, tops, zones }; }; const getSortedCompetitors = (categoryId) => { return competitors .filter(c => c.categoryId === categoryId) .map(competitor => ({ ...competitor, roundScore: calculateRoundScore(competitor, currentRound) })) .sort((a, b) => { // Sort by total score, then by tops, then by zones if (b.roundScore.total !== a.roundScore.total) { return b.roundScore.total - a.roundScore.total; } if (b.roundScore.tops !== a.roundScore.tops) { return b.roundScore.tops - a.roundScore.tops; } return b.roundScore.zones - a.roundScore.zones; }); }; const getRankBadge = (position) => { if (position === 1) return <Medal className="w-4 h-4 text-yellow-500" />; if (position === 2) return <Medal className="w-4 h-4 text-gray-400" />; if (position === 3) return <Medal className="w-4 h-4 text-amber-600" />; return <span className="w-4 h-4 flex items-center justify-center text-xs font-bold text-gray-600">#{position}</span>; }; const ScoreboardView = () => ( <div className="space-y-6"> <div className="bg-gradient-to-r from-blue-600 to-purple-600 text-white p-6 rounded-xl"> <div className="flex items-center justify-between"> <div> <h2 className="text-2xl font-bold flex items-center gap-2"> <Trophy className="w-6 h-6" /> Live Scoreboard - {currentRound} </h2> <p className="text-blue-100 mt-1">Real-time competition results</p> </div> <div className="flex gap-2"> {rounds.map(round => ( <button key={round} onClick={() => setCurrentRound(round)} className={`px-4 py-2 rounded-lg transition-colors ${ currentRound === round ? 'bg-white text-blue-600 font-semibold' : 'bg-blue-700 hover:bg-blue-600 text-white' }`} > {round} </button> ))} </div> </div> </div> {categories.map(category => { const categoryCompetitors = getSortedCompetitors(category.id); if (categoryCompetitors.length === 0) return null; return ( <div key={category.id} className="bg-white rounded-xl shadow-lg overflow-hidden"> <div className="bg-gray-50 px-6 py-4 border-b"> <h3 className="text-xl font-bold text-gray-800">{category.name}</h3> <p className="text-gray-600">{categoryCompetitors.length} competitors</p> </div> <div className="overflow-x-auto"> <table className="w-full"> <thead className="bg-gray-100"> <tr> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Rank</th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Bib</th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Competitor</th> <th className="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Score</th> <th className="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Tops</th> <th className="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Zones</th> {problems[currentRound].map(problem => ( <th key={problem.id} className="px-3 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider"> {problem.id} </th> ))} </tr> </thead> <tbody className="bg-white divide-y divide-gray-200"> {categoryCompetitors.map((competitor, index) => ( <tr key={competitor.id} className={`hover:bg-gray-50 ${index < 3 ? 'bg-gradient-to-r from-yellow-50 to-orange-50' : ''}`}> <td className="px-6 py-4 whitespace-nowrap"> <div className="flex items-center"> {getRankBadge(index + 1)} </div> </td> <td className="px-6 py-4 whitespace-nowrap"> <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"> {competitor.bibNumber} </span> </td> <td className="px-6 py-4 whitespace-nowrap"> <div className="text-sm font-medium text-gray-900">{competitor.name}</div> </td> <td className="px-6 py-4 whitespace-nowrap text-center"> <span className="text-lg font-bold text-gray-900">{competitor.roundScore.total}</span> </td> <td className="px-6 py-4 whitespace-nowrap text-center"> <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"> {competitor.roundScore.tops} </span> </td> <td className="px-6 py-4 whitespace-nowrap text-center"> <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800"> {competitor.roundScore.zones} </span> </td> {problems[currentRound].map(problem => { const score = competitor.scores[currentRound][problem.id]; return ( <td key={problem.id} className="px-3 py-4 whitespace-nowrap text-center"> <div className="flex flex-col items-center space-y-1"> {score.top ? ( <span className="w-6 h-6 bg-green-500 text-white rounded-full flex items-center justify-center text-xs font-bold">T</span> ) : score.zone ? ( <span className="w-6 h-6 bg-yellow-500 text-white rounded-full flex items-center justify-center text-xs font-bold">Z</span> ) : ( <span className="w-6 h-6 bg-gray-300 rounded-full flex items-center justify-center text-xs">-</span> )} </div> </td> ); })} </tr> ))} </tbody> </table> </div> </div> ); })} </div> ); const ScoringView = () => ( <div className="space-y-6"> <div className="bg-gradient-to-r from-green-600 to-teal-600 text-white p-6 rounded-xl"> <h2 className="text-2xl font-bold flex items-center gap-2"> <Target className="w-6 h-6" /> Score Entry - {currentRound} </h2> <p className="text-green-100 mt-1">Update competitor scores</p> </div> <div className="flex gap-4 mb-6"> {rounds.map(round => ( <button key={round} onClick={() => setCurrentRound(round)} className={`px-4 py-2 rounded-lg transition-colors ${ currentRound === round ? 'bg-green-600 text-white' : 'bg-gray-200 text-gray-700 hover:bg-gray-300' }`} > {round} </button> ))} </div> {categories.map(category => { const categoryCompetitors = competitors.filter(c => c.categoryId === category.id); if (categoryCompetitors.length === 0) return null; return ( <div key={category.id} className="bg-white rounded-xl shadow-lg overflow-hidden"> <div className="bg-gray-50 px-6 py-4 border-b"> <h3 className="text-xl font-bold text-gray-800">{category.name}</h3> </div> <div className="overflow-x-auto"> <table className="w-full"> <thead className="bg-gray-100"> <tr> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Competitor</th> {problems[currentRound].map(problem => ( <th key={problem.id} className="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider"> <div>{problem.id}</div> <div className="text-xs text-gray-400">{problem.points}pts</div> </th> ))} </tr> </thead> <tbody className="bg-white divide-y divide-gray-200"> {categoryCompetitors.map(competitor => ( <tr key={competitor.id}> <td className="px-6 py-4"> <div className="flex items-center space-x-3"> <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"> {competitor.bibNumber} </span> <span className="font-medium text-gray-900">{competitor.name}</span> </div> </td> {problems[currentRound].map(problem => { const score = competitor.scores[currentRound][problem.id]; return ( <td key={problem.id} className="px-4 py-4 text-center"> <div className="flex flex-col space-y-2"> <div className="flex justify-center space-x-1"> <button onClick={() => updateScore(competitor.id, currentRound, problem.id, 'top', !score.top)} className={`px-3 py-1 rounded text-xs font-medium transition-colors ${ score.top ? 'bg-green-500 text-white' : 'bg-gray-200 text-gray-700 hover:bg-gray-300' }`} > Top </button> <button onClick={() => updateScore(competitor.id, currentRound, problem.id, 'zone', !score.zone)} className={`px-3 py-1 rounded text-xs font-medium transition-colors ${ score.zone ? 'bg-yellow-500 text-white' : 'bg-gray-200 text-gray-700 hover:bg-gray-300' }`} disabled={score.top} > Zone </button> </div> </div> </td> ); })} </tr> ))} </tbody> </table> </div> </div> ); })} </div> ); const ManagementView = () => ( <div className="space-y-6"> <div className="bg-gradient-to-r from-purple-600 to-pink-600 text-white p-6 rounded-xl"> <h2 className="text-2xl font-bold flex items-center gap-2"> <Users className="w-6 h-6" /> Competition Management </h2> <p className="text-purple-100 mt-1">Manage competitors and categories</p> </div> {/* Add New Competitor */} <div className="bg-white rounded-xl shadow-lg p-6"> <h3 className="text-lg font-semibold text-gray-800 mb-4">Add New Competitor</h3> <div className="flex gap-4"> <input type="text" placeholder="Competitor Name" value={newCompetitor.name} onChange={(e) => setNewCompetitor({...newCompetitor, name: e.target.value})} className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" /> <input type="text" placeholder="Bib Number" value={newCompetitor.bibNumber} onChange={(e) => setNewCompetitor({...newCompetitor, bibNumber: e.target.value})} className="w-32 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" /> <select value={newCompetitor.categoryId} onChange={(e) => setNewCompetitor({...newCompetitor, categoryId: parseInt(e.target.value)})} className="w-48 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" > {categories.map(category => ( <option key={category.id} value={category.id}>{category.name}</option> ))} </select> <button onClick={addCompetitor} className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center gap-2" > <Plus className="w-4 h-4" /> Add </button> </div> </div> {/* Competitors List */} <div className="bg-white rounded-xl shadow-lg overflow-hidden"> <div className="bg-gray-50 px-6 py-4 border-b"> <h3 className="text-lg font-semibold text-gray-800">Registered Competitors ({competitors.length})</h3> </div> <div className="divide-y divide-gray-200"> {competitors.map(competitor => { const category = categories.find(c => c.id === competitor.categoryId); return ( <div key={competitor.id} className="px-6 py-4 flex items-center justify-between hover:bg-gray-50"> <div className="flex items-center space-x-4"> <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"> {competitor.bibNumber} </span> <div> <div className="font-medium text-gray-900">{competitor.name}</div> <div className="text-sm text-gray-500">{category?.name}</div> </div> </div> <button onClick={() => setCompetitors(competitors.filter(c => c.id !== competitor.id))} className="text-red-600 hover:text-red-800" > <X className="w-4 h-4" /> </button> </div> ); })} </div> </div> </div> ); return ( <div className="min-h-screen bg-gray-100"> {/* Header */} <div className="bg-white shadow-sm border-b"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="flex justify-between items-center py-4"> <div className="flex items-center space-x-3"> <Trophy className="w-8 h-8 text-blue-600" /> <div> <h1 className="text-2xl font-bold text-gray-900">Bouldering Competition System</h1> <p className="text-sm text-gray-500">Live results and scoring platform</p> </div> </div> <div className="flex items-center space-x-4"> <Clock className="w-5 h-5 text-gray-400" /> <span className="text-sm text-gray-500"> {new Date().toLocaleTimeString()} </span> </div> </div> </div> </div> {/* Navigation */} <div className="bg-white border-b"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <nav className="flex space-x-8"> <button onClick={() => setActiveTab('scoreboard')} className={`py-4 px-1 border-b-2 font-medium text-sm transition-colors ${ activeTab === 'scoreboard' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }`} > Live Scoreboard </button> <button onClick={() => setActiveTab('scoring')} className={`py-4 px-1 border-b-2 font-medium text-sm transition-colors ${ activeTab === 'scoring' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }`} > Score Entry </button> <button onClick={() => setActiveTab('management')} className={`py-4 px-1 border-b-2 font-medium text-sm transition-colors ${ activeTab === 'management' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }`} > Management </button> </nav> </div> </div> {/* Main Content */} <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> {activeTab === 'scoreboard' && <ScoreboardView />} {activeTab === 'scoring' && <ScoringView />} {activeTab === 'management' && <ManagementView />} </div> </div> ); }; export default BoulderingCompetitionSystem;