refactor(admin): convert to client-side rendering and add missing routes
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
Responding to Chief Cole's feedback after validation of BUILD:283: - Manager portal should match consumer app pattern (client-side fetching) - Fix 404 errors for /dashboard and /admin/clubs/[id] - Ensure locale-prefixed routing works smoothly Changes: 1. Client-side rendering for admin views: - Converted /admin/clubs list to use 'use client' pattern - Created AdminClubsList.tsx component with useEffect data fetching - Removed SSR cookie forwarding (no longer needed for client-side) 2. Added club detail page: - Created /admin/clubs/[club_id] route with AdminClubDetail.tsx - Client-side fetching with proper loading/error states - Support for 401/403/404 error handling with user-friendly messages - Breadcrumb navigation back to clubs list 3. Added dashboard route: - Created /dashboard that redirects to /admin/clubs - Resolves post-login 404 error reported in testing 4. Locale handling: - Middleware automatically adds locale prefix to all routes - Internal links include locale parameter to avoid extra redirects - /dashboard → /en-US/dashboard → /en-US/admin/clubs Result: Manager portal now follows consumer app patterns with full client-side data fetching, proper error handling, and complete routing. Refs: docs/owners/Frontend_Faye-needs-to-read-from-Chief_Cole-20251107073302.mdmaster
parent
51a928c1f4
commit
9f47dab8d8
@ -0,0 +1,175 @@
|
||||
'use client';
|
||||
|
||||
import { Building, AlertCircle, Lock, Loader2 } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getAdminClubs } from '@/src/lib/api/admin-clubs';
|
||||
import type { AdminClubsResponse, AdminApiError } from '@/src/types/admin-api';
|
||||
|
||||
interface AdminClubsListProps {
|
||||
locale: string;
|
||||
t: (key: string) => string;
|
||||
}
|
||||
|
||||
export default function AdminClubsList({ locale, t }: AdminClubsListProps) {
|
||||
const [clubs, setClubs] = useState<AdminClubsResponse | null>(null);
|
||||
const [error, setError] = useState<AdminApiError | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadClubs() {
|
||||
setLoading(true);
|
||||
const result = await getAdminClubs();
|
||||
|
||||
if (result.success) {
|
||||
setClubs(result.data);
|
||||
setError(null);
|
||||
} else {
|
||||
setError(result.error);
|
||||
setClubs(null);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
loadClubs();
|
||||
}, []);
|
||||
|
||||
// Loading state
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||||
<div className="flex flex-col items-center justify-center space-y-4">
|
||||
<Loader2 className="w-12 h-12 text-indigo-600 animate-spin" />
|
||||
<p className="text-gray-600">{t('Loading clubs...')}</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-16">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-xl p-8">
|
||||
<div className="flex items-start space-x-4">
|
||||
<Lock className="w-8 h-8 text-yellow-600 flex-shrink-0 mt-1" />
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">
|
||||
{t('Authentication Required')}
|
||||
</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
{t('Please log in to access the venue management portal.')}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
{t('If you are a venue administrator and do not have access, please contact support.')}
|
||||
</p>
|
||||
</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-16">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl 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>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">
|
||||
{t('Error Loading Clubs')}
|
||||
</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
{error.detail}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 font-mono">
|
||||
{t('Error code')}: {error.code}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// No clubs assigned
|
||||
if (!clubs || clubs.length === 0) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
||||
{t('Club Management')}
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
{t('View and manage your venue locations')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-xl p-8">
|
||||
<div className="flex flex-col items-center text-center space-y-4">
|
||||
<Building className="w-16 h-16 text-blue-600" />
|
||||
<h2 className="text-xl font-bold text-gray-900">
|
||||
{t('No Clubs Assigned')}
|
||||
</h2>
|
||||
<p className="text-gray-600 max-w-md">
|
||||
{t('You are not currently assigned as an administrator for any clubs. Contact your organization to request access.')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Success - render clubs list
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
||||
{t('Club Management')}
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
{t('View and manage your venue locations')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{clubs.map((club) => (
|
||||
<Link
|
||||
key={club.club_id}
|
||||
href={`/${locale}/admin/clubs/${club.club_id}`}
|
||||
className="bg-white border border-gray-200 rounded-xl p-6 hover:shadow-lg hover:border-indigo-300 transition-all duration-200"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="p-3 bg-indigo-100 rounded-lg">
|
||||
<Building className="w-6 h-6 text-indigo-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-1">
|
||||
{club.name}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600">
|
||||
{club.timezone}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 pt-4 border-t border-gray-100">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600">{t('Courts')}</span>
|
||||
<span className="font-semibold text-gray-900">{club.courts}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,262 @@
|
||||
'use client';
|
||||
|
||||
import { Building, AlertCircle, Lock, Loader2, ArrowLeft, MapPin, Calendar, Server } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getAdminClubDetail } from '@/src/lib/api/admin-clubs';
|
||||
import type { AdminClubDetail, AdminApiError } from '@/src/types/admin-api';
|
||||
|
||||
interface AdminClubDetailProps {
|
||||
clubId: number;
|
||||
locale: string;
|
||||
t: (key: string) => string;
|
||||
}
|
||||
|
||||
export default function AdminClubDetailComponent({ clubId, locale, t }: AdminClubDetailProps) {
|
||||
const [clubDetail, setClubDetail] = useState<AdminClubDetail | null>(null);
|
||||
const [error, setError] = useState<AdminApiError | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
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);
|
||||
}
|
||||
|
||||
loadClubDetail();
|
||||
}, [clubId]);
|
||||
|
||||
// Loading state
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||||
<div className="flex flex-col items-center justify-center space-y-4">
|
||||
<Loader2 className="w-12 h-12 text-indigo-600 animate-spin" />
|
||||
<p className="text-gray-600">{t('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-16">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-xl p-8">
|
||||
<div className="flex items-start space-x-4">
|
||||
<Lock className="w-8 h-8 text-yellow-600 flex-shrink-0 mt-1" />
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">
|
||||
{t('Authentication Required')}
|
||||
</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
{t('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-16">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl 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-gray-900 mb-4">
|
||||
{t('Access Denied')}
|
||||
</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
{error.detail}
|
||||
</p>
|
||||
<Link
|
||||
href={`/${locale}/admin/clubs`}
|
||||
className="inline-flex items-center text-indigo-600 hover:text-indigo-700 font-medium"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
{t('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-16">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl 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>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">
|
||||
{t('Error Loading Club')}
|
||||
</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
{error.detail}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 font-mono mb-4">
|
||||
{t('Error code')}: {error.code}
|
||||
</p>
|
||||
<Link
|
||||
href={`/${locale}/admin/clubs`}
|
||||
className="inline-flex items-center text-indigo-600 hover:text-indigo-700 font-medium"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
{t('Back to clubs')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!clubDetail) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Success - render club detail
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* Breadcrumb */}
|
||||
<div className="mb-6">
|
||||
<Link
|
||||
href={`/${locale}/admin/clubs`}
|
||||
className="inline-flex items-center text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
{t('Back to clubs')}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Club Header */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-6 mb-6">
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="p-3 bg-indigo-100 rounded-lg">
|
||||
<Building className="w-8 h-8 text-indigo-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
||||
{clubDetail.club.name}
|
||||
</h1>
|
||||
<div className="flex items-center text-gray-600 space-x-4">
|
||||
<div className="flex items-center">
|
||||
<MapPin className="w-4 h-4 mr-1" />
|
||||
<span>{clubDetail.club.timezone}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Provider Info */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-6 mb-6">
|
||||
<div className="flex items-center space-x-3 mb-4">
|
||||
<Server className="w-5 h-5 text-gray-600" />
|
||||
<h2 className="text-xl font-bold text-gray-900">{t('Provider Information')}</h2>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">{t('Type')}:</span>
|
||||
<span className="font-medium text-gray-900">{clubDetail.provider.remote_type}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">{t('Manages Slot Storage')}:</span>
|
||||
<span className="font-medium text-gray-900">
|
||||
{clubDetail.provider.capabilities.manages_slot_storage ? t('Yes') : t('No')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">{t('Supports Payment Verification')}:</span>
|
||||
<span className="font-medium text-gray-900">
|
||||
{clubDetail.provider.capabilities.supports_payment_verification ? t('Yes') : t('No')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Courts */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-6 mb-6">
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-4">{t('Courts')}</h2>
|
||||
{clubDetail.courts.length === 0 ? (
|
||||
<p className="text-gray-600">{t('No courts configured')}</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{clubDetail.courts.map((court) => (
|
||||
<div
|
||||
key={court.court_id}
|
||||
className="border border-gray-200 rounded-lg p-4"
|
||||
>
|
||||
<p className="font-medium text-gray-900">{court.name}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Slot Definitions */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-6 mb-6">
|
||||
<div className="flex items-center space-x-3 mb-4">
|
||||
<Calendar className="w-5 h-5 text-gray-600" />
|
||||
<h2 className="text-xl font-bold text-gray-900">{t('Slot Definitions')}</h2>
|
||||
</div>
|
||||
{clubDetail.slot_definitions.length === 0 ? (
|
||||
<p className="text-gray-600">{t('No slot definitions configured')}</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{clubDetail.slot_definitions.map((slotDef, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border border-gray-200 rounded-lg p-4"
|
||||
>
|
||||
<p className="text-sm text-gray-600">{t('Slot definition')} #{index + 1}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Upcoming Slots */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-6">
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-4">{t('Upcoming Slots')}</h2>
|
||||
{clubDetail.upcoming_slots.length === 0 ? (
|
||||
<p className="text-gray-600">{t('No upcoming slots available')}</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{clubDetail.upcoming_slots.map((slot, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border border-gray-200 rounded-lg p-4"
|
||||
>
|
||||
<p className="text-sm text-gray-600">{t('Slot')} #{index + 1}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
import { Locale } from '@/i18n-config';
|
||||
import { getTranslate } from '../../../dictionaries';
|
||||
import AdminClubDetailComponent from './AdminClubDetail';
|
||||
|
||||
export default async function AdminClubDetailPage({
|
||||
params
|
||||
}: {
|
||||
params: Promise<{ locale: Locale; club_id: string }>
|
||||
}) {
|
||||
const { locale, club_id } = await params;
|
||||
const {t} = await getTranslate(locale);
|
||||
const clubId = parseInt(club_id, 10);
|
||||
|
||||
return <AdminClubDetailComponent clubId={clubId} locale={locale} t={t} />;
|
||||
}
|
||||
@ -1,131 +1,10 @@
|
||||
import { Locale } from '@/i18n-config';
|
||||
import { getTranslate } from '../../dictionaries';
|
||||
import { Building, AlertCircle, Lock } from 'lucide-react';
|
||||
import { getAdminClubs } from '@/src/lib/api/admin-clubs';
|
||||
import Link from 'next/link';
|
||||
import { cookies } from 'next/headers';
|
||||
import AdminClubsList from './AdminClubsList';
|
||||
|
||||
export default async function AdminClubsPage({ params }: { params: Promise<{ locale: Locale }>}) {
|
||||
const { locale } = await params;
|
||||
const {t} = await getTranslate(locale);
|
||||
|
||||
// Get cookies for SSR auth
|
||||
const cookieStore = await cookies();
|
||||
const cookieHeader = cookieStore.toString();
|
||||
|
||||
// Fetch clubs from API - this will handle auth states via HTTP status codes
|
||||
const result = await getAdminClubs(cookieHeader);
|
||||
|
||||
// Handle authentication error (401)
|
||||
if (!result.success && result.error.status === 401) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-xl p-8">
|
||||
<div className="flex items-start space-x-4">
|
||||
<Lock className="w-8 h-8 text-yellow-600 flex-shrink-0 mt-1" />
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">
|
||||
{t('Authentication Required')}
|
||||
</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
{t('Please log in to access the venue management portal.')}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
{t('If you are a venue administrator and do not have access, please contact support.')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle other API errors
|
||||
if (!result.success) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl 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>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">
|
||||
{t('Error Loading Clubs')}
|
||||
</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
{result.error.detail}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 font-mono">
|
||||
{t('Error code')}: {result.error.code}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const clubs = result.data;
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
||||
{t('Club Management')}
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
{t('View and manage your venue locations')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{clubs.length === 0 ? (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-xl p-8">
|
||||
<div className="flex flex-col items-center text-center space-y-4">
|
||||
<Building className="w-16 h-16 text-blue-600" />
|
||||
<h2 className="text-xl font-bold text-gray-900">
|
||||
{t('No Clubs Assigned')}
|
||||
</h2>
|
||||
<p className="text-gray-600 max-w-md">
|
||||
{t('You are not currently assigned as an administrator for any clubs. Contact your organization to request access.')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{clubs.map((club) => (
|
||||
<Link
|
||||
key={club.club_id}
|
||||
href={`/${locale}/admin/clubs/${club.club_id}`}
|
||||
className="bg-white border border-gray-200 rounded-xl p-6 hover:shadow-lg hover:border-indigo-300 transition-all duration-200"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="p-3 bg-indigo-100 rounded-lg">
|
||||
<Building className="w-6 h-6 text-indigo-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-1">
|
||||
{club.name}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600">
|
||||
{club.timezone}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 pt-4 border-t border-gray-100">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600">{t('Courts')}</span>
|
||||
<span className="font-semibold text-gray-900">{club.courts}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return <AdminClubsList locale={locale} t={t} />;
|
||||
}
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
import { Locale } from '@/i18n-config';
|
||||
|
||||
export default async function DashboardPage({
|
||||
params
|
||||
}: {
|
||||
params: Promise<{ locale: Locale }>
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
|
||||
// Manager portal dashboard redirects to club management
|
||||
redirect(`/${locale}/admin/clubs`);
|
||||
}
|
||||
Loading…
Reference in New Issue