refactor: add shared club detail layout with persistent tab navigation
continuous-integration/drone/push Build is passing Details

- Create ClubTabNavigation component with responsive horizontal scroll
- Create ClubDetailHeader component with shared layout (breadcrumb, header, tabs)
- Add layout.tsx for club_id route to wrap all sub-pages
- Extract Courts tab into separate page route
- Remove redundant AdminAuthGuard from individual pages (now in layout)
- Remove container wrappers from components (now in shared header)
- Remove "Back to club" links (breadcrumb now in header)
- Standardize colors: gray-* to slate-* for consistent dark text styling
master
Guillermo Pages 2 months ago
parent 308d9d70bf
commit 256eccc997

@ -1,252 +1,11 @@
'use client';
import { useState, useEffect } from 'react';
import { ArrowLeft, Loader2, AlertCircle, Lock } from 'lucide-react';
import Link from 'next/link';
import { getAdminClubDetail } from '@/src/lib/api/admin-clubs';
import type { AdminClubDetail, AdminApiError } from '@/src/types/admin-api';
import useTranslation from '@/src/hooks/useTranslation';
import ClubProfileTab from './tabs/ClubProfileTab';
import ClubCourtsTab from './tabs/ClubCourtsTab';
interface ClubDetailTabsProps {
clubId: number;
}
type TabKey = 'profile' | 'courts' | 'slot-definitions';
export default function ClubDetailTabs({ clubId }: ClubDetailTabsProps) {
const { t, locale } = useTranslation();
const [clubDetail, setClubDetail] = useState<AdminClubDetail | null>(null);
const [error, setError] = useState<AdminApiError | null>(null);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState<TabKey>('profile');
useEffect(() => {
loadClubDetail();
}, [clubId]);
async function loadClubDetail() {
setLoading(true);
const result = await getAdminClubDetail(clubId);
if (result.success) {
setClubDetail(result.data);
setError(null);
} else {
setError(result.error);
setClubDetail(null);
}
setLoading(false);
}
// Loading state
if (loading) {
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
<div className="flex flex-col items-center justify-center space-y-4">
<Loader2 className="w-12 h-12 text-slate-900 animate-spin" />
<p className="text-slate-600 font-medium">Loading club details...</p>
</div>
</div>
);
}
// Authentication error (401)
if (error && error.status === 401) {
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
<div className="max-w-2xl mx-auto">
<div className="bg-amber-50 border-2 border-amber-200 rounded-2xl p-8">
<div className="flex items-start space-x-4">
<Lock className="w-8 h-8 text-amber-700 flex-shrink-0 mt-1" />
<div>
<h2 className="text-2xl font-bold text-slate-900 mb-4">
Authentication Required
</h2>
<p className="text-slate-700 leading-relaxed">
Please log in to access the venue management portal.
</p>
</div>
</div>
</div>
</div>
</div>
);
}
// Forbidden error (403)
if (error && error.status === 403) {
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
<div className="max-w-2xl mx-auto">
<div className="bg-red-50 border-2 border-red-200 rounded-2xl p-8">
<div className="flex items-start space-x-4">
<Lock className="w-8 h-8 text-red-600 flex-shrink-0 mt-1" />
<div>
<h2 className="text-2xl font-bold text-slate-900 mb-4">
Access Denied
</h2>
<p className="text-slate-700 mb-6 leading-relaxed">
{error.detail}
</p>
<Link
href={`/${locale}/admin/clubs`}
className="inline-flex items-center px-4 py-2 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to clubs
</Link>
</div>
</div>
</div>
</div>
</div>
);
}
// Other API errors
if (error) {
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
<div className="max-w-2xl mx-auto">
<div className="bg-red-50 border-2 border-red-200 rounded-2xl p-8">
<div className="flex items-start space-x-4">
<AlertCircle className="w-8 h-8 text-red-600 flex-shrink-0 mt-1" />
<div className="flex-1">
<h2 className="text-2xl font-bold text-slate-900 mb-4">
Error Loading Club
</h2>
<p className="text-slate-700 mb-4 leading-relaxed">
{error.detail}
</p>
<p className="text-sm text-slate-600 font-mono mb-6">
Error code: {error.code}
</p>
<Link
href={`/${locale}/admin/clubs`}
className="inline-flex items-center px-4 py-2 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to clubs
</Link>
</div>
</div>
</div>
</div>
</div>
);
}
if (!clubDetail) {
return null;
}
// Success - render tabbed interface
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
{/* Breadcrumb */}
<div className="mb-8">
<Link
href={`/${locale}/admin/clubs`}
className="inline-flex items-center text-slate-600 hover:text-slate-900 font-medium transition-colors"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to clubs
</Link>
</div>
{/* Club Header */}
<div className="mb-8">
<h1 className="text-4xl font-bold text-slate-900 mb-2 tracking-tight">
{clubDetail.facility.name}
</h1>
<p className="text-lg text-slate-600 font-light">
{clubDetail.facility.timezone}
</p>
</div>
{/* Tab Navigation */}
<div className="border-b-2 border-slate-200 mb-8">
<div className="flex space-x-1">
<button
onClick={() => setActiveTab('profile')}
className={`px-6 py-3 font-semibold transition-colors border-b-2 ${
activeTab === 'profile'
? 'border-slate-900 text-slate-900'
: 'border-transparent text-slate-600 hover:text-slate-900 hover:border-slate-300'
}`}
>
Profile
</button>
<button
onClick={() => setActiveTab('courts')}
className={`px-6 py-3 font-semibold transition-colors border-b-2 ${
activeTab === 'courts'
? 'border-slate-900 text-slate-900'
: 'border-transparent text-slate-600 hover:text-slate-900 hover:border-slate-300'
}`}
>
Courts
</button>
<Link
href={`/${locale}/admin/clubs/${clubId}/slot-definitions`}
className="px-6 py-3 font-semibold text-slate-600 hover:text-slate-900 hover:border-slate-300 transition-colors border-b-2 border-transparent"
>
Slot Definitions
</Link>
<Link
href={`/${locale}/admin/clubs/${clubId}/slot-instances`}
className="px-6 py-3 font-semibold text-slate-600 hover:text-slate-900 hover:border-slate-300 transition-colors border-b-2 border-transparent"
>
Slot Instances
</Link>
<Link
href={`/${locale}/admin/clubs/${clubId}/plans`}
className="px-6 py-3 font-semibold text-purple-600 hover:text-purple-700 hover:border-purple-300 transition-colors border-b-2 border-transparent"
>
Plans
</Link>
<Link
href={`/${locale}/admin/clubs/${clubId}/members`}
className="px-6 py-3 font-semibold text-purple-600 hover:text-purple-700 hover:border-purple-300 transition-colors border-b-2 border-transparent"
>
Members
</Link>
<Link
href={`/${locale}/admin/clubs/${clubId}/settings`}
className="px-6 py-3 font-semibold text-purple-600 hover:text-purple-700 hover:border-purple-300 transition-colors border-b-2 border-transparent"
>
Settings
</Link>
<Link
href={`/${locale}/admin/clubs/${clubId}/credits`}
className="px-6 py-3 font-semibold text-emerald-600 hover:text-emerald-700 hover:border-emerald-300 transition-colors border-b-2 border-transparent"
>
Credits
</Link>
<Link
href={`/${locale}/admin/clubs/${clubId}/transfers`}
className="px-6 py-3 font-semibold text-amber-600 hover:text-amber-700 hover:border-amber-300 transition-colors border-b-2 border-transparent"
>
Transfers
</Link>
</div>
</div>
{/* Tab Content */}
{activeTab === 'profile' && (
<ClubProfileTab clubId={clubId} onUpdate={loadClubDetail} />
)}
{activeTab === 'courts' && (
<ClubCourtsTab
clubId={clubId}
courts={clubDetail.courts}
onUpdate={loadClubDetail}
/>
)}
</div>
);
return <ClubProfileTab clubId={clubId} />;
}

