From 9f47dab8d88c736654dbfcaa4a2faa4a20f07f38 Mon Sep 17 00:00:00 2001 From: Guillermo Pages Date: Fri, 7 Nov 2025 07:39:24 +0100 Subject: [PATCH] refactor(admin): convert to client-side rendering and add missing routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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.md --- .../[locale]/admin/clubs/AdminClubsList.tsx | 175 ++++++++++++ .../admin/clubs/[club_id]/AdminClubDetail.tsx | 262 ++++++++++++++++++ .../[locale]/admin/clubs/[club_id]/page.tsx | 15 + src/app/[locale]/admin/clubs/page.tsx | 125 +-------- src/app/[locale]/dashboard/page.tsx | 13 + 5 files changed, 467 insertions(+), 123 deletions(-) create mode 100644 src/app/[locale]/admin/clubs/AdminClubsList.tsx create mode 100644 src/app/[locale]/admin/clubs/[club_id]/AdminClubDetail.tsx create mode 100644 src/app/[locale]/admin/clubs/[club_id]/page.tsx create mode 100644 src/app/[locale]/dashboard/page.tsx diff --git a/src/app/[locale]/admin/clubs/AdminClubsList.tsx b/src/app/[locale]/admin/clubs/AdminClubsList.tsx new file mode 100644 index 0000000..0bcd677 --- /dev/null +++ b/src/app/[locale]/admin/clubs/AdminClubsList.tsx @@ -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(null); + const [error, setError] = useState(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 ( +
+
+ +

{t('Loading clubs...')}

+
+
+ ); + } + + // Authentication error (401) + if (error && error.status === 401) { + return ( +
+
+
+
+ +
+

+ {t('Authentication Required')} +

+

+ {t('Please log in to access the venue management portal.')} +

+

+ {t('If you are a venue administrator and do not have access, please contact support.')} +

+
+
+
+
+
+ ); + } + + // Other API errors + if (error) { + return ( +
+
+
+
+ +
+

+ {t('Error Loading Clubs')} +

+

+ {error.detail} +

+

+ {t('Error code')}: {error.code} +

+
+
+
+
+
+ ); + } + + // No clubs assigned + if (!clubs || clubs.length === 0) { + return ( +
+
+

+ {t('Club Management')} +

+

+ {t('View and manage your venue locations')} +

+
+ +
+
+ +

+ {t('No Clubs Assigned')} +

+

+ {t('You are not currently assigned as an administrator for any clubs. Contact your organization to request access.')} +

+
+
+
+ ); + } + + // Success - render clubs list + return ( +
+
+

+ {t('Club Management')} +

+

+ {t('View and manage your venue locations')} +

+
+ +
+ {clubs.map((club) => ( + +
+
+
+ +
+
+

+ {club.name} +

+

+ {club.timezone} +

+
+
+
+
+
+ {t('Courts')} + {club.courts} +
+
+ + ))} +
+
+ ); +} diff --git a/src/app/[locale]/admin/clubs/[club_id]/AdminClubDetail.tsx b/src/app/[locale]/admin/clubs/[club_id]/AdminClubDetail.tsx new file mode 100644 index 0000000..e86a593 --- /dev/null +++ b/src/app/[locale]/admin/clubs/[club_id]/AdminClubDetail.tsx @@ -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(null); + const [error, setError] = useState(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 ( +
+
+ +

{t('Loading club details...')}

+
+
+ ); + } + + // Authentication error (401) + if (error && error.status === 401) { + return ( +
+
+
+
+ +
+

+ {t('Authentication Required')} +

+

+ {t('Please log in to access the venue management portal.')} +

+
+
+
+
+
+ ); + } + + // Forbidden error (403) + if (error && error.status === 403) { + return ( +
+
+
+
+ +
+

+ {t('Access Denied')} +

+

+ {error.detail} +

+ + + {t('Back to clubs')} + +
+
+
+
+
+ ); + } + + // Other API errors + if (error) { + return ( +
+
+
+
+ +
+

+ {t('Error Loading Club')} +

+

