refactor: add shared club detail layout with persistent tab navigation
continuous-integration/drone/push Build is passing
Details
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 stylingmaster
parent
308d9d70bf
commit
256eccc997
@ -1,252 +1,11 @@
|
|||||||
'use client';
|
'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 ClubProfileTab from './tabs/ClubProfileTab';
|
||||||
import ClubCourtsTab from './tabs/ClubCourtsTab';
|
|
||||||
|
|
||||||
interface ClubDetailTabsProps {
|
interface ClubDetailTabsProps {
|
||||||
clubId: number;
|
clubId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type TabKey = 'profile' | 'courts' | 'slot-definitions';
|
|
||||||
|
|
||||||
export default function ClubDetailTabs({ clubId }: ClubDetailTabsProps) {
|
export default function ClubDetailTabs({ clubId }: ClubDetailTabsProps) {
|
||||||
const { t, locale } = useTranslation();
|
return <ClubProfileTab clubId={clubId} />;
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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} />;
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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…
Reference in New Issue