feat: Implement tournament schedule tab and fix E2E tests (#27)
## 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:
+103
-57
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user