+ {error.detail} +

+

+ {t('Error code')}: {error.code} +

+ + + {t('Back to clubs')} + +
+
+
+
+
+ ); + } + + if (!clubDetail) { + return null; + } + + // Success - render club detail + return ( +
+ {/* Breadcrumb */} +
+ + + {t('Back to clubs')} + +
+ + {/* Club Header */} +
+
+
+ +
+
+

+ {clubDetail.club.name} +

+
+
+ + {clubDetail.club.timezone} +
+
+
+
+
+ + {/* Provider Info */} +
+
+ +

{t('Provider Information')}

+
+
+
+ {t('Type')}: + {clubDetail.provider.remote_type} +
+
+ {t('Manages Slot Storage')}: + + {clubDetail.provider.capabilities.manages_slot_storage ? t('Yes') : t('No')} + +
+
+ {t('Supports Payment Verification')}: + + {clubDetail.provider.capabilities.supports_payment_verification ? t('Yes') : t('No')} + +
+
+
+ + {/* Courts */} +
+

{t('Courts')}

+ {clubDetail.courts.length === 0 ? ( +

{t('No courts configured')}

+ ) : ( +
+ {clubDetail.courts.map((court) => ( +
+

{court.name}

+
+ ))} +
+ )} +
+ + {/* Slot Definitions */} +
+
+ +

{t('Slot Definitions')}

+
+ {clubDetail.slot_definitions.length === 0 ? ( +

{t('No slot definitions configured')}

+ ) : ( +
+ {clubDetail.slot_definitions.map((slotDef, index) => ( +
+

{t('Slot definition')} #{index + 1}

+
+ ))} +
+ )} +
+ + {/* Upcoming Slots */} +
+

{t('Upcoming Slots')}

+ {clubDetail.upcoming_slots.length === 0 ? ( +

{t('No upcoming slots available')}

+ ) : ( +
+ {clubDetail.upcoming_slots.map((slot, index) => ( +
+

{t('Slot')} #{index + 1}

+
+ ))} +
+ )} +
+
+ ); +} diff --git a/src/app/[locale]/admin/clubs/[club_id]/page.tsx b/src/app/[locale]/admin/clubs/[club_id]/page.tsx new file mode 100644 index 0000000..0d0de1e --- /dev/null +++ b/src/app/[locale]/admin/clubs/[club_id]/page.tsx @@ -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 ; +} diff --git a/src/app/[locale]/admin/clubs/page.tsx b/src/app/[locale]/admin/clubs/page.tsx index d22a12e..e0dff73 100644 --- a/src/app/[locale]/admin/clubs/page.tsx +++ b/src/app/[locale]/admin/clubs/page.tsx @@ -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 ( -
-
-
-
- -
-

- {t('Authentication Required')} -

-

- {t('Please log in to access the venue management portal.')} -

-

- {t('If you are a venue administrator and do not have access, please contact support.')} -

-
-
-
-
-
- ); - } - - // Handle other API errors - if (!result.success) { - return ( -
-
-
-
- -
-

- {t('Error Loading Clubs')} -

-

- {result.error.detail} -

-

- {t('Error code')}: {result.error.code} -

-
-
-
-
-
- ); - } - - const clubs = result.data; - - return ( -
-
-

- {t('Club Management')} -

-

- {t('View and manage your venue locations')} -

-
- - {clubs.length === 0 ? ( -
-
- -

- {t('No Clubs Assigned')} -

-

- {t('You are not currently assigned as an administrator for any clubs. Contact your organization to request access.')} -

-
-
- ) : ( -
- {clubs.map((club) => ( - -
-
-
- -
-
-

- {club.name} -

-

- {club.timezone} -

-
-
-
-
-
- {t('Courts')} - {club.courts} -
-
- - ))} -
- )} -
- ); + return ; } diff --git a/src/app/[locale]/dashboard/page.tsx b/src/app/[locale]/dashboard/page.tsx new file mode 100644 index 0000000..52204a3 --- /dev/null +++ b/src/app/[locale]/dashboard/page.tsx @@ -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`); +}