feat: Implement tournament schedule tab and fix E2E tests (#27)
Release / release (push) Failing after 9s
Build CI Images / build-ci-base (push) Failing after 18s

## Summary

This PR implements the tournament schedule tab functionality and fixes all remaining E2E test failures.

### Changes Included

1. **Tournament Schedule Feature**
   - Added tournament schedule page at `/admin/tournaments/[id]/schedule`
   - Implemented "Generate Schedule" button functionality
   - Added schedule generation logic for round-robin tournaments

2. **E2E Test Fixes**
   - Fixed database connection issues in production builds
   - Improved test reliability with better error handling and debugging
   - Updated test infrastructure to use environment variables instead of hardcoded values

3. **CI/CD Updates**
   - Added E2E test job to PR workflow
   - Configured tests to run against development database
   - Moved database password to Gitea secrets

4. **Code Quality**
   - Removed hardcoded passwords from codebase
   - Improved Prisma client configuration
   - Enhanced authentication and navigation components

### Test Results
All 16 E2E test scenarios are now passing:
- Authentication tests: 
- Registration tests: 
- Tournament schedule tests: 
- Player schedule tests: 
- Admin navigation tests: 

### Database Configuration
- Tests run against `euchre_camp_dev` database
- Production builds use environment variables for database configuration
- Database password stored in Gitea secrets as `DB_PASSWORD`

### CI Pipeline
The PR workflow now includes:
1. Unit tests
2. E2E tests (using production build)
3. Version bump analysis

E2E tests must pass before PR can be merged.

Reviewed-on: #27
Co-authored-by: David Gwilliam <dhgwilliam@gmail.com>
Co-committed-by: David Gwilliam <dhgwilliam@gmail.com>
This commit was merged in pull request #27.
This commit is contained in:
2026-04-27 01:59:16 +00:00
committed by david
parent 75358bf20d
commit bb6be245b7
97 changed files with 7950 additions and 1341 deletions
+103 -57
View File
@@ -9,6 +9,12 @@ interface MatchEditorProps {
matches: Match[]
targetScore: number | null
allowTies: boolean
// Optional pre-filled player IDs from URL params
prefilledP1?: number
prefilledP2?: number
prefilledP3?: number
prefilledP4?: number
prefilledRound?: number
}
interface MatchData {
@@ -23,15 +29,28 @@ interface MatchData {
isCasual: boolean
}
export default function MatchEditor({ tournamentId, players, targetScore, allowTies }: MatchEditorProps) {
export default function MatchEditor({
tournamentId,
players,
targetScore,
allowTies,
prefilledP1,
prefilledP2,
prefilledP3,
prefilledP4,
prefilledRound,
}: MatchEditorProps) {
// Check if players are prefilled from URL params
const hasPrefilledPlayers = prefilledP1 && prefilledP2 && prefilledP3 && prefilledP4;
const [formData, setFormData] = useState<MatchData>({
team1P1Id: null,
team1P2Id: null,
team2P1Id: null,
team2P2Id: null,
team1P1Id: hasPrefilledPlayers ? prefilledP1 : null,
team1P2Id: hasPrefilledPlayers ? prefilledP2 : null,
team2P1Id: hasPrefilledPlayers ? prefilledP3 : null,
team2P2Id: hasPrefilledPlayers ? prefilledP4 : null,
team1Score: 0,
team2Score: 0,
round: 1,
round: prefilledRound || 1,
table: "Clubs",
isCasual: false,
})
@@ -124,10 +143,13 @@ export default function MatchEditor({ tournamentId, players, targetScore, allowT
}),
})
const data = await response.json()
if (!response.ok) {
setError(data.error || "Failed to record match")
try {
const data = await response.json()
setError(data.error || "Failed to record match")
} catch {
setError(`Failed to record match: ${response.status} ${response.statusText}`)
}
setIsLoading(false)
return
}
@@ -214,35 +236,47 @@ export default function MatchEditor({ tournamentId, players, targetScore, allowT
<label htmlFor="team1P1Id" className="block text-sm font-medium text-gray-700">
Player 1
</label>
<select
id="team1P1Id"
name="team1P1Id"
value={formData.team1P1Id || ""}
onChange={handleChange}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-green-500 focus:ring-green-500 sm:text-sm"
>
<option value="">Select player...</option>
{players.map((player) => (
<option key={player.id} value={player.id}>{player.name}</option>
))}
</select>
{hasPrefilledPlayers ? (
<p className="mt-1 text-sm text-gray-600 bg-gray-50 px-3 py-2 rounded-md">
{players.find(p => p.id === prefilledP1)?.name || "Unknown Player"}
</p>
) : (
<select
id="team1P1Id"
name="team1P1Id"
value={formData.team1P1Id || ""}
onChange={handleChange}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-green-500 focus:ring-green-500 sm:text-sm"
>
<option value="">Select player...</option>
{players.map((player) => (
<option key={player.id} value={player.id}>{player.name}</option>
))}
</select>
)}
</div>
<div>
<label htmlFor="team1P2Id" className="block text-sm font-medium text-gray-700">
Player 2
</label>
<select
id="team1P2Id"
name="team1P2Id"
value={formData.team1P2Id || ""}
onChange={handleChange}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-green-500 focus:ring-green-500 sm:text-sm"
>
<option value="">Select player...</option>
{players.map((player) => (
<option key={player.id} value={player.id}>{player.name}</option>
))}
</select>
{hasPrefilledPlayers ? (
<p className="mt-1 text-sm text-gray-600 bg-gray-50 px-3 py-2 rounded-md">
{players.find(p => p.id === prefilledP2)?.name || "Unknown Player"}
</p>
) : (
<select
id="team1P2Id"
name="team1P2Id"
value={formData.team1P2Id || ""}
onChange={handleChange}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-green-500 focus:ring-green-500 sm:text-sm"
>
<option value="">Select player...</option>
{players.map((player) => (
<option key={player.id} value={player.id}>{player.name}</option>
))}
</select>
)}
</div>
</div>
<div className="mt-4">
@@ -270,35 +304,47 @@ export default function MatchEditor({ tournamentId, players, targetScore, allowT
<label htmlFor="team2P1Id" className="block text-sm font-medium text-gray-700">
Player 1
</label>
<select
id="team2P1Id"
name="team2P1Id"
value={formData.team2P1Id || ""}
onChange={handleChange}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-green-500 focus:ring-green-500 sm:text-sm"
>
<option value="">Select player...</option>
{players.map((player) => (
<option key={player.id} value={player.id}>{player.name}</option>
))}
</select>
{hasPrefilledPlayers ? (
<p className="mt-1 text-sm text-gray-600 bg-gray-50 px-3 py-2 rounded-md">
{players.find(p => p.id === prefilledP3)?.name || "Unknown Player"}
</p>
) : (
<select
id="team2P1Id"
name="team2P1Id"
value={formData.team2P1Id || ""}
onChange={handleChange}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-green-500 focus:ring-green-500 sm:text-sm"
>
<option value="">Select player...</option>
{players.map((player) => (
<option key={player.id} value={player.id}>{player.name}</option>
))}
</select>
)}
</div>
<div>
<label htmlFor="team2P2Id" className="block text-sm font-medium text-gray-700">
Player 2
</label>
<select
id="team2P2Id"
name="team2P2Id"
value={formData.team2P2Id || ""}
onChange={handleChange}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-green-500 focus:ring-green-500 sm:text-sm"
>
<option value="">Select player...</option>
{players.map((player) => (
<option key={player.id} value={player.id}>{player.name}</option>
))}
</select>
{hasPrefilledPlayers ? (
<p className="mt-1 text-sm text-gray-600 bg-gray-50 px-3 py-2 rounded-md">
{players.find(p => p.id === prefilledP4)?.name || "Unknown Player"}
</p>
) : (
<select
id="team2P2Id"
name="team2P2Id"
value={formData.team2P2Id || ""}
onChange={handleChange}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-green-500 focus:ring-green-500 sm:text-sm"
>
<option value="">Select player...</option>
{players.map((player) => (
<option key={player.id} value={player.id}>{player.name}</option>
))}
</select>
)}
</div>
</div>
<div className="mt-4">