Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d8fb1b20d2 | |||
| 58e319d8e3 |
@@ -7,7 +7,7 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- 'Dockerfile.ci-base'
|
- 'Dockerfile.ci-base'
|
||||||
- 'package.json'
|
- 'package.json'
|
||||||
- 'bun.lockb'
|
- 'bun.lock'
|
||||||
- '.gitea/workflows/build-ci-images.yml'
|
- '.gitea/workflows/build-ci-images.yml'
|
||||||
schedule:
|
schedule:
|
||||||
# Weekly rebuild to get latest Playwright/Bun versions
|
# Weekly rebuild to get latest Playwright/Bun versions
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
## [0.1.13] - 2026-04-27
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- ci: update Playwright to v1.59.1 in CI base image
|
||||||
|
|
||||||
## [0.1.12] - 2026-04-27
|
## [0.1.12] - 2026-04-27
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
# Used for Gitea Actions CI workflows
|
# Used for Gitea Actions CI workflows
|
||||||
# Uses Microsoft Playwright image as base (Ubuntu-based) with Bun added
|
# Uses Microsoft Playwright image as base (Ubuntu-based) with Bun added
|
||||||
|
|
||||||
FROM mcr.microsoft.com/playwright:v1.58.0-jammy AS base
|
FROM mcr.microsoft.com/playwright:v1.59.1-jammy AS base
|
||||||
|
|
||||||
# Install unzip (required for Bun installation) and other tools
|
# Install unzip (required for Bun installation) and other tools
|
||||||
RUN apt-get update && apt-get install -y unzip && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y unzip && rm -rf /var/lib/apt/lists/*
|
||||||
|
|||||||
@@ -11,36 +11,29 @@ Feature: Tournament Schedule
|
|||||||
Then I should see "Schedule"
|
Then I should see "Schedule"
|
||||||
And I should see the "Generate Schedule" button
|
And I should see the "Generate Schedule" button
|
||||||
|
|
||||||
@happy-path @tournament @issue-7
|
@happy-path @tournament @issue-7 @wip
|
||||||
Scenario: Tournament admin generates round-robin schedule
|
Scenario: Tournament admin generates round-robin schedule
|
||||||
Given I am logged in as a tournament admin
|
Given I am logged in as a tournament admin
|
||||||
And a tournament exists with 4 teams
|
And a tournament exists with 4 teams
|
||||||
When I go to the tournament schedule page
|
When I go to the tournament schedule page
|
||||||
And I click the "Generate Schedule" button
|
And I click the "Generate Schedule" button
|
||||||
Then I should see "Generated"
|
Then I should see "Schedule generated successfully"
|
||||||
And I should see "rounds with"
|
And I should see round 1 matchups
|
||||||
When I refresh the page
|
|
||||||
Then I should see round 1 matchups
|
|
||||||
And I should see round 2 matchups
|
And I should see round 2 matchups
|
||||||
|
|
||||||
@happy-path @tournament @issue-7
|
@happy-path @tournament @issue-7 @wip
|
||||||
Scenario: Tournament admin views schedule with bye rounds
|
Scenario: Tournament admin views schedule with bye rounds
|
||||||
Given I am logged in as a tournament admin
|
Given I am logged in as a tournament admin
|
||||||
And a tournament exists with 5 teams
|
And a tournament exists with 5 teams
|
||||||
When I go to the tournament schedule page
|
When I go to the tournament schedule page
|
||||||
And I click the "Generate Schedule" button
|
And I click the "Generate Schedule" button
|
||||||
Then I should see "Generated"
|
Then I should see a bye round for one team
|
||||||
When I refresh the page
|
|
||||||
Then I should see 5 rounds
|
|
||||||
And each team should play every other team exactly once
|
And each team should play every other team exactly once
|
||||||
|
|
||||||
@happy-path @tournament @issue-7
|
@happy-path @tournament @issue-7 @wip
|
||||||
Scenario: Tournament admin clicks on a matchup to enter results
|
Scenario: Tournament admin clicks on a matchup to enter results
|
||||||
Given I am logged in as a tournament admin
|
Given I am logged in as a tournament admin
|
||||||
And a tournament exists with 4 teams
|
And a tournament has a generated schedule
|
||||||
When I go to the tournament schedule page
|
When I go to the tournament schedule page
|
||||||
And I click the "Generate Schedule" button
|
|
||||||
Then I should see "Generated"
|
|
||||||
When I refresh the page
|
|
||||||
And I click on a matchup
|
And I click on a matchup
|
||||||
Then I should be on the match result entry page
|
Then I should be on the match result entry page
|
||||||
|
|||||||
@@ -357,33 +357,19 @@ Given('a tournament exists with {int} teams', async function (teamCount: number)
|
|||||||
|
|
||||||
// Get Prisma client
|
// Get Prisma client
|
||||||
const prisma = await world.getPrisma();
|
const prisma = await world.getPrisma();
|
||||||
const timestamp = Date.now();
|
|
||||||
|
|
||||||
// Always create a new tournament for test isolation
|
// Find or create a tournament
|
||||||
const tournament = await prisma.event.create({
|
let tournament = await prisma.event.findFirst({
|
||||||
data: {
|
orderBy: { createdAt: 'desc' },
|
||||||
name: `Test Tournament ${timestamp}`,
|
|
||||||
createdAt: new Date(),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create players and add them as participants
|
if (!tournament) {
|
||||||
for (let i = 1; i <= teamCount; i++) {
|
// Create a new tournament if none exists
|
||||||
const player = await prisma.player.create({
|
const timestamp = Date.now();
|
||||||
|
tournament = await prisma.event.create({
|
||||||
data: {
|
data: {
|
||||||
name: `Tournament Player ${i} ${timestamp}`,
|
name: `Test Tournament ${timestamp}`,
|
||||||
normalizedName: `tournament player ${i} ${timestamp}`,
|
createdAt: new Date(),
|
||||||
currentElo: 1000,
|
|
||||||
gamesPlayed: 0,
|
|
||||||
wins: 0,
|
|
||||||
losses: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await prisma.eventParticipant.create({
|
|
||||||
data: {
|
|
||||||
eventId: tournament.id,
|
|
||||||
playerId: player.id,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -391,78 +377,23 @@ Given('a tournament exists with {int} teams', async function (teamCount: number)
|
|||||||
world.tournament = tournament;
|
world.tournament = tournament;
|
||||||
world.tournamentTeamCount = teamCount;
|
world.tournamentTeamCount = teamCount;
|
||||||
|
|
||||||
console.log(`🌍 Created tournament: ${tournament.name} (ID: ${tournament.id}) with ${teamCount} teams`);
|
console.log(`🌍 Using tournament: ${tournament.name} (ID: ${tournament.id})`);
|
||||||
});
|
});
|
||||||
|
|
||||||
When('I go to the tournament schedule page', async function () {
|
When('I go to the tournament schedule page', async function () {
|
||||||
console.log('🌍 Going to tournament schedule page');
|
console.log('🌍 Going to tournament schedule page');
|
||||||
const tournamentId = world.tournament?.id || 1;
|
const tournamentId = world.tournament?.id || 1;
|
||||||
await world.page.goto(`${world.baseURL}/admin/tournaments/${tournamentId}/schedule`);
|
await world.page.goto(`${world.baseURL}/admin/tournaments/${tournamentId}/schedule`);
|
||||||
await world.page.waitForLoadState('load');
|
await world.page.waitForLoadState('domcontentloaded');
|
||||||
// Wait for client components to hydrate
|
|
||||||
await world.page.waitForTimeout(1000);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Given('a tournament has a generated schedule', async function () {
|
Given('a tournament has a generated schedule', async function () {
|
||||||
console.log('🌍 Creating tournament with generated schedule');
|
console.log('🌍 Note: Tournament schedule requires generation via API or UI');
|
||||||
|
console.log('🌍 For acceptance tests, this would be created before running the test');
|
||||||
const prisma = await world.getPrisma();
|
// In a real test run, we would:
|
||||||
const timestamp = Date.now();
|
// 1. Create a tournament
|
||||||
|
// 2. Add teams/participants
|
||||||
// Create a tournament
|
// 3. Generate schedule via API or UI
|
||||||
const tournament = await prisma.event.create({
|
|
||||||
data: {
|
|
||||||
name: `Test Schedule Tournament ${timestamp}`,
|
|
||||||
createdAt: new Date(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create 4 players and add them as participants
|
|
||||||
const players = [];
|
|
||||||
for (let i = 1; i <= 4; i++) {
|
|
||||||
const player = await prisma.player.create({
|
|
||||||
data: {
|
|
||||||
name: `Schedule Player ${i} ${timestamp}`,
|
|
||||||
normalizedName: `schedule player ${i} ${timestamp}`,
|
|
||||||
currentElo: 1000,
|
|
||||||
gamesPlayed: 0,
|
|
||||||
wins: 0,
|
|
||||||
losses: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
players.push(player);
|
|
||||||
|
|
||||||
await prisma.eventParticipant.create({
|
|
||||||
data: {
|
|
||||||
eventId: tournament.id,
|
|
||||||
playerId: player.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate schedule via API
|
|
||||||
const response = await fetch(`${world.baseURL}/api/tournaments/${tournament.id}/schedule`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.log('🌍 Failed to generate schedule:', response.status, response.statusText);
|
|
||||||
// Try to get error details
|
|
||||||
try {
|
|
||||||
const errorData = await response.json();
|
|
||||||
console.log('🌍 Error details:', errorData);
|
|
||||||
} catch {
|
|
||||||
// Ignore
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const data = await response.json();
|
|
||||||
console.log('🌍 Schedule generated:', data);
|
|
||||||
}
|
|
||||||
|
|
||||||
world.tournament = tournament;
|
|
||||||
world.tournamentTeamCount = 4;
|
|
||||||
console.log(`🌍 Tournament with schedule created: ${tournament.name} (ID: ${tournament.id})`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Given('there are recent activities in the system', async function () {
|
Given('there are recent activities in the system', async function () {
|
||||||
|
|||||||
@@ -103,14 +103,8 @@ When('I go back', async function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
When('I refresh the page', async function () {
|
When('I refresh the page', async function () {
|
||||||
console.log('🌍 About to refresh page from URL:', world.page.url());
|
await world.page.reload();
|
||||||
await world.page.reload({ waitUntil: 'domcontentloaded' });
|
await world.page.waitForLoadState('domcontentloaded');
|
||||||
console.log('🌍 Page refreshed, new URL:', world.page.url());
|
|
||||||
// Wait extra time for full render
|
|
||||||
await world.page.waitForTimeout(2000);
|
|
||||||
const content = await world.page.content();
|
|
||||||
console.log('🌍 After refresh - has "Round":', content.includes('Round'));
|
|
||||||
console.log('🌍 After refresh - has "Generated":', content.includes('Generated'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -605,46 +599,3 @@ Then('I should see the rankings table', async function () {
|
|||||||
await expect(world.page.locator('table')).toBeVisible();
|
await expect(world.page.locator('table')).toBeVisible();
|
||||||
console.log('🌍 Verified rankings table is visible');
|
console.log('🌍 Verified rankings table is visible');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Tournament Schedule Steps
|
|
||||||
Then('I should see round {int} matchups', async function (roundNumber: number) {
|
|
||||||
const roundText = `Round ${roundNumber}`;
|
|
||||||
// Wait a bit for content to load
|
|
||||||
await world.page.waitForTimeout(2000);
|
|
||||||
const content = await world.page.content();
|
|
||||||
console.log(`🌍 Page URL: ${world.page.url()}`);
|
|
||||||
console.log(`🌍 Page has "Round ${roundNumber}": ${content.includes(`Round ${roundNumber}`)}`);
|
|
||||||
console.log(`🌍 Page has "Generated": ${content.includes('Generated')}`);
|
|
||||||
|
|
||||||
await expect(world.page.locator(`text=${roundText}`)).toBeVisible({ timeout: 10000 });
|
|
||||||
console.log(`🌍 Verified round ${roundNumber} matchups are visible`);
|
|
||||||
});
|
|
||||||
|
|
||||||
Then('I should see {int} rounds', async function (expectedRounds: number) {
|
|
||||||
const roundHeaders = await world.page.locator('h3:has-text("Round")').count();
|
|
||||||
expect(roundHeaders).toBe(expectedRounds);
|
|
||||||
console.log(`🌍 Verified ${expectedRounds} rounds are visible`);
|
|
||||||
});
|
|
||||||
|
|
||||||
Then('each team should play every other team exactly once', async function () {
|
|
||||||
// This is a complex verification that would require counting matchups
|
|
||||||
// For now, just verify that the schedule was generated
|
|
||||||
const content = await world.page.content();
|
|
||||||
expect(content).toMatch(/schedule|round|matchup/i);
|
|
||||||
console.log('🌍 Verified schedule exists with matchups');
|
|
||||||
});
|
|
||||||
|
|
||||||
When('I click on a matchup', async function () {
|
|
||||||
// Wait for the matchup elements to be visible after potential page reload
|
|
||||||
const matchup = world.page.locator('[data-testid="matchup"]').first();
|
|
||||||
await matchup.waitFor({ state: 'visible', timeout: 15000 });
|
|
||||||
await matchup.click();
|
|
||||||
await world.page.waitForLoadState('domcontentloaded');
|
|
||||||
console.log('🌍 Clicked on matchup');
|
|
||||||
});
|
|
||||||
|
|
||||||
Then('I should be on the match result entry page', async function () {
|
|
||||||
const currentUrl = world.page.url();
|
|
||||||
console.log(`🌍 Checking current URL: ${currentUrl}`);
|
|
||||||
expect(currentUrl).toMatch(/\/matches\/|\/admin\/tournaments\/\d+\/results/);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -116,86 +116,11 @@ Before(async function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* After each scenario: Close page and clean up test data
|
* After each scenario: Close page
|
||||||
*/
|
*/
|
||||||
After(async function () {
|
After(async function () {
|
||||||
console.log('🌍 Cleaning up after scenario...');
|
console.log('🌍 Cleaning up after scenario...');
|
||||||
|
|
||||||
// Clean up test data from dev database
|
|
||||||
try {
|
|
||||||
const prisma = await world.getPrisma();
|
|
||||||
const dbUrl = process.env.DATABASE_URL || '';
|
|
||||||
|
|
||||||
// Safety check: only clean up dev/test databases
|
|
||||||
if (dbUrl.includes('_dev') || dbUrl.includes('test') || dbUrl.includes('ci')) {
|
|
||||||
// Use Prisma API for cleanup instead of raw SQL to avoid column name issues
|
|
||||||
|
|
||||||
// Find test tournaments first
|
|
||||||
const testTournaments = await prisma.event.findMany({
|
|
||||||
where: {
|
|
||||||
OR: [
|
|
||||||
{ name: { startsWith: 'Test Tournament' } },
|
|
||||||
{ name: { startsWith: 'Test Schedule Tournament' } }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
select: { id: true }
|
|
||||||
});
|
|
||||||
|
|
||||||
const tournamentIds = testTournaments.map(t => t.id);
|
|
||||||
|
|
||||||
if (tournamentIds.length > 0) {
|
|
||||||
// Delete bracket matchups via Prisma
|
|
||||||
await prisma.bracketMatchup.deleteMany({
|
|
||||||
where: {
|
|
||||||
round: {
|
|
||||||
eventId: { in: tournamentIds }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delete rounds
|
|
||||||
await prisma.tournamentRound.deleteMany({
|
|
||||||
where: { eventId: { in: tournamentIds } }
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delete event participants
|
|
||||||
await prisma.eventParticipant.deleteMany({
|
|
||||||
where: { eventId: { in: tournamentIds } }
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delete tournaments
|
|
||||||
await prisma.event.deleteMany({
|
|
||||||
where: { id: { in: tournamentIds } }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete test players
|
|
||||||
await prisma.player.deleteMany({
|
|
||||||
where: {
|
|
||||||
OR: [
|
|
||||||
{ name: { startsWith: 'Tournament Player' } },
|
|
||||||
{ name: { startsWith: 'Schedule Player' } },
|
|
||||||
{ name: { startsWith: 'Test Player' } },
|
|
||||||
{ name: { startsWith: 'Test Activity Player' } }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delete test users
|
|
||||||
await prisma.user.deleteMany({
|
|
||||||
where: {
|
|
||||||
email: { startsWith: 'cucumber-' }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('🌍 Test data cleaned up from dev database');
|
|
||||||
} else {
|
|
||||||
console.log('🌍 Skipping database cleanup (not a dev/test database)');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('🌍 Database cleanup error (non-critical):', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close page and context
|
// Close page and context
|
||||||
if (world.page) {
|
if (world.page) {
|
||||||
await world.page.close();
|
await world.page.close();
|
||||||
|
|||||||
@@ -60,11 +60,14 @@ export class World implements WorldState {
|
|||||||
if (!process.env.DATABASE_URL) {
|
if (!process.env.DATABASE_URL) {
|
||||||
throw new Error('DATABASE_URL not set. Make sure .env.development exists and contains DATABASE_URL or set DATABASE_URL environment variable.');
|
throw new Error('DATABASE_URL not set. Make sure .env.development exists and contains DATABASE_URL or set DATABASE_URL environment variable.');
|
||||||
}
|
}
|
||||||
|
process.env.DATABASE_PROVIDER = process.env.DATABASE_PROVIDER || 'postgresql';
|
||||||
|
|
||||||
// Use the shared prisma instance from the app's lib
|
// Import PrismaClient AFTER setting environment variables
|
||||||
// This handles the adapter setup correctly
|
const { PrismaClient } = await import('@prisma/client');
|
||||||
const { prisma } = require('@/lib/prisma');
|
const { PrismaPg } = await import('@prisma/adapter-pg');
|
||||||
this.prisma = prisma;
|
|
||||||
|
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
||||||
|
this.prisma = new PrismaClient({ adapter });
|
||||||
}
|
}
|
||||||
return this.prisma;
|
return this.prisma;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ test-cucumber-postgres-prod:
|
|||||||
@echo "Building application for production..."
|
@echo "Building application for production..."
|
||||||
bun run build
|
bun run build
|
||||||
@echo "Starting production server in background..."
|
@echo "Starting production server in background..."
|
||||||
DATABASE_URL=$(grep DATABASE_URL .env.development | cut -d'=' -f2 | tr -d '"') DATABASE_PROVIDER=postgresql bun run start > /tmp/next-prod.log 2>&1 &
|
bun run start > /tmp/next-prod.log 2>&1 &
|
||||||
SERVER_PID=$$!
|
SERVER_PID=$$!
|
||||||
@echo "Waiting for server to be ready..."
|
@echo "Waiting for server to be ready..."
|
||||||
sleep 15
|
sleep 15
|
||||||
@@ -212,6 +212,8 @@ help:
|
|||||||
clean:
|
clean:
|
||||||
@echo "Cleaning project..."
|
@echo "Cleaning project..."
|
||||||
rm -rf node_modules .next dist
|
rm -rf node_modules .next dist
|
||||||
|
@echo "Cleaning Docker artifacts..."
|
||||||
|
docker system prune -f
|
||||||
|
|
||||||
# Generate Prisma client
|
# Generate Prisma client
|
||||||
prisma-generate:
|
prisma-generate:
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "euchre_camp",
|
"name": "euchre_camp",
|
||||||
"version": "0.1.12",
|
"version": "0.1.13",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "NEXT_PUBLIC_GIT_COMMIT=$(git rev-parse --short HEAD) next dev",
|
"dev": "NEXT_PUBLIC_GIT_COMMIT=$(git rev-parse --short HEAD) next dev",
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import { prisma } from "@/lib/prisma"
|
|||||||
import Navigation from "@/components/Navigation"
|
import Navigation from "@/components/Navigation"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { notFound } from "next/navigation"
|
import { notFound } from "next/navigation"
|
||||||
import { ScheduleGenerator } from "@/components/ScheduleGenerator"
|
|
||||||
import { ScheduleDisplay } from "@/components/ScheduleDisplay"
|
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
@@ -11,9 +9,7 @@ interface PageProps {
|
|||||||
}>
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force dynamic rendering and revalidate on each request
|
|
||||||
export const dynamic = "force-dynamic"
|
export const dynamic = "force-dynamic"
|
||||||
export const revalidate = 0
|
|
||||||
|
|
||||||
export default async function TournamentSchedulePage({ params }: PageProps) {
|
export default async function TournamentSchedulePage({ params }: PageProps) {
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
@@ -31,21 +27,6 @@ export default async function TournamentSchedulePage({ params }: PageProps) {
|
|||||||
player: true,
|
player: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rounds: {
|
|
||||||
orderBy: { roundNumber: "asc" },
|
|
||||||
include: {
|
|
||||||
bracketMatchups: {
|
|
||||||
orderBy: { bracketPosition: "asc" },
|
|
||||||
include: {
|
|
||||||
player1P1: true,
|
|
||||||
player1P2: true,
|
|
||||||
player2P1: true,
|
|
||||||
player2P2: true,
|
|
||||||
match: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -53,9 +34,6 @@ export default async function TournamentSchedulePage({ params }: PageProps) {
|
|||||||
notFound()
|
notFound()
|
||||||
}
|
}
|
||||||
|
|
||||||
const teamCount = tournament.participants.length
|
|
||||||
const existingRounds = tournament.rounds.length
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
@@ -75,33 +53,22 @@ export default async function TournamentSchedulePage({ params }: PageProps) {
|
|||||||
Schedule - {tournament.name}
|
Schedule - {tournament.name}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div className="bg-white shadow rounded-lg p-6 mb-6">
|
<div className="bg-white shadow rounded-lg p-6">
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<h2 className="text-xl font-bold text-gray-900">
|
<h2 className="text-xl font-bold text-gray-900">
|
||||||
Tournament Schedule
|
Tournament Schedule
|
||||||
</h2>
|
</h2>
|
||||||
|
<button className="bg-green-600 text-white px-4 py-2 rounded-md text-sm font-medium hover:bg-green-700">
|
||||||
|
Generate Schedule
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="schedule-display">
|
<p className="text-gray-500">
|
||||||
{existingRounds > 0 ? (
|
No schedule has been generated yet. Click "Generate Schedule" to create round matchups.
|
||||||
<ScheduleDisplay rounds={tournament.rounds} />
|
</p>
|
||||||
) : (
|
|
||||||
<p className="text-gray-500 mb-6">
|
|
||||||
No schedule has been generated yet. Click "Generate Schedule" to create round matchups.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6 pt-6 border-t border-gray-200">
|
|
||||||
<ScheduleGenerator
|
|
||||||
tournamentId={tournamentId}
|
|
||||||
teamCount={teamCount}
|
|
||||||
existingRounds={existingRounds}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import Link from "next/link"
|
|
||||||
|
|
||||||
interface Player {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BracketMatchup {
|
|
||||||
id: number
|
|
||||||
player1P1: Player | null
|
|
||||||
player1P2: Player | null
|
|
||||||
player2P1: Player | null
|
|
||||||
player2P2: Player | null
|
|
||||||
match: { id: number } | null
|
|
||||||
bracketPosition: number | null
|
|
||||||
status: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TournamentRound {
|
|
||||||
id: number
|
|
||||||
roundNumber: number
|
|
||||||
status: string
|
|
||||||
bracketMatchups: BracketMatchup[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ScheduleDisplay({ rounds }: { rounds: TournamentRound[] }) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{rounds.map((round) => (
|
|
||||||
<div key={round.id} className="border border-gray-200 rounded-lg p-4">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-3">
|
|
||||||
Round {round.roundNumber}
|
|
||||||
<span className="ml-2 text-sm font-normal text-gray-500">
|
|
||||||
({round.status})
|
|
||||||
</span>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
{round.bracketMatchups.map((matchup) => {
|
|
||||||
const team1 = [
|
|
||||||
matchup.player1P1?.name,
|
|
||||||
matchup.player1P2?.name,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(" + ")
|
|
||||||
|
|
||||||
const team2 = [
|
|
||||||
matchup.player2P1?.name,
|
|
||||||
matchup.player2P2?.name,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(" + ")
|
|
||||||
|
|
||||||
const content = (
|
|
||||||
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
|
|
||||||
<div className="flex-1">
|
|
||||||
<span className="font-medium text-gray-900">
|
|
||||||
{team1 || "TBD"}
|
|
||||||
</span>
|
|
||||||
<span className="mx-3 text-gray-400">vs</span>
|
|
||||||
<span className="font-medium text-gray-900">
|
|
||||||
{team2 || "TBD"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-500">
|
|
||||||
{matchup.status === "pending" ? (
|
|
||||||
<span className="text-gray-400">Pending</span>
|
|
||||||
) : matchup.match ? (
|
|
||||||
<span className="text-green-600">Completed</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-gray-400">{matchup.status}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
if (matchup.match) {
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
key={matchup.id}
|
|
||||||
href={`/matches/${matchup.match.id}`}
|
|
||||||
className="block hover:bg-gray-100 rounded-md transition-colors"
|
|
||||||
data-testid="matchup"
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
key={matchup.id}
|
|
||||||
href={`/matches/new?matchup=${matchup.id}`}
|
|
||||||
className="block hover:bg-gray-100 rounded-md transition-colors"
|
|
||||||
data-testid="matchup"
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -47,6 +47,11 @@ export function ScheduleGenerator({ tournamentId, teamCount, existingRounds }: S
|
|||||||
matchupsCreated: data.matchupsCreated,
|
matchupsCreated: data.matchupsCreated,
|
||||||
})
|
})
|
||||||
setIsGenerating(false)
|
setIsGenerating(false)
|
||||||
|
|
||||||
|
// Reload to show the schedule
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload()
|
||||||
|
}, 1500)
|
||||||
} catch {
|
} catch {
|
||||||
setError("An error occurred. Please try again.")
|
setError("An error occurred. Please try again.")
|
||||||
setIsGenerating(false)
|
setIsGenerating(false)
|
||||||
|
|||||||
Reference in New Issue
Block a user