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>
225 lines
8.8 KiB
TypeScript
225 lines
8.8 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import Navigation from "@/components/Navigation"
|
|
import { redirect } from "next/navigation"
|
|
import { getSession } from "@/lib/auth-simple"
|
|
|
|
interface ClubSettings {
|
|
id: number
|
|
clubName: string
|
|
defaultEloRating: number
|
|
partnershipEnabled: boolean
|
|
notificationsEnabled: boolean
|
|
matchVerification: boolean
|
|
}
|
|
|
|
export default function ClubSettingsPage() {
|
|
const [settings, setSettings] = useState<ClubSettings | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [saving, setSaving] = useState(false)
|
|
const [error, setError] = useState("")
|
|
const [success, setSuccess] = useState("")
|
|
|
|
const [formSettings, setFormSettings] = useState<Partial<ClubSettings>>({})
|
|
|
|
useEffect(() => {
|
|
fetchSettings()
|
|
}, [])
|
|
|
|
const fetchSettings = async () => {
|
|
try {
|
|
const response = await fetch("/api/admin/settings")
|
|
if (!response.ok) {
|
|
throw new Error("Failed to fetch settings")
|
|
}
|
|
const data = await response.json()
|
|
setSettings(data)
|
|
setFormSettings(data || {})
|
|
} catch (err: unknown) {
|
|
setError(err instanceof Error ? err.message : "Failed to fetch settings")
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleSave = async () => {
|
|
setSaving(true)
|
|
setError("")
|
|
setSuccess("")
|
|
|
|
try {
|
|
const response = await fetch("/api/admin/settings", {
|
|
method: "PATCH",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(formSettings),
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json()
|
|
throw new Error(errorData.error || "Failed to save settings")
|
|
}
|
|
|
|
const updatedSettings = await response.json()
|
|
setSettings(updatedSettings)
|
|
setSuccess("Settings saved successfully!")
|
|
} catch (err: unknown) {
|
|
setError(err instanceof Error ? err.message : "Failed to save settings")
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen bg-gray-50">
|
|
<Navigation />
|
|
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
|
<div className="px-4 py-6 sm:px-0">
|
|
<p className="text-gray-500">Loading settings...</p>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50">
|
|
<Navigation />
|
|
|
|
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
|
<div className="px-4 py-6 sm:px-0">
|
|
{/* Page Header */}
|
|
<div className="bg-white shadow rounded-lg p-6 mb-6">
|
|
<h1 className="text-2xl font-bold text-gray-900">Club Settings</h1>
|
|
<p className="text-gray-500 mt-1">
|
|
Configure your club's default settings and preferences.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Settings Form */}
|
|
<div className="bg-white shadow rounded-lg p-6">
|
|
{error && (
|
|
<div className="mb-4 p-4 bg-red-100 text-red-700 rounded">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{success && (
|
|
<div className="mb-4 p-4 bg-green-100 text-green-700 rounded">
|
|
{success}
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-6">
|
|
{/* Club Name */}
|
|
<div>
|
|
<label htmlFor="clubName" className="block text-sm font-medium text-gray-700 mb-2">
|
|
Club Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="clubName"
|
|
value={formSettings.clubName || ""}
|
|
onChange={(e) => setFormSettings({ ...formSettings, clubName: e.target.value })}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500"
|
|
/>
|
|
</div>
|
|
|
|
{/* Default Elo Rating */}
|
|
<div>
|
|
<label htmlFor="defaultEloRating" className="block text-sm font-medium text-gray-700 mb-2">
|
|
Default Elo Rating
|
|
</label>
|
|
<input
|
|
type="number"
|
|
id="defaultEloRating"
|
|
value={formSettings.defaultEloRating || 1000}
|
|
onChange={(e) => setFormSettings({ ...formSettings, defaultEloRating: parseInt(e.target.value) })}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500"
|
|
/>
|
|
</div>
|
|
|
|
{/* Partnership Tracking */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<label className="text-sm font-medium text-gray-700">Partnership Tracking</label>
|
|
<p className="text-sm text-gray-500">Enable partnership performance analytics</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => setFormSettings({ ...formSettings, partnershipEnabled: !formSettings.partnershipEnabled })}
|
|
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${
|
|
formSettings.partnershipEnabled ? 'bg-green-600' : 'bg-gray-200'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
|
formSettings.partnershipEnabled ? 'translate-x-5' : 'translate-x-0'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Notifications */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<label className="text-sm font-medium text-gray-700">Notifications</label>
|
|
<p className="text-sm text-gray-500">Send email notifications for updates</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => setFormSettings({ ...formSettings, notificationsEnabled: !formSettings.notificationsEnabled })}
|
|
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${
|
|
formSettings.notificationsEnabled ? 'bg-green-600' : 'bg-gray-200'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
|
formSettings.notificationsEnabled ? 'translate-x-5' : 'translate-x-0'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Match Verification */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<label className="text-sm font-medium text-gray-700">Match Verification</label>
|
|
<p className="text-sm text-gray-500">Require admin verification for match results</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => setFormSettings({ ...formSettings, matchVerification: !formSettings.matchVerification })}
|
|
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${
|
|
formSettings.matchVerification ? 'bg-green-600' : 'bg-gray-200'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
|
formSettings.matchVerification ? 'translate-x-5' : 'translate-x-0'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Save Button */}
|
|
<div className="mt-8 flex justify-end">
|
|
<button
|
|
onClick={handleSave}
|
|
disabled={saving}
|
|
className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{saving ? 'Saving...' : 'Save Settings'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
)
|
|
}
|