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;