@ -0,0 +1,47 @@
'use client';
import { useState, useEffect } from 'react';
import { Loader2 } from 'lucide-react';
import { getAdminClubDetail } from '@/src/lib/api/admin-clubs';
import type { Court } from '@/src/types/courts';
import ClubCourtsTab from '../tabs/ClubCourtsTab';
interface CourtsComponentProps {
clubId: number;
}
export default function CourtsComponent({ clubId }: CourtsComponentProps) {
const [courts, setCourts] = useState<Court[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadCourts();
}, [clubId]);
async function loadCourts() {
setLoading(true);
const result = await getAdminClubDetail(clubId);
if (result.success) {
setCourts(result.data.courts);
}
setLoading(false);
}
if (loading) {
return (
<div className="flex items-center justify-center py-20">
<Loader2 className="w-8 h-8 text-slate-600 animate-spin" />
</div>
);
}
return (
<ClubCourtsTab
clubId={clubId}
courts={courts}
onUpdate={loadCourts}
/>
);
}

@ -0,0 +1,12 @@
import CourtsComponent from './CourtsComponent';
export default async function CourtsPage({
params
}: {
params: Promise<{ club_id: string }>
}) {
const { club_id } = await params;
const clubId = parseInt(club_id, 10);
return <CourtsComponent clubId={clubId} />;
}

