bb6be245b7
## 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>
180 lines
5.9 KiB
TypeScript
180 lines
5.9 KiB
TypeScript
"use client"
|
|
|
|
import { useState } from "react"
|
|
import { useRouter } from "next/navigation"
|
|
import Link from "next/link"
|
|
import type { Player } from "@prisma/client"
|
|
|
|
interface CreateUserFormProps {
|
|
selectedPlayer: Player | null;
|
|
availablePlayers: Player[];
|
|
}
|
|
|
|
export default function CreateUserForm({ selectedPlayer, availablePlayers }: CreateUserFormProps) {
|
|
const router = useRouter()
|
|
const [formData, setFormData] = useState({
|
|
email: "",
|
|
password: "",
|
|
name: "",
|
|
role: "player",
|
|
playerId: selectedPlayer?.id?.toString() || "",
|
|
})
|
|
const [error, setError] = useState("")
|
|
const [success, setSuccess] = useState("")
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
setError("")
|
|
setSuccess("")
|
|
setLoading(true)
|
|
|
|
try {
|
|
const response = await fetch("/api/admin/users", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(formData),
|
|
})
|
|
|
|
if (!response.ok) {
|
|
try {
|
|
const errorData = await response.json()
|
|
throw new Error(errorData.error || "Failed to create user")
|
|
} catch (jsonError) {
|
|
if (jsonError instanceof Error && jsonError.message !== "Failed to create user") {
|
|
throw jsonError
|
|
}
|
|
throw new Error(`Failed to create user: ${response.status} ${response.statusText}`)
|
|
}
|
|
}
|
|
|
|
setSuccess("User created successfully!")
|
|
setTimeout(() => {
|
|
router.push("/admin/users")
|
|
}, 1500)
|
|
} catch (err) {
|
|
if (err instanceof Error) {
|
|
setError(err.message)
|
|
} else {
|
|
setError("An unknown error occurred")
|
|
}
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
{error && (
|
|
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
|
|
{error}
|
|
</div>
|
|
)}
|
|
{success && (
|
|
<div className="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded">
|
|
{success}
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div>
|
|
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
|
Email <span className="text-red-500">*</span>
|
|
</label>
|
|
<input
|
|
type="email"
|
|
id="email"
|
|
required
|
|
value={formData.email}
|
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-green-500 focus:ring-green-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
|
|
Password <span className="text-red-500">*</span>
|
|
</label>
|
|
<input
|
|
type="password"
|
|
id="password"
|
|
required
|
|
value={formData.password}
|
|
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-green-500 focus:ring-green-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
|
Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="name"
|
|
value={formData.name}
|
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-green-500 focus:ring-green-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="role" className="block text-sm font-medium text-gray-700">
|
|
Role <span className="text-red-500">*</span>
|
|
</label>
|
|
<select
|
|
id="role"
|
|
required
|
|
value={formData.role}
|
|
onChange={(e) => setFormData({ ...formData, role: e.target.value })}
|
|
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="player">Player</option>
|
|
<option value="tournament_admin">Tournament Admin</option>
|
|
<option value="club_admin">Club Admin</option>
|
|
<option value="site_admin">Site Admin</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="md:col-span-2">
|
|
<label htmlFor="playerId" className="block text-sm font-medium text-gray-700">
|
|
Associate with Player
|
|
</label>
|
|
<select
|
|
id="playerId"
|
|
value={formData.playerId}
|
|
onChange={(e) => setFormData({ ...formData, playerId: e.target.value })}
|
|
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="">-- No player association --</option>
|
|
{selectedPlayer && (
|
|
<option value={selectedPlayer.id}>{selectedPlayer.name} (selected)</option>
|
|
)}
|
|
{availablePlayers.map((player) => (
|
|
<option key={player.id} value={player.id}>
|
|
{player.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end space-x-3">
|
|
<Link
|
|
href="/admin/users"
|
|
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
|
|
>
|
|
Cancel
|
|
</Link>
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors disabled:opacity-50"
|
|
>
|
|
{loading ? "Creating..." : "Create User"}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
)
|
|
}
|