'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; onApplicationsCountChange?: (count: number) => void; } export function FounderApplications({ club, permissions, callerUid, isDemoFixture = false, onApplicationsCountChange, }: 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); onApplicationsCountChange?.(data.length); 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 const remaining = applications.filter((a) => a.uid !== applicant.uid); setApplications(remaining); onApplicationsCountChange?.(remaining.length); setLocalMemberCount((c) => c + 1); setApproveModalApplicant(null); // Select next applicant if the approved one was selected if (selectedApplicantUid === 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) const remaining = applications.filter((a) => a.uid !== applicant.uid); setApplications(remaining); onApplicationsCountChange?.(remaining.length); setDeclineModalApplicant(null); // Select next applicant if the declined one was selected if (selectedApplicantUid === 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 */}
Precision, Not Aggression // Membership Triage

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-9 pl-9 pr-8 rounded-xl bg-surface-container-low/90 border border-brand-border/80 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-brand-cyan transition-colors" 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 to join this Club.

) : 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-brand-cyan/10 border-l-2 border-brand-cyan' : 'hover:bg-surface-container-high/40' }`} > ); })}
Rider Requested Status Actions
{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.

)}
); }