@ -1,11 +1,9 @@
'use client';
import { useState, useEffect } from 'react';
import { ArrowLeft, Loader2, Wallet, Search, ChevronRight } from 'lucide-react';
import Link from 'next/link';
import { Loader2, Wallet, Search, ChevronRight } from 'lucide-react';
import { listMemberCredits } from '@/src/lib/api/facility-admin';
import type { MemberCreditBalance } from '@/src/types/facility-admin';
import useTranslation from '@/src/hooks/useTranslation';
import CreditDetailModal from './CreditDetailModal';
interface CreditsManagementComponentProps {
@ -13,7 +11,6 @@ interface CreditsManagementComponentProps {
}
export default function CreditsManagementComponent({ clubId }: CreditsManagementComponentProps) {
const { locale } = useTranslation();
const [credits, setCredits] = useState<MemberCreditBalance[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@ -57,20 +54,13 @@ export default function CreditsManagementComponent({ clubId }: CreditsManagement
const totalBalance = credits.reduce((sum, c) => sum + c.balance_cents, 0);
return (
<div className="container mx-auto px-4 py-8">
<div>
{/* Header */}
<div className="mb-6">
<Link
href={`/${locale}/admin/clubs/${clubId}`}
className="inline-flex items-center text-gray-600 hover:text-gray-800 font-medium mb-4"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Club
</Link>
<div className="flex justify-between items-start">
<div>
<h1 className="text-3xl font-bold text-gray-800">Member Credits</h1>
<p className="text-gray-600 mt-1">View and manage member credit balances</p>
<h1 className="text-2xl font-bold text-slate-900">Member Credits</h1>
<p className="text-slate-600 mt-1">View and manage member credit balances</p>
</div>
<div className="bg-gradient-to-br from-emerald-500 to-teal-600 rounded-xl p-4 text-white shadow-lg">
<div className="flex items-center gap-2 mb-1">
@ -90,21 +80,21 @@ export default function CreditsManagementComponent({ clubId }: CreditsManagement
{/* Filters */}
<div className="flex flex-col sm:flex-row gap-4 mb-6">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400 w-5 h-5" />
<input
type="text"
placeholder="Search by name or email..."
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
className="w-full pl-10 pr-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-slate-500"
/>
</div>
<label className="flex items-center gap-2 text-sm text-gray-700">
<label className="flex items-center gap-2 text-sm text-slate-700">
<input
type="checkbox"
checked={showZeroBalances}
onChange={e => setShowZeroBalances(e.target.checked)}
className="w-4 h-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500"
className="w-4 h-4 text-slate-600 border-slate-300 rounded focus:ring-slate-500"
/>
Show zero balances
</label>
@ -120,60 +110,60 @@ export default function CreditsManagementComponent({ clubId }: CreditsManagement
{/* Credits List */}
{loading ? (
<div className="flex items-center justify-center py-16">
<Loader2 className="w-8 h-8 text-purple-600 animate-spin" />
<span className="ml-2 text-gray-600">Loading credits...</span>
<Loader2 className="w-8 h-8 text-slate-600 animate-spin" />
<span className="ml-2 text-slate-600">Loading credits...</span>
</div>
) : filteredCredits.length === 0 ? (
<div className="text-center py-16 bg-gray-50 rounded-xl">
<div className="text-center py-16 bg-slate-50 rounded-xl">
<div className="text-6xl mb-4">
<Wallet className="w-16 h-16 mx-auto text-gray-400" />
<Wallet className="w-16 h-16 mx-auto text-slate-400" />
</div>
<h2 className="text-xl font-semibold text-gray-700 mb-2">
<h2 className="text-xl font-semibold text-slate-900 mb-2">
{searchQuery ? 'No matching members' : 'No credit balances'}
</h2>
<p className="text-gray-500">
<p className="text-slate-600">
{searchQuery
? 'Try a different search term'
: 'Members with credit balances will appear here'}
</p>
</div>
) : (
<div className="bg-white border border-gray-200 rounded-xl shadow-sm overflow-hidden">
<div className="bg-white border border-slate-200 rounded-xl shadow-sm overflow-hidden">
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<thead className="bg-slate-50 border-b border-slate-200">
<tr>
<th className="text-left px-6 py-3 text-sm font-semibold text-gray-600">Member</th>
<th className="text-right px-6 py-3 text-sm font-semibold text-gray-600">Balance</th>
<th className="text-right px-6 py-3 text-sm font-semibold text-gray-600">Updated</th>
<th className="text-left px-6 py-3 text-sm font-semibold text-slate-700">Member</th>
<th className="text-right px-6 py-3 text-sm font-semibold text-slate-700">Balance</th>
<th className="text-right px-6 py-3 text-sm font-semibold text-slate-700">Updated</th>
<th className="w-12"></th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
<tbody className="divide-y divide-slate-100">
{filteredCredits.map(credit => (
<tr
key={credit.app_user_id}
className="hover:bg-gray-50 cursor-pointer transition-colors"
className="hover:bg-slate-50 cursor-pointer transition-colors"
onClick={() => setSelectedUser(credit)}
>
<td className="px-6 py-4">
<div className="font-medium text-gray-900">{credit.display_name}</div>
<div className="text-sm text-gray-500">{credit.email}</div>
<div className="font-medium text-slate-900">{credit.display_name}</div>
<div className="text-sm text-slate-500">{credit.email}</div>
</td>
<td className="px-6 py-4 text-right">
<span className={`font-semibold ${
credit.balance_cents > 0 ? 'text-emerald-600' :
credit.balance_cents < 0 ? 'text-red-600' : 'text-gray-500'
credit.balance_cents < 0 ? 'text-red-600' : 'text-slate-500'
}`}>
{formatCurrency(credit.balance_cents, credit.currency)}
</span>
</td>
<td className="px-6 py-4 text-right text-sm text-gray-500">
<td className="px-6 py-4 text-right text-sm text-slate-500">
{credit.updated_at
? new Date(credit.updated_at).toLocaleDateString()
: '-'}
</td>
<td className="px-4 py-4">
<ChevronRight className="w-5 h-5 text-gray-400" />
<ChevronRight className="w-5 h-5 text-slate-400" />
</td>
</tr>
))}

@ -1,4 +1,3 @@
import AdminAuthGuard from '@/src/components/AdminAuthGuard';
import CreditsManagementComponent from './CreditsManagementComponent';
export default async function CreditsManagementPage({
@ -9,9 +8,5 @@ export default async function CreditsManagementPage({
const { club_id } = await params;
const clubId = parseInt(club_id, 10);
return (
<AdminAuthGuard>
<CreditsManagementComponent clubId={clubId} />
</AdminAuthGuard>
);
return <CreditsManagementComponent clubId={clubId} />;
}

@ -0,0 +1,21 @@
import AdminAuthGuard from '@/src/components/AdminAuthGuard';
import ClubDetailHeader from '@/src/components/ClubDetailHeader';
export default async function ClubDetailLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ club_id: string }>;
}) {
const { club_id } = await params;
const clubId = parseInt(club_id, 10);
return (
<AdminAuthGuard>
<ClubDetailHeader clubId={clubId}>
{children}
</ClubDetailHeader>
</AdminAuthGuard>
);
}

@ -87,12 +87,12 @@ export default function MembersManagementComponent({ clubId }: MembersManagement
}
return (
<div className="container mx-auto px-4 py-8">
<div>
{/* Header */}
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-3xl font-bold text-gray-800">Members</h1>
<p className="text-gray-600 mt-1">Manage facility members and their access</p>
<h1 className="text-2xl font-bold text-slate-900">Members</h1>
<p className="text-slate-600 mt-1">Manage facility members and their access</p>
</div>
<button
onClick={() => setAddModalOpen(true)}
@ -110,11 +110,11 @@ export default function MembersManagementComponent({ clubId }: MembersManagement
)}
{/* Filters */}
<div className="bg-white border border-gray-200 rounded-lg p-4 mb-6 shadow-sm">
<div className="bg-white border border-slate-200 rounded-lg p-4 mb-6 shadow-sm">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{/* Search */}
<div className="md:col-span-2">
<label htmlFor="search" className="block text-sm font-medium text-gray-700 mb-1">
<label htmlFor="search" className="block text-sm font-medium text-slate-700 mb-1">
Search
</label>
<input
@ -123,20 +123,20 @@ export default function MembersManagementComponent({ clubId }: MembersManagement
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
placeholder="Search by name or email..."
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-slate-500 focus:border-transparent"
/>
</div>
{/* Role Filter */}
<div>
<label htmlFor="role" className="block text-sm font-medium text-gray-700 mb-1">
<label htmlFor="role" className="block text-sm font-medium text-slate-700 mb-1">
Role
</label>
<select
id="role"
value={roleFilter}
onChange={e => setRoleFilter(e.target.value as MemberRole | '')}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-slate-500 focus:border-transparent"
>
<option value="">All Roles</option>
<option value="guest">Guest</option>
@ -148,14 +148,14 @@ export default function MembersManagementComponent({ clubId }: MembersManagement
{/* Status Filter */}
<div>
<label htmlFor="status" className="block text-sm font-medium text-gray-700 mb-1">
<label htmlFor="status" className="block text-sm font-medium text-slate-700 mb-1">
Status
</label>
<select
id="status"
value={statusFilter}
onChange={e => setStatusFilter(e.target.value as MemberStatus | '')}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-slate-500 focus:border-transparent"
>
<option value="">All Statuses</option>
<option value="active">Active</option>
@ -176,10 +176,10 @@ export default function MembersManagementComponent({ clubId }: MembersManagement
) : filteredMembers.length === 0 ? (
<div className="text-center py-16">
<div className="text-6xl mb-4">👥</div>
<h2 className="text-2xl font-semibold text-gray-700 mb-2">
<h2 className="text-2xl font-semibold text-slate-900 mb-2">
{searchQuery || roleFilter ? 'No members found' : 'No members yet'}
</h2>
<p className="text-gray-500 mb-6">
<p className="text-slate-600 mb-6">
{searchQuery || roleFilter
? 'Try adjusting your filters'
: 'Add your first member to get started'}

@ -1,4 +1,3 @@
import AdminAuthGuard from '@/src/components/AdminAuthGuard';
import MembersManagementComponent from './MembersManagementComponent';
export default async function MembersManagementPage({
@ -9,9 +8,5 @@ export default async function MembersManagementPage({
const { club_id } = await params;
const clubId = parseInt(club_id, 10);
return (
<AdminAuthGuard>
<MembersManagementComponent clubId={clubId} />
</AdminAuthGuard>
);
return <MembersManagementComponent clubId={clubId} />;
}

@ -1,5 +1,4 @@
import ClubDetailTabs from './ClubDetailTabs';
import AdminAuthGuard from '@/src/components/AdminAuthGuard';
export default async function AdminClubDetailPage({
params
@ -9,9 +8,5 @@ export default async function AdminClubDetailPage({
const { club_id } = await params;
const clubId = parseInt(club_id, 10);
return (
<AdminAuthGuard>
<ClubDetailTabs clubId={clubId} />
</AdminAuthGuard>
);
return <ClubDetailTabs clubId={clubId} />;
}

@ -95,12 +95,12 @@ export default function MembershipPlansComponent({ clubId }: MembershipPlansComp
}
return (
<div className="container mx-auto px-4 py-8">
<div>
{/* Header */}
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-3xl font-bold text-gray-800">Membership Plans</h1>
<p className="text-gray-600 mt-1">Create and manage subscription plans for your facility</p>
<h1 className="text-2xl font-bold text-slate-900">Membership Plans</h1>
<p className="text-slate-600 mt-1">Create and manage subscription plans for your facility</p>
</div>
<button
onClick={handleCreateClick}
@ -123,8 +123,8 @@ export default function MembershipPlansComponent({ clubId }: MembershipPlansComp
) : plans.length === 0 ? (
<div className="text-center py-16">
<div className="text-6xl mb-4">📋</div>
<h2 className="text-2xl font-semibold text-gray-700 mb-2">No membership plans yet</h2>
<p className="text-gray-500 mb-6">Create your first plan to get started</p>
<h2 className="text-2xl font-semibold text-slate-900 mb-2">No membership plans yet</h2>
<p className="text-slate-600 mb-6">Create your first plan to get started</p>
<button
onClick={handleCreateClick}
className="bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 text-white px-6 py-3 rounded-lg font-semibold shadow-md transition-all duration-200"

@ -1,4 +1,3 @@
import AdminAuthGuard from '@/src/components/AdminAuthGuard';
import MembershipPlansComponent from './MembershipPlansComponent';
export default async function MembershipPlansPage({
@ -9,9 +8,5 @@ export default async function MembershipPlansPage({
const { club_id } = await params;
const clubId = parseInt(club_id, 10);
return (
<AdminAuthGuard>
<MembershipPlansComponent clubId={clubId} />
</AdminAuthGuard>
);
return <MembershipPlansComponent clubId={clubId} />;
}

@ -66,13 +66,13 @@ export default function FacilitySettingsComponent({ clubId }: FacilitySettingsCo
if (loading) {
return (
<div className="container mx-auto px-4 py-8">
<div>
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/3 mb-4"></div>
<div className="h-8 bg-slate-200 rounded w-1/3 mb-4"></div>
<div className="space-y-4">
<div className="h-32 bg-gray-200 rounded"></div>
<div className="h-32 bg-gray-200 rounded"></div>
<div className="h-32 bg-gray-200 rounded"></div>
<div className="h-32 bg-slate-200 rounded"></div>
<div className="h-32 bg-slate-200 rounded"></div>
<div className="h-32 bg-slate-200 rounded"></div>
</div>
</div>
</div>
@ -81,7 +81,7 @@ export default function FacilitySettingsComponent({ clubId }: FacilitySettingsCo
if (!policy) {
return (
<div className="container mx-auto px-4 py-8">
<div>
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg">
Failed to load facility policy
</div>
@ -90,11 +90,11 @@ export default function FacilitySettingsComponent({ clubId }: FacilitySettingsCo
}
return (
<div className="container mx-auto px-4 py-8">
<div>
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-800">Facility Settings</h1>
<p className="text-gray-600 mt-1">Configure access model, pricing, and booking policies</p>
<h1 className="text-2xl font-bold text-slate-900">Facility Settings</h1>
<p className="text-slate-600 mt-1">Configure access model, pricing, and booking policies</p>
</div>
{/* Success Message */}
@ -114,8 +114,8 @@ export default function FacilitySettingsComponent({ clubId }: FacilitySettingsCo
{/* Settings Form */}
<div className="space-y-6">
{/* Access Model Section */}
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
<h2 className="text-xl font-bold text-gray-800 mb-4">Access Model</h2>
<div className="bg-white border border-slate-200 rounded-xl p-6 shadow-sm">
<h2 className="text-xl font-bold text-slate-900 mb-4">Access Model</h2>
<AccessModelSelector
value={policy.access_model}
onChange={(value: AccessModel) => setPolicy({ ...policy, access_model: value })}
@ -124,8 +124,8 @@ export default function FacilitySettingsComponent({ clubId }: FacilitySettingsCo
{/* Guest Pricing Section (conditional) */}
{policy.access_model === 'payg_allowed' && (
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
<h2 className="text-xl font-bold text-gray-800 mb-4">Guest Pricing</h2>
<div className="bg-white border border-slate-200 rounded-xl p-6 shadow-sm">
<h2 className="text-xl font-bold text-slate-900 mb-4">Guest Pricing</h2>
<GuestPricingInput
value={policy.guest_price_cents}
onChange={(value: number) => setPolicy({ ...policy, guest_price_cents: value })}
@ -134,8 +134,8 @@ export default function FacilitySettingsComponent({ clubId }: FacilitySettingsCo
)}
{/* Booking Limits Section */}
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
<h2 className="text-xl font-bold text-gray-800 mb-4">Booking Limits</h2>
<div className="bg-white border border-slate-200 rounded-xl p-6 shadow-sm">
<h2 className="text-xl font-bold text-slate-900 mb-4">Booking Limits</h2>
<BookingLimitsForm
maxFutureBookingDays={policy.max_future_booking_days}
maxActiveBookings={policy.max_active_bookings}

@ -1,4 +1,3 @@
import AdminAuthGuard from '@/src/components/AdminAuthGuard';
import FacilitySettingsComponent from './FacilitySettingsComponent';
export default async function FacilitySettingsPage({
@ -9,9 +8,5 @@ export default async function FacilitySettingsPage({
const { club_id } = await params;
const clubId = parseInt(club_id, 10);
return (
<AdminAuthGuard>
<FacilitySettingsComponent clubId={clubId} />
</AdminAuthGuard>
);
return <FacilitySettingsComponent clubId={clubId} />;
}

@ -1,9 +1,7 @@
'use client';
import { useState, useEffect } from 'react';
import { Calendar, Plus, Loader2, AlertCircle, Edit, Trash2, ArrowLeft, Wand2, Copy } from 'lucide-react';
import Link from 'next/link';
import useTranslation from '@/src/hooks/useTranslation';
import { Calendar, Plus, Loader2, AlertCircle, Edit, Trash2, Wand2, Copy } from 'lucide-react';
import { getSlotDefinitions, deleteSlotDefinition } from '@/src/lib/api/slot-definitions';
import type { SlotDefinition, SlotDefinitionError } from '@/src/types/slot-definitions';
import { DAY_NAMES, formatTime, calculateEndTime } from '@/src/types/slot-definitions';
@ -19,7 +17,6 @@ interface SlotDefinitionsComponentProps {
}
export default function SlotDefinitionsComponent({ clubId }: SlotDefinitionsComponentProps) {
const { t, locale } = useTranslation();
const [definitions, setDefinitions] = useState<SlotDefinition[]>([]);
const [courts, setCourts] = useState<Court[]>([]);
const [loading, setLoading] = useState(true);
@ -96,11 +93,9 @@ export default function SlotDefinitionsComponent({ clubId }: SlotDefinitionsComp
// Loading state
if (loading) {
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
<div className="flex flex-col items-center justify-center space-y-4">
<Loader2 className="w-12 h-12 text-slate-900 animate-spin" />
<p className="text-slate-600 font-medium">Loading slot definitions...</p>
</div>
<div className="flex flex-col items-center justify-center py-20 space-y-4">
<Loader2 className="w-12 h-12 text-slate-900 animate-spin" />
<p className="text-slate-600 font-medium">Loading slot definitions...</p>
</div>
);
}
@ -108,29 +103,20 @@ export default function SlotDefinitionsComponent({ clubId }: SlotDefinitionsComp
// Error state
if (error) {
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
<div className="max-w-2xl mx-auto">
<div className="bg-red-50 border-2 border-red-200 rounded-2xl p-8">
<div className="flex items-start space-x-4">
<AlertCircle className="w-8 h-8 text-red-600 flex-shrink-0 mt-1" />
<div className="flex-1">
<h2 className="text-2xl font-bold text-slate-900 mb-4">
Error Loading Slot Definitions
</h2>
<p className="text-slate-700 mb-4 leading-relaxed">
{error.detail}
</p>
<p className="text-sm text-slate-600 font-mono mb-6">
Error code: {error.code}
</p>
<Link
href={`/${locale}/admin/clubs/${clubId}`}
className="inline-flex items-center px-4 py-2 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to club
</Link>
</div>
<div className="max-w-2xl mx-auto">
<div className="bg-red-50 border-2 border-red-200 rounded-2xl p-8">
<div className="flex items-start space-x-4">
<AlertCircle className="w-8 h-8 text-red-600 flex-shrink-0 mt-1" />
<div className="flex-1">
<h2 className="text-2xl font-bold text-slate-900 mb-4">
Error Loading Slot Definitions
</h2>
<p className="text-slate-700 mb-4 leading-relaxed">
{error.detail}
</p>
<p className="text-sm text-slate-600 font-mono">
Error code: {error.code}
</p>
</div>
</div>
</div>
@ -139,23 +125,15 @@ export default function SlotDefinitionsComponent({ clubId }: SlotDefinitionsComp
}
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div>
{/* Header */}
<div className="mb-8">
<Link
href={`/${locale}/admin/clubs/${clubId}`}
className="inline-flex items-center text-slate-600 hover:text-slate-900 font-medium transition-colors mb-4"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to club
</Link>
<div className="flex items-center justify-between">
<div>
<h1 className="text-4xl font-bold text-slate-900 mb-2 tracking-tight">
<h1 className="text-2xl font-bold text-slate-900 mb-2 tracking-tight">
Slot Definitions
</h1>
<p className="text-lg text-slate-600 font-light">
<p className="text-slate-600 font-light">
Recurring schedule templates for automatic slot generation
</p>
</div>

@ -1,4 +1,3 @@
import AdminAuthGuard from '@/src/components/AdminAuthGuard';
import SlotDefinitionsComponent from './SlotDefinitionsComponent';
export default async function SlotDefinitionsPage({
@ -9,9 +8,5 @@ export default async function SlotDefinitionsPage({
const { club_id } = await params;
const clubId = parseInt(club_id, 10);
return (
<AdminAuthGuard>
<SlotDefinitionsComponent clubId={clubId} />
</AdminAuthGuard>
);
return <SlotDefinitionsComponent clubId={clubId} />;
}

@ -6,14 +6,11 @@ import {
Plus,
Loader2,
AlertCircle,
ArrowLeft,
ChevronLeft,
ChevronRight,
RefreshCw,
Filter,
} from 'lucide-react';
import Link from 'next/link';
import useTranslation from '@/src/hooks/useTranslation';
import { getSlotInstances, deleteSlotInstance, cancelSlotInstance } from '@/src/lib/api/slot-instances';
import { getAdminClubDetail } from '@/src/lib/api/admin-clubs';
import type { SlotInstance, SlotInstanceError } from '@/src/types/slot-instances';
@ -35,7 +32,6 @@ interface SlotInstancesComponentProps {
}
export default function SlotInstancesComponent({ clubId }: SlotInstancesComponentProps) {
const { t, locale } = useTranslation();
const [slots, setSlots] = useState<SlotInstance[]>([]);
const [courts, setCourts] = useState<Court[]>([]);
const [timezone, setTimezone] = useState<string>('UTC');
@ -145,29 +141,20 @@ export default function SlotInstancesComponent({ clubId }: SlotInstancesComponen
// Error state
if (error && !loading) {
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
<div className="max-w-2xl mx-auto">
<div className="bg-red-50 border-2 border-red-200 rounded-2xl p-8">
<div className="flex items-start space-x-4">
<AlertCircle className="w-8 h-8 text-red-600 flex-shrink-0 mt-1" />
<div className="flex-1">
<h2 className="text-2xl font-bold text-slate-900 mb-4">
Error Loading Slot Instances
</h2>
<p className="text-slate-700 mb-4 leading-relaxed">
{error.detail}
</p>
<p className="text-sm text-slate-600 font-mono mb-6">
Error code: {error.code}
</p>
<Link
href={`/${locale}/admin/clubs/${clubId}`}
className="inline-flex items-center px-4 py-2 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to club
</Link>
</div>
<div className="max-w-2xl mx-auto">
<div className="bg-red-50 border-2 border-red-200 rounded-2xl p-8">
<div className="flex items-start space-x-4">
<AlertCircle className="w-8 h-8 text-red-600 flex-shrink-0 mt-1" />
<div className="flex-1">
<h2 className="text-2xl font-bold text-slate-900 mb-4">
Error Loading Slot Instances
</h2>
<p className="text-slate-700 mb-4 leading-relaxed">
{error.detail}
</p>
<p className="text-sm text-slate-600 font-mono">
Error code: {error.code}
</p>
</div>
</div>
</div>
@ -176,23 +163,15 @@ export default function SlotInstancesComponent({ clubId }: SlotInstancesComponen
}
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div>
{/* Header */}
<div className="mb-8">
<Link
href={`/${locale}/admin/clubs/${clubId}`}
className="inline-flex items-center text-slate-600 hover:text-slate-900 font-medium transition-colors mb-4"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to club
</Link>
<div className="flex items-center justify-between">
<div>
<h1 className="text-4xl font-bold text-slate-900 mb-2 tracking-tight">
<h1 className="text-2xl font-bold text-slate-900 mb-2 tracking-tight">
Slot Instances
</h1>
<p className="text-lg text-slate-600 font-light">
<p className="text-slate-600 font-light">
View and manage individual booking slots
</p>
</div>

@ -1,4 +1,3 @@
import AdminAuthGuard from '@/src/components/AdminAuthGuard';
import SlotInstancesComponent from './SlotInstancesComponent';
export default async function SlotInstancesPage({
@ -9,9 +8,5 @@ export default async function SlotInstancesPage({
const { club_id } = await params;
const clubId = parseInt(club_id, 10);
return (
<AdminAuthGuard>
<SlotInstancesComponent clubId={clubId} />
</AdminAuthGuard>
);
return <SlotInstancesComponent clubId={clubId} />;
}

@ -1,11 +1,9 @@
'use client';
import { useState, useEffect } from 'react';
import { ArrowLeft, Loader2, ArrowRightLeft, Calendar, Filter, RefreshCw } from 'lucide-react';
import Link from 'next/link';
import { Loader2, ArrowRightLeft, Calendar, Filter, RefreshCw } from 'lucide-react';
import { listFacilityTransfers } from '@/src/lib/api/facility-admin';
import type { AdminTransferOffer, TransferListFilters } from '@/src/types/facility-admin';
import useTranslation from '@/src/hooks/useTranslation';
interface TransfersManagementComponentProps {
clubId: number;
@ -14,7 +12,6 @@ interface TransfersManagementComponentProps {
type TransferStatus = 'all' | 'pending' | 'accepted' | 'declined' | 'expired' | 'cancelled';
export default function TransfersManagementComponent({ clubId }: TransfersManagementComponentProps) {
const { locale } = useTranslation();
const [transfers, setTransfers] = useState<AdminTransferOffer[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@ -82,24 +79,17 @@ export default function TransfersManagementComponent({ clubId }: TransfersManage
};
return (
<div className="container mx-auto px-4 py-8">
<div>
{/* Header */}
<div className="mb-6">
<Link
href={`/${locale}/admin/clubs/${clubId}`}
className="inline-flex items-center text-gray-600 hover:text-gray-800 font-medium mb-4"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Club
</Link>
<div className="flex justify-between items-start">
<div>
<h1 className="text-3xl font-bold text-gray-800">Booking Transfers</h1>
<p className="text-gray-600 mt-1">Monitor booking transfer requests and their status</p>
<h1 className="text-2xl font-bold text-slate-900">Booking Transfers</h1>
<p className="text-slate-600 mt-1">Monitor booking transfer requests and their status</p>
</div>
<button
onClick={fetchTransfers}
className="flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
className="flex items-center gap-2 px-4 py-2 bg-slate-100 text-slate-700 rounded-lg hover:bg-slate-200 transition-colors"
>
<RefreshCw className="w-4 h-4" />
Refresh
@ -109,9 +99,9 @@ export default function TransfersManagementComponent({ clubId }: TransfersManage
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div className="bg-white border border-gray-200 rounded-xl p-4">
<div className="text-sm text-gray-600">Total</div>
<div className="text-2xl font-bold text-gray-900">{stats.total}</div>
<div className="bg-white border border-slate-200 rounded-xl p-4">
<div className="text-sm text-slate-600">Total</div>
<div className="text-2xl font-bold text-slate-900">{stats.total}</div>
</div>
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4">
<div className="text-sm text-amber-700">Pending</div>
@ -121,23 +111,23 @@ export default function TransfersManagementComponent({ clubId }: TransfersManage
<div className="text-sm text-emerald-700">Accepted</div>
<div className="text-2xl font-bold text-emerald-700">{stats.accepted}</div>
</div>
<div className="bg-gray-50 border border-gray-200 rounded-xl p-4">
<div className="text-sm text-gray-600">Expired</div>
<div className="text-2xl font-bold text-gray-600">{stats.expired}</div>
<div className="bg-slate-50 border border-slate-200 rounded-xl p-4">
<div className="text-sm text-slate-600">Expired</div>
<div className="text-2xl font-bold text-slate-600">{stats.expired}</div>
</div>
</div>
{/* Filters */}
<div className="flex flex-wrap gap-4 mb-6 bg-gray-50 rounded-xl p-4">
<div className="flex flex-wrap gap-4 mb-6 bg-slate-50 rounded-xl p-4">
<div className="flex items-center gap-2">
<Filter className="w-4 h-4 text-gray-500" />
<span className="text-sm font-medium text-gray-700">Filters:</span>
<Filter className="w-4 h-4 text-slate-500" />
<span className="text-sm font-medium text-slate-700">Filters:</span>
</div>
<select
value={statusFilter}
onChange={e => setStatusFilter(e.target.value as TransferStatus)}
className="px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 text-sm"
className="px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-slate-500 text-sm"
>
<option value="all">All statuses</option>
<option value="pending">Pending</option>
@ -148,20 +138,20 @@ export default function TransfersManagementComponent({ clubId }: TransfersManage
</select>
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4 text-gray-500" />
<Calendar className="w-4 h-4 text-slate-500" />
<input
type="date"
value={dateRange.from}
onChange={e => setDateRange({ ...dateRange, from: e.target.value })}
className="px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 text-sm"
className="px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-slate-500 text-sm"
placeholder="From"
/>
<span className="text-gray-400">to</span>
<span className="text-slate-400">to</span>
<input
type="date"
value={dateRange.to}
onChange={e => setDateRange({ ...dateRange, to: e.target.value })}
className="px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 text-sm"
className="px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-slate-500 text-sm"
placeholder="To"
/>
</div>
@ -172,7 +162,7 @@ export default function TransfersManagementComponent({ clubId }: TransfersManage
setStatusFilter('all');
setDateRange({ from: '', to: '' });
}}
className="text-sm text-purple-600 hover:text-purple-700 font-medium"
className="text-sm text-slate-700 hover:text-slate-900 font-medium"
>
Clear filters
</button>
@ -189,55 +179,55 @@ export default function TransfersManagementComponent({ clubId }: TransfersManage
{/* Transfers List */}
{loading ? (
<div className="flex items-center justify-center py-16">
<Loader2 className="w-8 h-8 text-purple-600 animate-spin" />
<span className="ml-2 text-gray-600">Loading transfers...</span>
<Loader2 className="w-8 h-8 text-slate-600 animate-spin" />
<span className="ml-2 text-slate-600">Loading transfers...</span>
</div>
) : transfers.length === 0 ? (
<div className="text-center py-16 bg-gray-50 rounded-xl">
<div className="text-center py-16 bg-slate-50 rounded-xl">
<div className="text-6xl mb-4">
<ArrowRightLeft className="w-16 h-16 mx-auto text-gray-400" />
<ArrowRightLeft className="w-16 h-16 mx-auto text-slate-400" />
</div>
<h2 className="text-xl font-semibold text-gray-700 mb-2">No transfers found</h2>
<p className="text-gray-500">
<h2 className="text-xl font-semibold text-slate-900 mb-2">No transfers found</h2>
<p className="text-slate-600">
{statusFilter !== 'all' || dateRange.from || dateRange.to
? 'Try adjusting your filters'
: 'Transfer requests will appear here when members initiate them'}
</p>
</div>
) : (
<div className="bg-white border border-gray-200 rounded-xl shadow-sm overflow-hidden">
<div className="bg-white border border-slate-200 rounded-xl shadow-sm overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<thead className="bg-slate-50 border-b border-slate-200">
<tr>
<th className="text-left px-6 py-3 text-sm font-semibold text-gray-600">From</th>
<th className="text-left px-6 py-3 text-sm font-semibold text-gray-600">To</th>
<th className="text-left px-6 py-3 text-sm font-semibold text-gray-600">Booking</th>
<th className="text-left px-6 py-3 text-sm font-semibold text-gray-600">Status</th>
<th className="text-left px-6 py-3 text-sm font-semibold text-gray-600">Created</th>
<th className="text-left px-6 py-3 text-sm font-semibold text-gray-600">Expires</th>
<th className="text-left px-6 py-3 text-sm font-semibold text-slate-700">From</th>
<th className="text-left px-6 py-3 text-sm font-semibold text-slate-700">To</th>
<th className="text-left px-6 py-3 text-sm font-semibold text-slate-700">Booking</th>
<th className="text-left px-6 py-3 text-sm font-semibold text-slate-700">Status</th>
<th className="text-left px-6 py-3 text-sm font-semibold text-slate-700">Created</th>
<th className="text-left px-6 py-3 text-sm font-semibold text-slate-700">Expires</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
<tbody className="divide-y divide-slate-100">
{transfers.map(transfer => (
<tr key={transfer.offer_id} className="hover:bg-gray-50">
<tr key={transfer.offer_id} className="hover:bg-slate-50">
<td className="px-6 py-4">
<div className="font-medium text-gray-900">{transfer.from_user.display_name}</div>
<div className="text-sm text-gray-500">{transfer.from_user.email}</div>
<div className="font-medium text-slate-900">{transfer.from_user.display_name}</div>
<div className="text-sm text-slate-500">{transfer.from_user.email}</div>
</td>
<td className="px-6 py-4">
{transfer.to_user ? (
<>
<div className="font-medium text-gray-900">{transfer.to_user.display_name || 'Unknown'}</div>
<div className="text-sm text-gray-500">{transfer.to_user.email || '-'}</div>
<div className="font-medium text-slate-900">{transfer.to_user.display_name || 'Unknown'}</div>
<div className="text-sm text-slate-500">{transfer.to_user.email || '-'}</div>
</>
) : (
<span className="text-gray-400 italic">Open offer</span>
<span className="text-slate-400 italic">Open offer</span>
)}
</td>
<td className="px-6 py-4">
<div className="font-medium text-gray-900">{transfer.booking_info.court_name}</div>
<div className="text-sm text-gray-500">
<div className="font-medium text-slate-900">{transfer.booking_info.court_name}</div>
<div className="text-sm text-slate-500">
{transfer.booking_info.sport_name}
{transfer.booking_info.starts_at && (
<> &middot; {new Date(transfer.booking_info.starts_at).toLocaleDateString()}</>
@ -247,10 +237,10 @@ export default function TransfersManagementComponent({ clubId }: TransfersManage
<td className="px-6 py-4">
{getStatusBadge(transfer.status)}
</td>
<td className="px-6 py-4 text-sm text-gray-500">
<td className="px-6 py-4 text-sm text-slate-500">
{formatDateTime(transfer.created_at)}
</td>
<td className="px-6 py-4 text-sm text-gray-500">
<td className="px-6 py-4 text-sm text-slate-500">
{transfer.status === 'pending'
? formatDateTime(transfer.expires_at)
: transfer.resolved_at

@ -1,4 +1,3 @@
import AdminAuthGuard from '@/src/components/AdminAuthGuard';
import TransfersManagementComponent from './TransfersManagementComponent';
export default async function TransfersManagementPage({
@ -9,9 +8,5 @@ export default async function TransfersManagementPage({
const { club_id } = await params;
const clubId = parseInt(club_id, 10);
return (
<AdminAuthGuard>
<TransfersManagementComponent clubId={clubId} />
</AdminAuthGuard>
);
return <TransfersManagementComponent clubId={clubId} />;
}

@ -0,0 +1,173 @@
'use client';
import { useState, useEffect } from 'react';
import { ArrowLeft, Loader2, AlertCircle, Lock } from 'lucide-react';
import Link from 'next/link';
import { getAdminClubDetail } from '@/src/lib/api/admin-clubs';
import type { AdminClubDetail, AdminApiError } from '@/src/types/admin-api';
import useTranslation from '@/src/hooks/useTranslation';
import ClubTabNavigation from './ClubTabNavigation';
interface ClubDetailHeaderProps {
clubId: number;
children: React.ReactNode;
}
export default function ClubDetailHeader({ clubId, children }: ClubDetailHeaderProps) {
const { locale } = useTranslation();
const [clubDetail, setClubDetail] = useState<AdminClubDetail | null>(null);
const [error, setError] = useState<AdminApiError | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadClubDetail();
}, [clubId]);
async function loadClubDetail() {
setLoading(true);
const result = await getAdminClubDetail(clubId);
if (result.success) {
setClubDetail(result.data);
setError(null);
} else {
setError(result.error);
setClubDetail(null);
}
setLoading(false);
}
// Loading state
if (loading) {
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
<div className="flex flex-col items-center justify-center space-y-4">
<Loader2 className="w-12 h-12 text-slate-900 animate-spin" />
<p className="text-slate-600 font-medium">Loading club details...</p>
</div>
</div>
);
}
// Authentication error (401)
if (error && error.status === 401) {
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
<div className="max-w-2xl mx-auto">
<div className="bg-amber-50 border-2 border-amber-200 rounded-2xl p-8">
<div className="flex items-start space-x-4">
<Lock className="w-8 h-8 text-amber-700 flex-shrink-0 mt-1" />
<div>
<h2 className="text-2xl font-bold text-slate-900 mb-4">
Authentication Required
</h2>
<p className="text-slate-700 leading-relaxed">
Please log in to access the venue management portal.
</p>
</div>
</div>
</div>
</div>
</div>
);
}
// Forbidden error (403)
if (error && error.status === 403) {
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
<div className="max-w-2xl mx-auto">
<div className="bg-red-50 border-2 border-red-200 rounded-2xl p-8">
<div className="flex items-start space-x-4">
<Lock className="w-8 h-8 text-red-600 flex-shrink-0 mt-1" />
<div>
<h2 className="text-2xl font-bold text-slate-900 mb-4">
Access Denied
</h2>
<p className="text-slate-700 mb-6 leading-relaxed">
{error.detail}
</p>
<Link
href={`/${locale}/admin/clubs`}
className="inline-flex items-center px-4 py-2 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to clubs
</Link>
</div>
</div>
</div>
</div>
</div>
);
}
// Other API errors
if (error) {
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
<div className="max-w-2xl mx-auto">
<div className="bg-red-50 border-2 border-red-200 rounded-2xl p-8">
<div className="flex items-start space-x-4">
<AlertCircle className="w-8 h-8 text-red-600 flex-shrink-0 mt-1" />
<div className="flex-1">
<h2 className="text-2xl font-bold text-slate-900 mb-4">
Error Loading Club
</h2>
<p className="text-slate-700 mb-4 leading-relaxed">
{error.detail}
</p>
<p className="text-sm text-slate-600 font-mono mb-6">
Error code: {error.code}
</p>
<Link
href={`/${locale}/admin/clubs`}
className="inline-flex items-center px-4 py-2 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to clubs
</Link>
</div>
</div>
</div>
</div>
</div>
);
}
if (!clubDetail) {
return null;
}
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
{/* Breadcrumb */}
<div className="mb-8">
<Link
href={`/${locale}/admin/clubs`}
className="inline-flex items-center text-slate-600 hover:text-slate-900 font-medium transition-colors"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to clubs
</Link>
</div>
{/* Club Header */}
<div className="mb-8">
<h1 className="text-4xl font-bold text-slate-900 mb-2 tracking-tight">
{clubDetail.facility.name}
</h1>
<p className="text-lg text-slate-600 font-light">
{clubDetail.facility.timezone}
</p>
</div>
{/* Tab Navigation */}
<ClubTabNavigation clubId={clubId} />
{/* Content */}
{children}
</div>
);
}

@ -0,0 +1,69 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import useTranslation from '@/src/hooks/useTranslation';
interface ClubTabNavigationProps {
clubId: number;
}
interface TabConfig {
key: string;
label: string;
href: string;
}
export default function ClubTabNavigation({ clubId }: ClubTabNavigationProps) {
const { locale } = useTranslation();
const pathname = usePathname();
const basePath = `/${locale}/admin/clubs/${clubId}`;
const tabs: TabConfig[] = [
{ key: 'profile', label: 'Profile', href: basePath },
{ key: 'courts', label: 'Courts', href: `${basePath}/courts` },
{ key: 'slot-definitions', label: 'Slot Definitions', href: `${basePath}/slot-definitions` },
{ key: 'slot-instances', label: 'Slot Instances', href: `${basePath}/slot-instances` },
{ key: 'plans', label: 'Plans', href: `${basePath}/plans` },
{ key: 'members', label: 'Members', href: `${basePath}/members` },
{ key: 'settings', label: 'Settings', href: `${basePath}/settings` },
{ key: 'credits', label: 'Credits', href: `${basePath}/credits` },
{ key: 'transfers', label: 'Transfers', href: `${basePath}/transfers` },
];
function isActive(tab: TabConfig): boolean {
// For the base club path (profile), check if we're exactly on it
if (tab.key === 'profile') {
// Active if pathname is exactly the basePath (no sub-routes)
return pathname === basePath;
}
// For sub-pages, check if pathname starts with the tab's href
return pathname.startsWith(tab.href);
}
return (
<div className="border-b-2 border-slate-200 mb-8">
<div className="overflow-x-auto scrollbar-hide -mb-px">
<div className="flex min-w-max">
{tabs.map((tab) => {
const active = isActive(tab);
return (
<Link
key={tab.key}
href={tab.href}
className={`px-4 sm:px-6 py-3 font-semibold transition-colors border-b-2 whitespace-nowrap text-sm sm:text-base ${
active
? 'border-slate-900 text-slate-900'
: 'border-transparent text-slate-600 hover:text-slate-900 hover:border-slate-300'
}`}
>
{tab.label}
</Link>
);
})}
</div>
</div>
</div>
);
}
Loading…
Cancel
Save