Files
euchre_camp/src/app/admin/settings/page.tsx
T
david bb6be245b7
Release / release (push) Failing after 9s
Build CI Images / build-ci-base (push) Failing after 18s
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>
2026-04-27 01:59:16 +00:00

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>
)
}