You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

459 lines
16 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { Loader2, AlertCircle, CheckCircle } from 'lucide-react';
import { getClubProfile, updateClubProfile } from '@/src/lib/api/courts';
import type { ClubProfile, ClubProfileUpdateRequest} from '@/src/types/courts';
import { COMMON_TIMEZONES, isValidEmail, isValidUrl } from '@/src/types/courts';
interface ClubProfileTabProps {
clubId: number;
onUpdate?: () => void;
}
export default function ClubProfileTab({ clubId, onUpdate }: ClubProfileTabProps) {
const [profile, setProfile] = useState<ClubProfile | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
// Form state
const [name, setName] = useState('');
const [timezone, setTimezone] = useState('');
const [addressLine1, setAddressLine1] = useState('');
const [addressLine2, setAddressLine2] = useState('');
const [city, setCity] = useState('');
const [postalCode, setPostalCode] = useState('');
const [country, setCountry] = useState('');
const [phone, setPhone] = useState('');
const [email, setEmail] = useState('');
const [website, setWebsite] = useState('');
useEffect(() => {
loadProfile();
}, [clubId]);
async function loadProfile() {
setLoading(true);
const result = await getClubProfile(clubId);
if (result.success) {
const prof = result.data;
setProfile(prof);
// Populate form
setName(prof.name);
setTimezone(prof.timezone);
setAddressLine1(prof.address_line_1 || '');
setAddressLine2(prof.address_line_2 || '');
setCity(prof.city || '');
setPostalCode(prof.postal_code || '');
setCountry(prof.country || '');
setPhone(prof.phone || '');
setEmail(prof.email || '');
setWebsite(prof.website || '');
setError(null);
} else {
setError(result.error.detail);
}
setLoading(false);
}
function validateForm(): boolean {
const newErrors: Record<string, string> = {};
if (!name || name.trim().length === 0) {
newErrors.name = 'Name is required';
}
if (!timezone) {
newErrors.timezone = 'Timezone is required';
}
if (email && !isValidEmail(email)) {
newErrors.email = 'Invalid email format';
}
if (website && !isValidUrl(website)) {
newErrors.website = 'Invalid URL format (must include http:// or https://)';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!validateForm()) {
return;
}
setSaving(true);
setError(null);
setSuccess(false);
const request: ClubProfileUpdateRequest = {
name: name.trim(),
timezone,
address_line_1: addressLine1.trim() || undefined,
address_line_2: addressLine2.trim() || undefined,
city: city.trim() || undefined,
postal_code: postalCode.trim() || undefined,
country: country.trim() || undefined,
phone: phone.trim() || undefined,
email: email.trim() || undefined,
website: website.trim() || undefined,
};
const result = await updateClubProfile(clubId, request);
if (result.success) {
setProfile(result.data);
setSuccess(true);
setError(null);
// Call parent update callback
if (onUpdate) {
onUpdate();
}
// Hide success message after 3 seconds
setTimeout(() => setSuccess(false), 3000);
} else {
// Handle validation errors
if (result.error.code === 'validation_error' && result.error.errors) {
const fieldErrors: Record<string, string> = {};
result.error.errors.forEach(err => {
fieldErrors[err.field] = err.message;
});
setErrors(fieldErrors);
} else {
setError(result.error.detail);
}
}
setSaving(false);
}
function handleCancel() {
if (!profile) return;
// Reset form to original values
setName(profile.name);
setTimezone(profile.timezone);
setAddressLine1(profile.address_line_1 || '');
setAddressLine2(profile.address_line_2 || '');
setCity(profile.city || '');
setPostalCode(profile.postal_code || '');
setCountry(profile.country || '');
setPhone(profile.phone || '');
setEmail(profile.email || '');
setWebsite(profile.website || '');
setErrors({});
setError(null);
}
// Loading state
if (loading) {
return (
<div className="flex items-center justify-center py-20">
<Loader2 className="w-8 h-8 text-slate-600 animate-spin" />
</div>
);
}
// Error state
if (error && !profile) {
return (
<div className="bg-red-50 border-2 border-red-200 rounded-2xl p-6">
<div className="flex items-start space-x-3">
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
<p className="text-red-700">{error}</p>
</div>
</div>
);
}
if (!profile) return null;
return (
<div className="max-w-4xl">
{/* Success message */}
{success && (
<div className="mb-6 bg-green-50 border-2 border-green-200 rounded-2xl p-6">
<div className="flex items-center space-x-3">
<CheckCircle className="w-5 h-5 text-green-600" />
<p className="text-green-700 font-medium">Profile updated successfully</p>
</div>
</div>
)}
{/* Error message */}
{error && (
<div className="mb-6 bg-red-50 border-2 border-red-200 rounded-2xl p-6">
<div className="flex items-start space-x-3">
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
<p className="text-red-700">{error}</p>
</div>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-8">
{/* Basic Information */}
<section className="bg-white border-2 border-slate-200 rounded-2xl p-6">
<h3 className="text-xl font-bold text-slate-900 mb-6">Basic Information</h3>
<div className="space-y-4">
{/* Name */}
<div>
<label className="block text-sm font-semibold text-slate-900 mb-2">
Name <span className="text-red-600">*</span>
</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className={`w-full px-4 py-3 border-2 rounded-lg font-medium transition-colors ${
errors.name
? 'border-red-300 focus:border-red-500'
: 'border-slate-200 focus:border-slate-900'
} focus:outline-none`}
disabled={saving}
/>
{errors.name && (
<p className="mt-1 text-sm text-red-600">{errors.name}</p>
)}
</div>
{/* Timezone */}
<div>
<label className="block text-sm font-semibold text-slate-900 mb-2">
Timezone <span className="text-red-600">*</span>
</label>
<select
value={timezone}
onChange={(e) => setTimezone(e.target.value)}
className={`w-full px-4 py-3 border-2 rounded-lg font-medium transition-colors ${
errors.timezone
? 'border-red-300 focus:border-red-500'
: 'border-slate-200 focus:border-slate-900'
} focus:outline-none`}
disabled={saving}
>
{COMMON_TIMEZONES.map((tz) => (
<option key={tz.value} value={tz.value}>
{tz.label}
</option>
))}
</select>
{errors.timezone && (
<p className="mt-1 text-sm text-red-600">{errors.timezone}</p>
)}
</div>
</div>
</section>
{/* Location */}
<section className="bg-white border-2 border-slate-200 rounded-2xl p-6">
<h3 className="text-xl font-bold text-slate-900 mb-6">Location</h3>
<div className="space-y-4">
{/* Address Line 1 */}
<div>
<label className="block text-sm font-semibold text-slate-900 mb-2">
Address Line 1
</label>
<input
type="text"
value={addressLine1}
onChange={(e) => setAddressLine1(e.target.value)}
placeholder="123 High Street"
maxLength={200}
className="w-full px-4 py-3 border-2 border-slate-200 rounded-lg font-medium focus:border-slate-900 focus:outline-none transition-colors"
disabled={saving}
/>
</div>
{/* Address Line 2 */}
<div>
<label className="block text-sm font-semibold text-slate-900 mb-2">
Address Line 2
</label>
<input
type="text"
value={addressLine2}
onChange={(e) => setAddressLine2(e.target.value)}
placeholder="Building A"
maxLength={200}
className="w-full px-4 py-3 border-2 border-slate-200 rounded-lg font-medium focus:border-slate-900 focus:outline-none transition-colors"
disabled={saving}
/>
</div>
{/* City & Postal Code */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-semibold text-slate-900 mb-2">
City
</label>
<input
type="text"
value={city}
onChange={(e) => setCity(e.target.value)}
placeholder="London"
maxLength={100}
className="w-full px-4 py-3 border-2 border-slate-200 rounded-lg font-medium focus:border-slate-900 focus:outline-none transition-colors"
disabled={saving}
/>
</div>
<div>
<label className="block text-sm font-semibold text-slate-900 mb-2">
Postal Code
</label>
<input
type="text"
value={postalCode}
onChange={(e) => setPostalCode(e.target.value)}
placeholder="SW1A 1AA"
maxLength={20}
className="w-full px-4 py-3 border-2 border-slate-200 rounded-lg font-medium focus:border-slate-900 focus:outline-none transition-colors"
disabled={saving}
/>
</div>
</div>
{/* Country */}
<div>
<label className="block text-sm font-semibold text-slate-900 mb-2">
Country
</label>
<input
type="text"
value={country}
onChange={(e) => setCountry(e.target.value)}
placeholder="United Kingdom"
maxLength={100}
className="w-full px-4 py-3 border-2 border-slate-200 rounded-lg font-medium focus:border-slate-900 focus:outline-none transition-colors"
disabled={saving}
/>
</div>
</div>
</section>
{/* Contact */}
<section className="bg-white border-2 border-slate-200 rounded-2xl p-6">
<h3 className="text-xl font-bold text-slate-900 mb-6">Contact</h3>
<div className="space-y-4">
{/* Phone */}
<div>
<label className="block text-sm font-semibold text-slate-900 mb-2">
Phone
</label>
<input
type="tel"
value={phone}
onChange={(e) => setPhone(e.target.value)}
placeholder="+44 20 1234 5678"
maxLength={50}
className="w-full px-4 py-3 border-2 border-slate-200 rounded-lg font-medium focus:border-slate-900 focus:outline-none transition-colors"
disabled={saving}
/>
</div>
{/* Email */}
<div>
<label className="block text-sm font-semibold text-slate-900 mb-2">
Email
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="info@centralpadel.com"
maxLength={100}
className={`w-full px-4 py-3 border-2 rounded-lg font-medium transition-colors ${
errors.email
? 'border-red-300 focus:border-red-500'
: 'border-slate-200 focus:border-slate-900'
} focus:outline-none`}
disabled={saving}
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">{errors.email}</p>
)}
</div>
{/* Website */}
<div>
<label className="block text-sm font-semibold text-slate-900 mb-2">
Website
</label>
<input
type="url"
value={website}
onChange={(e) => setWebsite(e.target.value)}
placeholder="https://www.centralpadel.com"
maxLength={200}
className={`w-full px-4 py-3 border-2 rounded-lg font-medium transition-colors ${
errors.website
? 'border-red-300 focus:border-red-500'
: 'border-slate-200 focus:border-slate-900'
} focus:outline-none`}
disabled={saving}
/>
{errors.website && (
<p className="mt-1 text-sm text-red-600">{errors.website}</p>
)}
</div>
</div>
</section>
{/* Integration (read-only) */}
<section className="bg-slate-50 border-2 border-slate-200 rounded-2xl p-6">
<h3 className="text-xl font-bold text-slate-900 mb-6">Integration</h3>
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-slate-600 font-medium">Provider:</span>
<span className="font-semibold text-slate-900">{profile.provider}</span>
</div>
{profile.provider !== 'local' && profile.remote_club_id && (
<div className="flex justify-between items-center">
<span className="text-slate-600 font-medium">Remote Club ID:</span>
<span className="font-mono text-slate-900">{profile.remote_club_id}</span>
</div>
)}
</div>
</section>
{/* Actions */}
<div className="flex items-center justify-end space-x-3 pt-4">
<button
type="button"
onClick={handleCancel}
className="px-6 py-3 text-slate-700 font-semibold rounded-lg hover:bg-slate-100 transition-colors"
disabled={saving}
>
Cancel
</button>
<button
type="submit"
className="inline-flex items-center px-6 py-3 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
disabled={saving}
>
{saving && <Loader2 className="w-4 h-4 mr-2 animate-spin" />}
Save Changes
</button>
</div>
</form>
</div>
);
}