const express = require('express'); const cors = require('cors'); const { PrismaClient } = require('./generated/prisma'); require('dotenv').config(); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const http = require('http'); const { Server } = require('socket.io'); const multer = require('multer'); const path = require('path'); const app = express(); const prisma = new PrismaClient(); app.use(cors()); app.use(express.json()); const PORT = process.env.PORT || 4000; const JWT_SECRET = process.env.JWT_SECRET || 'changeme'; // Serve static files from the React build app.use(express.static(path.join(__dirname, 'public'))); // API routes app.get('/api/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); // Serve React app for all non-API routes app.get('*', (req, res) => { // Don't serve React app for API routes if (req.path.startsWith('/api/')) { return res.status(404).json({ error: 'API endpoint not found' }); } res.sendFile(path.join(__dirname, 'public', 'index.html')); }); // Player registration app.post('/api/players', async (req, res) => { const { name, email } = req.body; if (!name || !email) { return res.status(400).json({ error: 'Name and email are required.' }); } try { const player = await prisma.player.create({ data: { name, email }, }); res.status(201).json(player); } catch (err) { if (err.code === 'P2002') { // Unique constraint failed return res.status(409).json({ error: 'Email already registered.' }); } res.status(500).json({ error: 'Failed to register player.' }); } }); // List all players app.get('/api/players', async (req, res) => { try { const players = await prisma.player.findMany({ orderBy: { registeredAt: 'asc' } }); res.json(players); } catch (err) { res.status(500).json({ error: 'Failed to fetch players.' }); } }); // Admin registration (for initial setup, can be removed later) app.post('/api/admin/register', async (req, res) => { const { username, password } = req.body; if (!username || !password) { return res.status(400).json({ error: 'Username and password are required.' }); } try { const hash = await bcrypt.hash(password, 10); const admin = await prisma.adminUser.create({ data: { username, passwordHash: hash }, }); res.status(201).json({ id: admin.id, username: admin.username }); } catch (err) { if (err.code === 'P2002') { return res.status(409).json({ error: 'Username already exists.' }); } res.status(500).json({ error: 'Failed to register admin.' }); } }); // Admin login app.post('/api/admin/login', async (req, res) => { const { username, password } = req.body; if (!username || !password) { return res.status(400).json({ error: 'Username and password are required.' }); } // Use environment variables for admin credentials if ( username === process.env.ADMIN_USERNAME && password === process.env.ADMIN_PASSWORD ) { const token = jwt.sign({ admin: true, username, email: process.env.ADMIN_EMAIL }, JWT_SECRET, { expiresIn: '8h' }); return res.json({ token }); } return res.status(401).json({ error: 'Invalid credentials.' }); }); // JWT authentication middleware function requireAdmin(req, res, next) { const auth = req.headers.authorization; if (!auth || !auth.startsWith('Bearer ')) { return res.status(401).json({ error: 'Missing or invalid Authorization header.' }); } const token = auth.split(' ')[1]; try { const payload = jwt.verify(token, JWT_SECRET); req.admin = payload; next(); } catch (err) { return res.status(401).json({ error: 'Invalid or expired token.' }); } } // Example protected admin route app.get('/api/admin/me', requireAdmin, async (req, res) => { // Return admin info from env res.json({ username: process.env.ADMIN_USERNAME, email: process.env.ADMIN_EMAIL }); }); // Player registration with username app.post('/api/register', async (req, res) => { const { name, username, password } = req.body; if (!name || !username || !password) { return res.status(400).json({ error: 'Name, username, and password are required.' }); } try { const hash = await bcrypt.hash(password, 10); const player = await prisma.player.create({ data: { name, username, passwordHash: hash }, }); res.status(201).json({ id: player.id, name: player.name, username: player.username }); } catch (err) { if (err.code === 'P2002') { return res.status(409).json({ error: 'Username already registered.' }); } res.status(500).json({ error: 'Failed to register player.' }); } }); // Player login with username app.post('/api/login', async (req, res) => { const { username, password } = req.body; if (!username || !password) { return res.status(400).json({ error: 'Username and password are required.' }); } try { const player = await prisma.player.findUnique({ where: { username } }); if (!player || !player.passwordHash) { return res.status(401).json({ error: 'Invalid credentials.' }); } const valid = await bcrypt.compare(password, player.passwordHash); if (!valid) { return res.status(401).json({ error: 'Invalid credentials.' }); } const token = jwt.sign({ playerId: player.id, name: player.name, username: player.username }, JWT_SECRET, { expiresIn: '8h' }); res.json({ token }); } catch (err) { res.status(500).json({ error: 'Failed to login.' }); } }); // JWT authentication middleware for players function requirePlayer(req, res, next) { const auth = req.headers.authorization; if (!auth || !auth.startsWith('Bearer ')) { return res.status(401).json({ error: 'Missing or invalid Authorization header.' }); } const token = auth.split(' ')[1]; try { const payload = jwt.verify(token, JWT_SECRET); req.player = payload; next(); } catch (err) { return res.status(401).json({ error: 'Invalid or expired token.' }); } } // Example protected player route app.get('/api/me', requirePlayer, async (req, res) => { const player = await prisma.player.findUnique({ where: { id: req.player.playerId }, select: { id: true, name: true, email: true } }); if (!player) return res.status(404).json({ error: 'Player not found.' }); res.json(player); }); // Utility to generate all unique pairs for round robin function generateRoundRobinPairs(teams) { const pairs = []; for (let i = 0; i < teams.length; i++) { for (let j = i + 1; j < teams.length; j++) { pairs.push([teams[i], teams[j]]); } } return pairs; } // Admin: Schedule round robin matches (teams) app.post('/api/admin/schedule/roundrobin', requireAdmin, async (req, res) => { try { const existingStage = await prisma.tournamentStage.findFirst({ where: { type: 'ROUND_ROBIN' } }); if (existingStage) { return res.status(400).json({ error: 'Round robin stage already scheduled.' }); } // Get all teams const teams = await prisma.team.findMany(); if (teams.length < 3) { return res.status(400).json({ error: 'At least 3 teams required.' }); } // Pool logic: min 3, max 5 const minTeamsPerPool = 3; const maxTeamsPerPool = 5; const numPools = Math.ceil(teams.length / maxTeamsPerPool); if (numPools * minTeamsPerPool > teams.length) { return res.status(400).json({ error: `Not enough teams to create ${numPools} pools with at least ${minTeamsPerPool} teams each.` }); } // Shuffle teams const shuffled = [...teams]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } // Distribute teams into pools as evenly as possible (no pool > 5, all pools >= 3) const pools = Array.from({ length: numPools }, () => []); let idx = 0; for (const team of shuffled) { pools[idx % numPools].push(team); idx++; } // Debug log console.log('Teams:', teams.length, 'Num pools:', numPools, 'Pool sizes:', pools.map(p => p.length)); if (pools.some(pool => pool.length < minTeamsPerPool || pool.length > maxTeamsPerPool)) { return res.status(400).json({ error: `Unable to distribute teams into pools of min ${minTeamsPerPool} and max ${maxTeamsPerPool} teams.` }); } // Get or create default tournament let tournament = await prisma.tournament.findFirst(); if (!tournament) { tournament = await prisma.tournament.create({ data: { name: 'Spring Championship 2024', date: new Date('2024-03-15'), location: 'Main Arena' } }); } // Create stage const stage = await prisma.tournamentStage.create({ data: { type: 'ROUND_ROBIN', startedAt: new Date(), tournamentId: tournament.id } }); // Generate matches for each pool const now = new Date(); let matchesData = []; pools.forEach((pool, poolIdx) => { const pairs = generateRoundRobinPairs(pool); matchesData.push(...pairs.map(([t1, t2]) => ({ stageId: stage.id, team1Id: t1.id, team2Id: t2.id, scheduledAt: now, pool: poolIdx + 1 }))); }); const matches = await prisma.match.createMany({ data: matchesData }); // Prepare pool assignments for response const poolAssignments = pools.map((pool, i) => ({ pool: i + 1, teams: pool.map(t => ({ id: t.id, name: t.name })) })); res.status(201).json({ stage, matchesCreated: matches.count, pools: poolAssignments }); } catch (err) { res.status(500).json({ error: 'Failed to schedule round robin.' }); } }); // List all matches with team names and results app.get('/api/matches', async (req, res) => { try { const matches = await prisma.match.findMany({ orderBy: { scheduledAt: 'asc' }, include: { team1: { select: { id: true, name: true, logo: true } }, team2: { select: { id: true, name: true, logo: true } }, result: true, stage: { select: { id: true, type: true, tier: true } } } }); res.json(matches); } catch (err) { res.status(500).json({ error: 'Failed to fetch matches.' }); } }); // Admin: Enter match result (teams) app.post('/api/admin/matches/:matchId/result', requireAdmin, async (req, res) => { const matchId = parseInt(req.params.matchId, 10); const { team1Score, team2Score } = req.body; if (!Number.isInteger(matchId) || matchId <= 0) { return res.status(400).json({ error: 'Invalid match ID.' }); } if (typeof team1Score !== 'number' || typeof team2Score !== 'number') { return res.status(400).json({ error: 'Scores must be numbers.' }); } try { const match = await prisma.match.findUnique({ where: { id: matchId }, include: { team1: true, team2: true, result: true } }); if (!match) { return res.status(404).json({ error: 'Match not found.' }); } if (match.result) { return res.status(409).json({ error: 'Result already entered for this match.' }); } let winnerId = null; if (team1Score > team2Score) winnerId = match.team1Id; else if (team2Score > team1Score) winnerId = match.team2Id; else return res.status(400).json({ error: 'Draws are not allowed.' }); const result = await prisma.result.create({ data: { matchId: match.id, team1Score, team2Score, winnerId } }); const updatedMatch = await prisma.match.findUnique({ where: { id: matchId }, include: { team1: { select: { id: true, name: true, logo: true } }, team2: { select: { id: true, name: true, logo: true } }, result: true, stage: { select: { id: true, type: true } } } }); res.status(201).json(updatedMatch); io.emit('matchResultUpdated', { matchId, updatedMatch }); emitBracketUpdate(); } catch (err) { res.status(500).json({ error: 'Failed to enter match result.' }); } }); // Compute and return round robin standings (teams) app.get('/api/standings', async (req, res) => { try { // Get all teams const teams = await prisma.team.findMany(); // Get all round robin matches with results const rrStage = await prisma.tournamentStage.findFirst({ where: { type: 'ROUND_ROBIN' } }); if (!rrStage) return res.status(400).json({ error: 'No round robin stage found.' }); const matches = await prisma.match.findMany({ where: { stageId: rrStage.id }, include: { result: true } }); // Compute stats const stats = {}; teams.forEach(t => { stats[t.id] = { id: t.id, name: t.name, logo: t.logo, wins: 0, losses: 0, played: 0, points: 0 }; }); matches.forEach(m => { if (!m.result) return; stats[m.team1Id].played++; stats[m.team2Id].played++; if (m.result.winnerId === m.team1Id) { stats[m.team1Id].wins++; stats[m.team2Id].losses++; stats[m.team1Id].points += 2; } else if (m.result.winnerId === m.team2Id) { stats[m.team2Id].wins++; stats[m.team1Id].losses++; stats[m.team2Id].points += 2; } }); // Convert to array and sort const standings = Object.values(stats).sort((a, b) => b.points - a.points || b.wins - a.wins || a.name.localeCompare(b.name) ); res.json(standings); } catch (err) { res.status(500).json({ error: 'Failed to compute standings.' }); } }); // Utility: get next power of 2 <= n function getBracketSize(n) { if (n >= 8) return 8; if (n >= 4) return 4; return 2; } // Admin: Schedule single elimination bracket (teams) app.post('/api/admin/schedule/singleelim', requireAdmin, async (req, res) => { try { // Check if any SINGLE_ELIM stage exists const existingStage = await prisma.tournamentStage.findFirst({ where: { type: 'SINGLE_ELIM' } }); if (existingStage) { return res.status(400).json({ error: 'Single elimination stage already scheduled.' }); } // Get round robin standings const rrStage = await prisma.tournamentStage.findFirst({ where: { type: 'ROUND_ROBIN' } }); if (!rrStage) return res.status(400).json({ error: 'No round robin stage found.' }); const rrMatches = await prisma.match.findMany({ where: { stageId: rrStage.id }, include: { result: true } }); if (rrMatches.some(m => !m.result)) { return res.status(400).json({ error: 'Not all round robin matches have results.' }); } // Compute standings const stats = {}; rrMatches.forEach(m => { if (!stats[m.team1Id]) stats[m.team1Id] = { id: m.team1Id, wins: 0, points: 0 }; if (!stats[m.team2Id]) stats[m.team2Id] = { id: m.team2Id, wins: 0, points: 0 }; if (m.result.winnerId === m.team1Id) { stats[m.team1Id].wins++; stats[m.team1Id].points += 2; } else if (m.result.winnerId === m.team2Id) { stats[m.team2Id].wins++; stats[m.team2Id].points += 2; } }); // Get team info const teams = await prisma.team.findMany(); const standings = Object.values(stats).map(s => ({ ...s, name: teams.find(t => t.id === s.id)?.name || '' })); standings.sort((a, b) => b.points - a.points || b.wins - a.wins || a.name.localeCompare(b.name)); if (standings.length < 2) return res.status(400).json({ error: 'Not enough teams for single elimination.' }); // Split standings into tiers of 4 const tiers = []; for (let i = 0; i < standings.length; i += 4) { tiers.push(standings.slice(i, i + 4)); } const now = new Date(); const allStages = []; const allMatches = []; for (let tierIdx = 0; tierIdx < tiers.length; tierIdx++) { const tierTeams = tiers[tierIdx]; if (tierTeams.length < 2) continue; // Skip tiers with less than 2 teams // Get or create default tournament let tournament = await prisma.tournament.findFirst(); if (!tournament) { tournament = await prisma.tournament.create({ data: { name: 'Spring Championship 2024', date: new Date('2024-03-15'), location: 'Main Arena' } }); } // Create stage for this tier const stage = await prisma.tournamentStage.create({ data: { type: 'SINGLE_ELIM', startedAt: now, tier: tierIdx + 1, tournamentId: tournament.id } }); let matchesData = []; if (tierTeams.length === 4) { // 4 teams: 1 vs 4, 2 vs 3 const pairs = [ [tierTeams[0], tierTeams[3]], [tierTeams[1], tierTeams[2]] ]; matchesData = pairs.map(([t1, t2]) => ({ stageId: stage.id, team1Id: t1.id, team2Id: t2.id, scheduledAt: now, pool: 0 })); } else if (tierTeams.length === 3) { // 3 teams: top seed gets a bye, 2 vs 3 play semifinal, winner faces 1 in final // Semifinal: 2 vs 3 matchesData.push({ stageId: stage.id, team1Id: tierTeams[1].id, team2Id: tierTeams[2].id, scheduledAt: now, pool: 0 }); // Final: 1 vs winner of semifinal (to be scheduled after semifinal is played) // We'll need to add logic elsewhere to schedule the final after the semifinal result is in. } else if (tierTeams.length === 2) { // 2 teams: single final matchesData.push({ stageId: stage.id, team1Id: tierTeams[0].id, team2Id: tierTeams[1].id, scheduledAt: now, pool: 0 }); } const matches = await prisma.match.createMany({ data: matchesData }); allStages.push(stage); allMatches.push({ tier: tierIdx + 1, matchesCreated: matches.count, teams: tierTeams.map(t => t ? t.name : null) }); } res.status(201).json({ stages: allStages, summary: allMatches }); } catch (err) { console.error('Error scheduling single elimination:', err); res.status(500).json({ error: 'Failed to schedule single elimination.' }); } }); // View single elimination bracket app.get('/api/bracket/singleelim', async (req, res) => { try { const stage = await prisma.tournamentStage.findFirst({ where: { type: 'SINGLE_ELIM' } }); if (!stage) return res.status(404).json({ error: 'Single elimination stage not found.' }); const matches = await prisma.match.findMany({ where: { stageId: stage.id }, orderBy: { scheduledAt: 'asc' }, include: { team1: { select: { id: true, name: true, logo: true } }, team2: { select: { id: true, name: true, logo: true } }, result: true } }); res.json({ stage, matches }); } catch (err) { res.status(500).json({ error: 'Failed to fetch single elimination bracket.' }); } }); // Admin: Advance winners and schedule next round in single elimination (by tier) app.post('/api/admin/schedule/singleelim/next', requireAdmin, async (req, res) => { try { const tier = parseInt(req.query.tier, 10); if (!tier || isNaN(tier)) { return res.status(400).json({ error: 'Tier parameter is required and must be a number.' }); } const stage = await prisma.tournamentStage.findFirst({ where: { type: 'SINGLE_ELIM', tier } }); if (!stage) return res.status(404).json({ error: 'Single elimination stage for this tier not found.' }); // Get all matches for this stage const matches = await prisma.match.findMany({ where: { stageId: stage.id }, orderBy: { scheduledAt: 'asc' }, include: { result: true } }); // Group matches by scheduledAt (rounds) const rounds = {}; matches.forEach(m => { const key = m.scheduledAt.toISOString(); if (!rounds[key]) rounds[key] = []; rounds[key].push(m); }); // Find latest round (by scheduledAt) const roundKeys = Object.keys(rounds).sort(); const latestKey = roundKeys[roundKeys.length - 1]; const latestRound = rounds[latestKey]; // Check all matches in latest round have results if (latestRound.some(m => !m.result)) { return res.status(400).json({ error: 'Not all matches in the current round have results.' }); } // Get winners const winners = latestRound.map(m => m.result.winnerId); if (winners.length === 1) { // Tournament complete for this tier const champion = await prisma.team.findUnique({ where: { id: winners[0] } }); return res.json({ message: 'Tournament complete for this tier', champion }); } // Pair winners for next round const pairs = []; for (let i = 0; i < winners.length; i += 2) { if (i + 1 < winners.length) { pairs.push([winners[i], winners[i + 1]]); } else { // Odd number: bye (auto-advance) pairs.push([winners[i], null]); } } // Schedule next round const now = new Date(); const matchesData = pairs.filter(p => p[1] !== null).map(([t1Id, t2Id]) => ({ stageId: stage.id, team1Id: t1Id, team2Id: t2Id, scheduledAt: now, pool: 0 })); const newMatches = await prisma.match.createMany({ data: matchesData }); res.status(201).json({ matchesCreated: newMatches.count, nextRoundPairs: pairs }); io.emit('roundAdvanced'); emitBracketUpdate(); } catch (err) { res.status(500).json({ error: 'Failed to schedule next round.' }); } }); // Change admin password (requires authentication) app.post('/api/admin/change-password', requireAdmin, async (req, res) => { const { oldPassword, newPassword } = req.body; if (!oldPassword || !newPassword) { return res.status(400).json({ error: 'Old and new passwords are required.' }); } try { const admin = await prisma.adminUser.findUnique({ where: { id: req.admin.adminId } }); if (!admin) { return res.status(404).json({ error: 'Admin not found.' }); } const valid = await bcrypt.compare(oldPassword, admin.passwordHash); if (!valid) { return res.status(401).json({ error: 'Old password is incorrect.' }); } const newHash = await bcrypt.hash(newPassword, 10); await prisma.adminUser.update({ where: { id: admin.id }, data: { passwordHash: newHash } }); res.json({ message: 'Password updated successfully.' }); } catch (err) { res.status(500).json({ error: 'Failed to update password.' }); } }); // Change player password (requires authentication) app.post('/api/player/change-password', requirePlayer, async (req, res) => { const { oldPassword, newPassword } = req.body; if (!oldPassword || !newPassword) { return res.status(400).json({ error: 'Old and new passwords are required.' }); } try { const player = await prisma.player.findUnique({ where: { id: req.player.playerId } }); if (!player || !player.passwordHash) { return res.status(404).json({ error: 'Player not found or password not set.' }); } const valid = await bcrypt.compare(oldPassword, player.passwordHash); if (!valid) { return res.status(401).json({ error: 'Old password is incorrect.' }); } const newHash = await bcrypt.hash(newPassword, 10); await prisma.player.update({ where: { id: player.id }, data: { passwordHash: newHash } }); res.json({ message: 'Password updated successfully.' }); } catch (err) { res.status(500).json({ error: 'Failed to update password.' }); } }); // Set up multer storage for /uploads directory const storage = multer.diskStorage({ destination: function (req, file, cb) { cb(null, path.join(__dirname, 'uploads')); }, filename: function (req, file, cb) { const ext = path.extname(file.originalname); const uniqueName = `player_${req.player.playerId}_${Date.now()}${ext}`; cb(null, uniqueName); } }); const upload = multer({ storage }); // Serve /uploads as static files app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); // Player profile image upload endpoint app.post('/api/player/upload-image', requirePlayer, upload.single('image'), async (req, res) => { if (!req.file) { return res.status(400).json({ error: 'No file uploaded.' }); } try { const imageUrl = `/uploads/${req.file.filename}`; await prisma.player.update({ where: { id: req.player.playerId }, data: { imageUrl } }); res.json({ imageUrl }); } catch (err) { res.status(500).json({ error: 'Failed to update profile image.' }); } }); // Update player name (requires authentication) app.post('/api/player/update-name', requirePlayer, async (req, res) => { const { name } = req.body; if (!name) { return res.status(400).json({ error: 'Name is required.' }); } try { const player = await prisma.player.update({ where: { id: req.player.playerId }, data: { name } }); res.json({ id: player.id, name: player.name, email: player.email, imageUrl: player.imageUrl }); } catch (err) { res.status(500).json({ error: 'Failed to update name.' }); } }); // In-memory store for password reset tokens (for development only) const passwordResetTokens = {}; // Request password reset: generate token and log link app.post('/api/player/request-password-reset', async (req, res) => { const { email } = req.body; if (!email) return res.status(400).json({ error: 'Email is required.' }); try { const player = await prisma.player.findUnique({ where: { email } }); if (!player) return res.status(404).json({ error: 'No user with that email.' }); // Generate a token (simple random string for dev) const token = Math.random().toString(36).substr(2) + Date.now(); passwordResetTokens[token] = { playerId: player.id, expires: Date.now() + 1000 * 60 * 15 }; // 15 min expiry const resetLink = `http://localhost:3000/reset-password?token=${token}`; console.log(`Password reset link for ${email}: ${resetLink}`); res.json({ message: 'Password reset link sent (check backend console).' }); } catch (err) { res.status(500).json({ error: 'Failed to process request.' }); } }); // Reset password using token app.post('/api/player/reset-password', async (req, res) => { const { token, newPassword } = req.body; if (!token || !newPassword) return res.status(400).json({ error: 'Token and new password are required.' }); const entry = passwordResetTokens[token]; if (!entry || entry.expires < Date.now()) { return res.status(400).json({ error: 'Invalid or expired token.' }); } try { const hash = await bcrypt.hash(newPassword, 10); await prisma.player.update({ where: { id: entry.playerId }, data: { passwordHash: hash } }); delete passwordResetTokens[token]; res.json({ message: 'Password has been reset.' }); } catch (err) { res.status(500).json({ error: 'Failed to reset password.' }); } }); // Function to automatically create pools when teams are registered async function createPoolsAutomatically() { try { // Get all teams const teams = await prisma.team.findMany(); if (teams.length < 3) { return { success: false, message: 'Need at least 3 teams to create pools.' }; } // Check if pools already exist const existingStage = await prisma.tournamentStage.findFirst({ where: { type: 'ROUND_ROBIN' } }); if (existingStage) { return { success: false, message: 'Pools already exist.' }; } // Pool logic: min 3, max 5 const minTeamsPerPool = 3; const maxTeamsPerPool = 5; const numPools = Math.ceil(teams.length / maxTeamsPerPool); if (numPools * minTeamsPerPool > teams.length) { return { success: false, message: `Not enough teams to create ${numPools} pools with at least ${minTeamsPerPool} teams each.` }; } // Shuffle teams const shuffled = [...teams]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } // Distribute teams into pools as evenly as possible (no pool > 5, all pools >= 3) const pools = Array.from({ length: numPools }, () => []); let idx = 0; for (const team of shuffled) { pools[idx % numPools].push(team); idx++; } // Debug log console.log('Teams:', teams.length, 'Num pools:', numPools, 'Pool sizes:', pools.map(p => p.length)); if (pools.some(pool => pool.length < minTeamsPerPool || pool.length > maxTeamsPerPool)) { return { success: false, message: `Unable to distribute teams into pools of min ${minTeamsPerPool} and max ${maxTeamsPerPool} teams.` }; } // Get or create default tournament let tournament = await prisma.tournament.findFirst(); if (!tournament) { tournament = await prisma.tournament.create({ data: { name: 'Championship Tournament', date: new Date(), location: 'Main Arena' } }); } // Create stage const stage = await prisma.tournamentStage.create({ data: { type: 'ROUND_ROBIN', startedAt: new Date(), tournamentId: tournament.id } }); // Generate matches for each pool const now = new Date(); let matchesData = []; pools.forEach((pool, poolIdx) => { const pairs = generateRoundRobinPairs(pool); matchesData.push(...pairs.map(([t1, t2]) => ({ stageId: stage.id, team1Id: t1.id, team2Id: t2.id, scheduledAt: now, pool: poolIdx + 1 }))); }); const matches = await prisma.match.createMany({ data: matchesData }); // Prepare pool assignments for response const poolAssignments = pools.map((pool, i) => ({ pool: i + 1, teams: pool.map(t => ({ id: t.id, name: t.name })) })); return { success: true, message: `Pools created successfully with ${matches.count} matches.`, pools: poolAssignments, matchesCreated: matches.count }; } catch (err) { console.error('Error creating pools automatically:', err); return { success: false, message: 'Failed to create pools automatically.' }; } } // Create a team app.post('/api/teams', async (req, res) => { const { name, logo } = req.body; if (!name) { return res.status(400).json({ error: 'Team name is required.' }); } // Generate SVG logo if not provided let teamLogo = logo; if (!teamLogo) { let hash = 0; for (let i = 0; i < name.length; i++) { hash = name.charCodeAt(i) + ((hash << 5) - hash); } const hue = Math.abs(hash) % 360; const saturation = 60 + (Math.abs(hash) % 30); const lightness = 40 + (Math.abs(hash) % 30); const color = `hsl(${hue}, ${saturation}%, ${lightness}%)`; const secondaryHue = (hue + 180) % 360; const secondaryColor = `hsl(${secondaryHue}, ${saturation}%, ${lightness}%)`; const pattern = Math.abs(hash) % 4; const initials = name.split(' ').map(w => w[0]).join('').toUpperCase().slice(0, 2); switch (pattern) { case 0: // Circle with initials teamLogo = `${initials}`; break; case 1: // Square with diagonal teamLogo = `${initials}`; break; case 2: // Triangle teamLogo = `${initials}`; break; case 3: // Diamond teamLogo = `${initials}`; break; } } try { const team = await prisma.team.create({ data: { name, logo: teamLogo } }); // Try to create pools automatically after team registration const poolResult = await createPoolsAutomatically(); if (poolResult.success) { res.status(201).json({ team, poolsCreated: true, poolMessage: poolResult.message, pools: poolResult.pools, matchesCreated: poolResult.matchesCreated }); } else { res.status(201).json({ team, poolsCreated: false, poolMessage: poolResult.message }); } } catch (err) { if (err.code === 'P2002') { return res.status(409).json({ error: 'Team name already exists.' }); } res.status(500).json({ error: 'Failed to create team.' }); } }); // List all teams app.get('/api/teams', async (req, res) => { try { const teams = await prisma.team.findMany({ orderBy: { registeredAt: 'asc' } }); res.json(teams); } catch (err) { res.status(500).json({ error: 'Failed to fetch teams.' }); } }); // Create a tournament app.post('/api/tournaments', requireAdmin, async (req, res) => { const { name, date, location } = req.body; if (!name || !date || !location) { return res.status(400).json({ error: 'Name, date, and location are required.' }); } try { const tournament = await prisma.tournament.create({ data: { name, date: new Date(date), location } }); res.status(201).json(tournament); } catch (err) { res.status(500).json({ error: 'Failed to create tournament.' }); } }); // Get available tournaments app.get('/api/tournaments', async (req, res) => { try { // Fetch real tournaments from database const tournaments = await prisma.tournament.findMany({ include: { stages: { include: { matches: { include: { team1: true, team2: true, result: true } } } } }, orderBy: { createdAt: 'desc' } }); // If no tournaments exist, create a default one if (tournaments.length === 0) { const defaultTournament = await prisma.tournament.create({ data: { name: 'Spring Championship 2024', date: new Date('2024-03-15'), location: 'Main Arena' } }); // Add some sample tournaments await prisma.tournament.createMany({ data: [ { name: 'Winter Cup 2024', date: new Date('2024-01-20'), location: 'Sports Complex' }, { name: 'Summer League 2024', date: new Date('2024-06-10'), location: 'City Stadium' } ] }); // Fetch the tournaments again after creating them const updatedTournaments = await prisma.tournament.findMany({ include: { stages: { include: { matches: { include: { team1: true, team2: true, result: true } } } } }, orderBy: { createdAt: 'desc' } }); // Transform the data for frontend const teamCount = await prisma.team.count(); const transformedTournaments = updatedTournaments.map(tournament => { const totalMatches = tournament.stages.reduce((sum, stage) => sum + stage.matches.length, 0); const completedMatches = tournament.stages.reduce((sum, stage) => sum + stage.matches.filter(match => match.result).length, 0); let status = 'upcoming'; if (totalMatches > 0) { if (completedMatches === totalMatches) { status = 'completed'; } else if (completedMatches > 0) { status = 'active'; } } return { id: tournament.id, name: tournament.name, date: tournament.date.toISOString().split('T')[0], location: tournament.location, teams: teamCount, // Total teams in system status: status, totalMatches: totalMatches, completedMatches: completedMatches }; }); res.json(transformedTournaments); } else { // Transform existing tournaments const transformedTournaments = await Promise.all(tournaments.map(async tournament => { const totalMatches = tournament.stages.reduce((sum, stage) => sum + stage.matches.length, 0); const completedMatches = tournament.stages.reduce((sum, stage) => sum + stage.matches.filter(match => match.result).length, 0); let status = 'upcoming'; if (totalMatches > 0) { if (completedMatches === totalMatches) { status = 'completed'; } else if (completedMatches > 0) { status = 'active'; } } return { id: tournament.id, name: tournament.name, date: tournament.date.toISOString().split('T')[0], location: tournament.location, teams: await prisma.team.count(), // Total teams in system status: status, totalMatches: totalMatches, completedMatches: completedMatches }; })); res.json(transformedTournaments); } } catch (err) { console.error('Error fetching tournaments:', err); res.status(500).json({ error: 'Failed to fetch tournaments.' }); } }); // Admin: Reset the entire tournament (delete all data) app.post('/api/admin/reset-tournament', requireAdmin, async (req, res) => { try { await prisma.result.deleteMany(); await prisma.match.deleteMany(); await prisma.tournamentStage.deleteMany(); await prisma.team.deleteMany(); res.json({ message: 'Tournament reset.' }); } catch (err) { res.status(500).json({ error: 'Failed to reset tournament.' }); } }); const server = http.createServer(app); const io = new Server(server, { cors: { origin: '*' } }); // Socket.IO connection io.on('connection', (socket) => { console.log('A client connected:', socket.id); socket.on('disconnect', () => { console.log('Client disconnected:', socket.id); }); }); // Helper to emit bracket update function emitBracketUpdate() { io.emit('bracketUpdated'); } server.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); });