From cd5201c13a5f0aa6c80d1e73c1704b2afd30a497 Mon Sep 17 00:00:00 2001 From: Developer Date: Sat, 12 Sep 2026 18:36:53 -0400 Subject: [PATCH] fix: isolate marketing layout checkpoint --- .../[clubId]/manage/applications/page.tsx | 139 -- .../clubs/[clubId]/manage/profile/page.tsx | 152 --- .../clubs/[clubId]/manage/settings/page.tsx | 158 --- .../founder-portal/founder-applications.tsx | 675 ---------- .../founder-portal/founder-portal-shell.tsx | 72 -- .../founder-portal/founder-profile-editor.tsx | 552 -------- .../founder-settings-editor.tsx | 1023 --------------- .../founder-portal/founder-sidebar.tsx | 285 ----- .../founder-portal/founder-top-bar.tsx | 178 --- website/lib/club-data.ts | 1138 ----------------- website/tests/founder-portal.test.cjs | 840 ------------ 11 files changed, 5212 deletions(-) delete mode 100644 website/app/clubs/[clubId]/manage/applications/page.tsx delete mode 100644 website/app/clubs/[clubId]/manage/profile/page.tsx delete mode 100644 website/app/clubs/[clubId]/manage/settings/page.tsx delete mode 100644 website/components/founder-portal/founder-applications.tsx delete mode 100644 website/components/founder-portal/founder-portal-shell.tsx delete mode 100644 website/components/founder-portal/founder-profile-editor.tsx delete mode 100644 website/components/founder-portal/founder-settings-editor.tsx delete mode 100644 website/components/founder-portal/founder-sidebar.tsx delete mode 100644 website/components/founder-portal/founder-top-bar.tsx delete mode 100644 website/lib/club-data.ts delete mode 100644 website/tests/founder-portal.test.cjs diff --git a/website/app/clubs/[clubId]/manage/applications/page.tsx b/website/app/clubs/[clubId]/manage/applications/page.tsx deleted file mode 100644 index b8e67e7..0000000 --- a/website/app/clubs/[clubId]/manage/applications/page.tsx +++ /dev/null @@ -1,139 +0,0 @@ -'use client'; - -import React, { useEffect, useState, use } from 'react'; -import { useRouter, useSearchParams } from 'next/navigation'; -import { - fetchFounderClubContext, - FounderClubContext, - sanitizePortalError, -} from '../../../../../lib/club-data'; -import { FounderPortalShell } from '../../../../../components/founder-portal/founder-portal-shell'; -import { FounderApplications } from '../../../../../components/founder-portal/founder-applications'; -import { OverviewSkeleton } from '../../../../../components/founder-portal/skeleton-state'; -import { SafeErrorState } from '../../../../../components/founder-portal/safe-error-state'; -import { firebaseAuth } from '../../../../../lib/firebase-client'; - -interface PageProps { - params: Promise<{ clubId: string }>; -} - -export default function ClubFounderApplicationsPage({ params }: PageProps) { - const { clubId } = use(params); - const router = useRouter(); - const searchParams = useSearchParams(); - - const [context, setContext] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [unauthenticated, setUnauthenticated] = useState(false); - const [unauthorized, setUnauthorized] = useState(false); - - async function loadData() { - setLoading(true); - setError(null); - setUnauthenticated(false); - setUnauthorized(false); - - try { - await firebaseAuth.authStateReady(); - const currentUser = firebaseAuth.currentUser; - const isDemoPreview = searchParams?.get('preview') === 'demo'; - - // Unauthenticated Guard: Direct access without demo preview requires authentication - if (!isDemoPreview && !currentUser) { - setUnauthenticated(true); - setLoading(false); - return; - } - - const ctx = await fetchFounderClubContext(clubId, currentUser?.uid, { - allowDemoPreview: isDemoPreview, - }); - - // Authorization Guard: Owner and Admin can access Applications; Members cannot - if (!isDemoPreview && (!ctx.permissions.canApproveApplications && !ctx.permissions.canManageSettings)) { - setUnauthorized(true); - setLoading(false); - return; - } - - setContext(ctx); - } catch (err) { - setError(sanitizePortalError(err, "Club applications couldn't be loaded.")); - } finally { - setLoading(false); - } - } - - useEffect(() => { - void loadData(); - }, [clubId]); - - if (loading) { - return ( -
- -
- ); - } - - // Unauthenticated safe error state - if (unauthenticated) { - return ( -
- router.push('/login')} - /> -
- ); - } - - // Unauthorized (Member or non-admin) safe error state - if (unauthorized) { - return ( -
- router.push('/clubs')} - /> -
- ); - } - - // Error or missing context - if (error || !context) { - return ( -
- router.push('/clubs')} - onRetry={loadData} - /> -
- ); - } - - return ( - - - - ); -} diff --git a/website/app/clubs/[clubId]/manage/profile/page.tsx b/website/app/clubs/[clubId]/manage/profile/page.tsx deleted file mode 100644 index 8a06364..0000000 --- a/website/app/clubs/[clubId]/manage/profile/page.tsx +++ /dev/null @@ -1,152 +0,0 @@ -'use client'; - -import React, { useEffect, useState, use } from 'react'; -import { useRouter, useSearchParams } from 'next/navigation'; -import { - fetchFounderClubContext, - FounderClubContext, - sanitizePortalError, -} from '../../../../../lib/club-data'; -import { FounderPortalShell } from '../../../../../components/founder-portal/founder-portal-shell'; -import { FounderProfileEditor } from '../../../../../components/founder-portal/founder-profile-editor'; -import { OverviewSkeleton } from '../../../../../components/founder-portal/skeleton-state'; -import { SafeErrorState } from '../../../../../components/founder-portal/safe-error-state'; -import { firebaseAuth } from '../../../../../lib/firebase-client'; - -interface PageProps { - params: Promise<{ clubId: string }>; -} - -export default function ClubFounderProfilePage({ params }: PageProps) { - const { clubId } = use(params); - const router = useRouter(); - const searchParams = useSearchParams(); - - const [context, setContext] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [unauthenticated, setUnauthenticated] = useState(false); - const [unauthorized, setUnauthorized] = useState(false); - - async function loadData() { - setLoading(true); - setError(null); - setUnauthenticated(false); - setUnauthorized(false); - - try { - await firebaseAuth.authStateReady(); - const currentUser = firebaseAuth.currentUser; - const isDemoPreview = searchParams?.get('preview') === 'demo'; - - // Unauthenticated Guard: Direct access without demo preview requires authentication - if (!isDemoPreview && !currentUser) { - setUnauthenticated(true); - setLoading(false); - return; - } - - const ctx = await fetchFounderClubContext(clubId, currentUser?.uid, { - allowDemoPreview: isDemoPreview, - }); - - // Authorization Guard: Only Owner and Admin can manage Club Profile - if (!isDemoPreview && !ctx.permissions.canManageSettings) { - setUnauthorized(true); - setLoading(false); - return; - } - - setContext(ctx); - } catch (err) { - setError(sanitizePortalError(err, "Club profile couldn't be loaded.")); - } finally { - setLoading(false); - } - } - - useEffect(() => { - void loadData(); - }, [clubId]); - - if (loading) { - return ( -
- -
- ); - } - - // Unauthenticated safe error state - if (unauthenticated) { - return ( -
- router.push('/login')} - /> -
- ); - } - - // Unauthorized (Member or non-admin) safe error state - if (unauthorized) { - return ( -
- router.push('/clubs')} - /> -
- ); - } - - // Error or missing context - if (error || !context) { - return ( -
- router.push('/clubs')} - onRetry={loadData} - /> -
- ); - } - - return ( - - { - setContext((prev) => - prev - ? { - ...prev, - club: { - ...prev.club, - ...updated, - }, - } - : null - ); - }} - /> - - ); -} diff --git a/website/app/clubs/[clubId]/manage/settings/page.tsx b/website/app/clubs/[clubId]/manage/settings/page.tsx deleted file mode 100644 index 4ca7957..0000000 --- a/website/app/clubs/[clubId]/manage/settings/page.tsx +++ /dev/null @@ -1,158 +0,0 @@ -'use client'; - -import React, { useEffect, useState, use } from 'react'; -import { useRouter, useSearchParams } from 'next/navigation'; -import { - fetchFounderClubContext, - FounderClubContext, - sanitizePortalError, -} from '../../../../../lib/club-data'; -import { FounderPortalShell } from '../../../../../components/founder-portal/founder-portal-shell'; -import { FounderSettingsEditor } from '../../../../../components/founder-portal/founder-settings-editor'; -import { OverviewSkeleton } from '../../../../../components/founder-portal/skeleton-state'; -import { SafeErrorState } from '../../../../../components/founder-portal/safe-error-state'; -import { firebaseAuth } from '../../../../../lib/firebase-client'; - -interface PageProps { - params: Promise<{ clubId: string }>; -} - -export default function ClubFounderSettingsPage({ params }: PageProps) { - const { clubId } = use(params); - const router = useRouter(); - const searchParams = useSearchParams(); - - const [context, setContext] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [unauthenticated, setUnauthenticated] = useState(false); - const [unauthorized, setUnauthorized] = useState(false); - - async function loadData() { - setLoading(true); - setError(null); - setUnauthenticated(false); - setUnauthorized(false); - - try { - await firebaseAuth.authStateReady(); - const currentUser = firebaseAuth.currentUser; - const isDemoPreview = searchParams?.get('preview') === 'demo'; - - // Unauthenticated Guard: Direct access without demo preview requires authentication - if (!isDemoPreview && !currentUser) { - setUnauthenticated(true); - setLoading(false); - return; - } - - const ctx = await fetchFounderClubContext(clubId, currentUser?.uid, { - allowDemoPreview: isDemoPreview, - }); - - // Authorization Guard: Owner and Admin can access Settings; Members cannot - if (!isDemoPreview && !ctx.permissions.canManageSettings) { - setUnauthorized(true); - setLoading(false); - return; - } - - setContext(ctx); - } catch (err) { - setError(sanitizePortalError(err, "Club settings couldn't be loaded.")); - } finally { - setLoading(false); - } - } - - useEffect(() => { - void loadData(); - }, [clubId]); - - if (loading) { - return ( -
- -
- ); - } - - // Unauthenticated safe error state - if (unauthenticated) { - return ( -
- router.push('/login')} - /> -
- ); - } - - // Unauthorized (Member or non-admin) safe error state - if (unauthorized) { - return ( -
- router.push('/clubs')} - /> -
- ); - } - - // Error or missing context - if (error || !context) { - return ( -
- router.push('/clubs')} - onRetry={loadData} - /> -
- ); - } - - return ( - - { - setContext((prev) => { - if (!prev) return null; - const isFormerOwner = updated.ownerUid && updated.ownerUid !== prev.club.ownerUid; - return { - ...prev, - club: { - ...prev.club, - ...updated, - }, - currentUser: isFormerOwner - ? { ...prev.currentUser, role: 'admin', displayRank: 'Admin' } - : prev.currentUser, - permissions: isFormerOwner - ? { ...prev.permissions, role: 'admin', canTransferOwnership: false } - : prev.permissions, - }; - }); - }} - /> - - ); -} diff --git a/website/components/founder-portal/founder-applications.tsx b/website/components/founder-portal/founder-applications.tsx deleted file mode 100644 index 8c2160b..0000000 --- a/website/components/founder-portal/founder-applications.tsx +++ /dev/null @@ -1,675 +0,0 @@ -'use client'; - -import React, { useState, useEffect, useMemo } from 'react'; -import { Club, UserPortalPermissions } from './types'; -import { - ClubApplication, - fetchClubApplications, - approveClubApplication, - declineClubApplication, - sanitizePortalError, -} from '../../lib/club-data'; - -interface FounderApplicationsProps { - club: Club; - permissions: UserPortalPermissions; - callerUid: string; - isDemoFixture?: boolean; -} - -export function FounderApplications({ - club, - permissions, - callerUid, - isDemoFixture = false, -}: FounderApplicationsProps) { - const [applications, setApplications] = useState([]); - const [selectedApplicantUid, setSelectedApplicantUid] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [searchQuery, setSearchQuery] = useState(''); - const [sortBy, setSortBy] = useState<'newest' | 'oldest' | 'name'>('newest'); - - // Mutation and modal states - const [mutatingUid, setMutatingUid] = useState(null); - const [actionError, setActionError] = useState(null); - const [approveModalApplicant, setApproveModalApplicant] = useState(null); - const [declineModalApplicant, setDeclineModalApplicant] = useState(null); - - // Local member count to reflect approval transitions immediately - const [localMemberCount, setLocalMemberCount] = useState(club.memberCount); - - async function loadApplications() { - setLoading(true); - setError(null); - try { - const data = await fetchClubApplications(club.id, callerUid, { - allowDemoPreview: isDemoFixture, - }); - setApplications(data); - if (data.length > 0) { - setSelectedApplicantUid((prev) => prev ?? data[0].uid); - } - } catch (err) { - setError(sanitizePortalError(err, "Applications couldn't be loaded.")); - } finally { - setLoading(false); - } - } - - useEffect(() => { - void loadApplications(); - }, [club.id, callerUid, isDemoFixture]); - - // Filtered and sorted applications - const filteredApplications = useMemo(() => { - let list = [...applications]; - - if (searchQuery.trim()) { - const q = searchQuery.toLowerCase().trim(); - list = list.filter((app) => { - const nameMatch = app.displayName.toLowerCase().includes(q); - const handleMatch = app.handle ? app.handle.toLowerCase().includes(q) : false; - return nameMatch || handleMatch; - }); - } - - list.sort((a, b) => { - if (sortBy === 'newest') { - return new Date(b.requestedAt).getTime() - new Date(a.requestedAt).getTime(); - } - if (sortBy === 'oldest') { - return new Date(a.requestedAt).getTime() - new Date(b.requestedAt).getTime(); - } - return a.displayName.localeCompare(b.displayName); - }); - - return list; - }, [applications, searchQuery, sortBy]); - - const selectedApplicant = useMemo(() => { - if (!selectedApplicantUid) return filteredApplications[0] || null; - return applications.find((a) => a.uid === selectedApplicantUid) || filteredApplications[0] || null; - }, [applications, filteredApplications, selectedApplicantUid]); - - function getMonogram(name: string) { - const parts = name.trim().split(/\s+/).filter(Boolean); - if (parts.length === 0) return 'CA'; - if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase(); - return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); - } - - function formatDate(isoString: string) { - try { - const date = new Date(isoString); - return date.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - }); - } catch { - return isoString; - } - } - - async function handleApprove(applicant: ClubApplication) { - if (mutatingUid) return; - setMutatingUid(applicant.uid); - setActionError(null); - - try { - if (!isDemoFixture) { - await approveClubApplication(club.id, callerUid, applicant.uid); - } - // Remove from list and increment member count - setApplications((prev) => prev.filter((a) => a.uid !== applicant.uid)); - setLocalMemberCount((c) => c + 1); - setApproveModalApplicant(null); - - // Select next applicant if the approved one was selected - if (selectedApplicantUid === applicant.uid) { - const remaining = applications.filter((a) => a.uid !== applicant.uid); - setSelectedApplicantUid(remaining[0]?.uid || null); - } - } catch (err) { - setActionError(sanitizePortalError(err, 'Unable to approve application. Please try again.')); - } finally { - setMutatingUid(null); - } - } - - async function handleDecline(applicant: ClubApplication) { - if (mutatingUid) return; - setMutatingUid(applicant.uid); - setActionError(null); - - try { - if (!isDemoFixture) { - await declineClubApplication(club.id, callerUid, applicant.uid); - } - // Remove from list (non-destructive in Firestore, memberCount delta 0) - setApplications((prev) => prev.filter((a) => a.uid !== applicant.uid)); - setDeclineModalApplicant(null); - - // Select next applicant if the declined one was selected - if (selectedApplicantUid === applicant.uid) { - const remaining = applications.filter((a) => a.uid !== applicant.uid); - setSelectedApplicantUid(remaining[0]?.uid || null); - } - } catch (err) { - setActionError(sanitizePortalError(err, 'Unable to decline application. Please try again.')); - } finally { - setMutatingUid(null); - } - } - - return ( -
- {/* 1. PAGE HEADER */} -
-
-
-

APPLICATIONS

- - {applications.length} PENDING - -
-

Review riders requesting to join this Club.

-
-
- - {/* 2. SUMMARY METRIC CARDS ROW */} -
- {/* Card 1: Pending */} -
-
- Pending - -
-
- {applications.length} -
-
Awaiting review
-
- - {/* Card 2: Admission Policy */} -
-
- Admission Policy - -
-
- - {club.admissionPolicy} - -
-
Club join governance rule
-
- - {/* Card 3: Active Members */} -
-
- Active Members - -
-
- {localMemberCount} -
-
Current club roster
-
- - {/* Card 4: Reviewer Authority */} -
-
- Review Authority - -
-
- - {permissions.role === 'owner' ? 'OWNER ACCESS' : 'ADMIN ACCESS'} - -
-
Software administrative rights
-
-
- - {/* Action Error Alert */} - {actionError && ( -
- {actionError} - -
- )} - - {/* 3. FILTER & SEARCH TOOLBAR */} -
- {/* Filter Tab */} -
- -
- - {/* Controls Right */} -
- {/* Text Search Bar */} -
- setSearchQuery(e.target.value)} - className="w-full h-8 pl-8 pr-8 rounded-md bg-[#141923] border border-[#262D3D] text-xs text-white placeholder-slate-500 focus:outline-none focus:border-[#0088FF]" - aria-label="Search applicants" - /> - - - - {searchQuery && ( - - )} -
- - {/* Sort By Dropdown */} -
- SORT: - -
-
-
- - {/* 4. MAIN CONTENT AREA */} - {loading ? ( -
- - progress_activity - - Loading pending applications... -
- ) : error ? ( -
-
- error - ERROR -
-

{error}

- -
- ) : applications.length === 0 ? ( - /* Empty State 1: Default zero-pending queue */ -
-
- person_add_disabled -
-
-

NO PENDING APPLICATIONS

-

- There are no membership requests waiting for review. New requests will appear here when riders apply. -

-
-
- ) : filteredApplications.length === 0 ? ( - /* Empty State 2: Filter zero-match */ -
-
- search_off -
-
-

NO APPLICATIONS MATCH

-

- No pending applications match "{searchQuery}". -

- -
-
- ) : ( - /* Two-Column Split View */ -
- {/* Left Column: Dense Application Table */} -
-
- - - - - - - - - - - {filteredApplications.map((app) => { - const isSelected = selectedApplicant?.uid === app.uid; - const isMutating = mutatingUid === app.uid; - - return ( - setSelectedApplicantUid(app.uid)} - className={`cursor-pointer transition-colors ${ - isSelected - ? 'bg-[#141923] border-l-2 border-[#0088FF]' - : 'hover:bg-[#141923]/50' - }`} - > - - - - - - ); - })} - -
RiderRequestedStatusActions
-
-
- {getMonogram(app.displayName)} -
-
-
- {app.displayName} - {isSelected && ( - - INSPECTING - - )} -
- {app.handle && ( -
- {app.handle} -
- )} -
-
-
- {formatDate(app.requestedAt)} - - - PENDING - - -
e.stopPropagation()} - > - - - -
-
-
- - {/* Pagination / Count Footer */} -
- - Showing {filteredApplications.length} of {applications.length} pending applications - -
-
- - {/* Right Column: Selected Application Inspector Drawer */} - {selectedApplicant && ( -
-
- {/* Top Cyan Accent Line */} -
- -
- {/* Header */} -
-
-
- {getMonogram(selectedApplicant.displayName)} -
-
-

- {selectedApplicant.displayName} -

- {selectedApplicant.handle && ( -
- {selectedApplicant.handle} -
- )} -
-
- -
- - PENDING - - - JOIN REQUEST - -
-
- -
- - {/* Rider Spec Grid */} -
-
- Requested - - {formatDate(selectedApplicant.requestedAt)} - -
- -
- Initial Club Rank - Member (Default) -
- -
- Administrative Role - Member (Non-admin) -
-
- - {/* Mandatory Privacy Architecture Notice */} -
-
- shield - MANDATORY PRIVACY ARCHITECTURE -
-

- Club administrators do not receive rider telemetry, private routes, or live GPS upon application. Club membership alone never grants access to rider location. Location sharing is strictly scoped to mutually joined active Group Rides. -

-
- - {/* Architecture Note */} -
- ⓘ Club ranks are organizational titles. Administrative roles determine Club management permissions. ClubMember != GroupRideParticipant. -
- - {/* Primary Actions */} -
- - - -
-
-
-
- )} -
- )} - - {/* MODAL 1: APPROVE APPLICATION */} - {approveModalApplicant && ( -
-
-
-
- - - APPROVAL CONFIRMATION - -
-
- -

Approve Application?

-

- Approve {approveModalApplicant.displayName}'s request to join {club.name}? On approval, this rider becomes an active Club member. -

- -
-
- Initial Club Rank: - Member (Default) -
-
- Administrative Role: - Member (Default Member, non-admin) -
-
- -
- ⓘ Club ranks are organizational titles. Administrative roles determine Club management permissions. ClubMember != GroupRideParticipant. -
- -
- - -
-
-
- )} - - {/* MODAL 2: DECLINE APPLICATION */} - {declineModalApplicant && ( -
-
-
-
- - - DECLINE APPLICATION - -
-
- -

Decline Application?

-

- Decline {declineModalApplicant.displayName}'s request to join this Club? This removes the pending request. The rider's MotorSignal account remains unaffected. -

- -
- - -
-
-
- )} -
- ); -} diff --git a/website/components/founder-portal/founder-portal-shell.tsx b/website/components/founder-portal/founder-portal-shell.tsx deleted file mode 100644 index 7b5768b..0000000 --- a/website/components/founder-portal/founder-portal-shell.tsx +++ /dev/null @@ -1,72 +0,0 @@ -'use client'; - -import React, { useState } from 'react'; -import { Club, UserPortalPermissions, ClubRole } from './types'; -import { FounderSidebar } from './founder-sidebar'; -import { FounderTopBar } from './founder-top-bar'; - -interface FounderPortalShellProps { - club: Club; - permissions: UserPortalPermissions; - activeNav?: string; - onNavSelect?: (nav: string) => void; - onPlanRide?: () => void; - onPostAnnouncement?: () => void; - onSwitchRole?: (role: ClubRole) => void; - onSwitchClub?: () => void; - pendingApplicationsCount?: number; - upcomingRidesCount?: number; - children: React.ReactNode; -} - -export function FounderPortalShell({ - club, - permissions, - activeNav = 'overview', - onNavSelect, - onPlanRide, - onPostAnnouncement, - onSwitchRole, - onSwitchClub, - pendingApplicationsCount = 4, - upcomingRidesCount = 2, - children, -}: FounderPortalShellProps) { - const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); - - return ( -
- {/* Sidebar (Desktop persistent + Mobile off-canvas drawer) */} - setMobileSidebarOpen(false)} - /> - - {/* Main Layout Area */} -
- {/* Top App Bar */} - setMobileSidebarOpen(!mobileSidebarOpen)} - onPlanRide={onPlanRide} - onPostAnnouncement={onPostAnnouncement} - onSwitchRole={onSwitchRole} - onSwitchClub={onSwitchClub} - /> - - {/* Content Body Container */} -
- {children} -
-
-
- ); -} diff --git a/website/components/founder-portal/founder-profile-editor.tsx b/website/components/founder-portal/founder-profile-editor.tsx deleted file mode 100644 index 5a9453a..0000000 --- a/website/components/founder-portal/founder-profile-editor.tsx +++ /dev/null @@ -1,552 +0,0 @@ -'use client'; - -import React, { useState } from 'react'; -import Link from 'next/link'; -import { Club, UserPortalPermissions } from './types'; -import { updateClubProfile, sanitizePortalError } from '../../lib/club-data'; - -interface FounderProfileEditorProps { - club: Club; - permissions: UserPortalPermissions; - callerUid?: string; - isDemoFixture?: boolean; - onProfileUpdated?: (updated: Partial) => void; -} - -export function FounderProfileEditor({ - club, - permissions, - callerUid, - isDemoFixture = false, - onProfileUpdated, -}: FounderProfileEditorProps) { - // Form fields matching Section 1 & 2 - const [name, setName] = useState(club.name || ''); - const [description, setDescription] = useState(club.description || ''); - const [homeRegion, setHomeRegion] = useState(club.homeRegion || ''); - const [motorcycleFocus, setMotorcycleFocus] = useState(club.motorcycleFocus || ''); - - // Save states - const [isSaving, setIsSaving] = useState(false); - const [saveStatus, setSaveStatus] = useState<'idle' | 'success' | 'error'>('idle'); - const [errorMessage, setErrorMessage] = useState(null); - - // Validation - const trimmedName = name.trim(); - const isNameValid = trimmedName.length >= 3 && trimmedName.length <= 50; - const isDescValid = description.trim().length <= 500; - const isRegionValid = homeRegion.trim().length <= 100; - const isFocusValid = motorcycleFocus.trim().length <= 100; - const canSave = isNameValid && isDescValid && isRegionValid && isFocusValid && !isSaving && permissions.canManageSettings; - - const isDirty = - trimmedName !== (club.name || '').trim() || - description.trim() !== (club.description || '').trim() || - homeRegion.trim() !== (club.homeRegion || '').trim() || - motorcycleFocus.trim() !== (club.motorcycleFocus || '').trim(); - - // Short initials for preview emblem - const initials = name - .split(/\s+/) - .filter(Boolean) - .map((w) => w[0].toUpperCase()) - .slice(0, 3) - .join('') || 'MSR'; - - async function handleSave(e: React.FormEvent) { - e.preventDefault(); - if (!canSave) return; - - setIsSaving(true); - setSaveStatus('idle'); - setErrorMessage(null); - - try { - if (isDemoFixture) { - // Simulated latency in development demo preview mode - await new Promise((resolve) => setTimeout(resolve, 600)); - setSaveStatus('success'); - onProfileUpdated?.({ - name: trimmedName, - description: description.trim(), - homeRegion: homeRegion.trim(), - motorcycleFocus: motorcycleFocus.trim(), - }); - } else { - if (!callerUid) { - throw new Error('Authentication required to save profile.'); - } - await updateClubProfile(club.id, callerUid, { - name: trimmedName, - description: description.trim(), - homeRegion: homeRegion.trim(), - motorcycleFocus: motorcycleFocus.trim(), - }); - setSaveStatus('success'); - onProfileUpdated?.({ - name: trimmedName, - description: description.trim(), - homeRegion: homeRegion.trim(), - motorcycleFocus: motorcycleFocus.trim(), - }); - } - } catch (err) { - setSaveStatus('error'); - setErrorMessage(sanitizePortalError(err, 'Failed to update club profile. Please try again.')); - } finally { - setIsSaving(false); - } - } - - return ( -
- {/* 3. PAGE HEADER */} -
-
-
- - PRECISION, NOT AGGRESSION - - - // {homeRegion ? `${homeRegion.toUpperCase()} ZONE` : 'REGIONAL SQUADRON'} - -
-

- CLUB PROFILE -

-

- Manage public-facing club identity, branding presentation, and discovery presence. -

-
-
- - {/* 4. TOP 4 METRIC CARDS */} -
- {/* Card 1: Club Name */} -
-
- CLUB NAME - -
-
-
{name || club.name}
-
3-50 chars validated
-
-
- - {/* Card 2: Home Region */} -
-
- HOME REGION - location_on -
-
-
- {homeRegion || 'Not specified'} -
-
Home Region
-
-
- - {/* Card 3: Motorcycle Focus */} -
-
- MOTORCYCLE FOCUS - sports_motorsports -
-
-
- {motorcycleFocus || 'General'} -
-
Motorcycle Focus
-
-
- - {/* Card 4: Discovery Presence */} -
-
- DISCOVERY PRESENCE - -
-
-
- {club.visibility} -
-
- {club.visibility === 'public' - ? 'Listed in public club index & directory' - : club.visibility === 'unlisted' - ? 'Accessible via direct handle only' - : 'Restricted private club'} -
-
-
-
- - {/* 5. MAIN WORKSPACE (12-Column Grid) */} -
- {/* LEFT MAIN (8 cols) */} -
- {/* Status Feedback Banner */} - {saveStatus === 'success' && ( -
- check_circle -
- Club profile changes have been saved successfully. -
-
- )} - - {saveStatus === 'error' && errorMessage && ( -
- error -
{errorMessage}
-
- )} - - {/* SECTION 1: PUBLIC CLUB IDENTITY */} -
-
-
- badge -

- SECTION 1: PUBLIC CLUB IDENTITY -

-
-
- - {/* Club Name */} -
- -
- setName(e.target.value)} - disabled={!permissions.canManageSettings || isSaving} - placeholder="e.g. MotorSignal Riders Atlanta" - className="w-full bg-surface-dim border border-outline-variant rounded px-3 py-2 text-sm font-body text-on-surface focus:border-primary focus:ring-0 outline-none transition-colors" - /> - - {isNameValid ? 'VALID' : `${trimmedName.length}/50`} - -
-

- 3-50 characters. Displayed on public discovery, ride manifests, and club roster. -

-
- - {/* Club Handle / Identifier (Locked) */} -
-
- - - IMMUTABLE - -
-
- lock - -
-

- Club handles are permanently reserved and cannot be renamed or reassigned. -

-
- - {/* Club Description */} -
-
- - - {description.length}/500 - -
-