'use client'; import React, { useEffect, useState, use } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import { fetchFounderClubContext, FounderClubContext, sanitizePortalError, fetchClubMembers, updateMemberRank, DEFAULT_CLUB_RANKS, DEFAULT_CLUB_CAPABILITIES, DEFAULT_RANK_PERMISSIONS, } from '../../../../../lib/club-data'; import { ClubMember, ClubCapability } from '../../../../../components/founder-portal/types'; import { FounderPortalShell } from '../../../../../components/founder-portal/founder-portal-shell'; import { RolesAndRanksSummary } from '../../../../../components/founder-portal/roles-and-ranks-summary'; import { ClubHierarchyBuilder } from '../../../../../components/founder-portal/club-hierarchy-builder'; import { ClubPermissionMatrix } from '../../../../../components/founder-portal/club-permission-matrix'; 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 ClubFounderRanksRolesPage({ params }: PageProps) { const { clubId } = use(params); const router = useRouter(); const searchParams = useSearchParams(); const [context, setContext] = useState(null); const [members, setMembers] = useState([]); const [permissions, setPermissions] = useState(DEFAULT_RANK_PERMISSIONS); const [selectedRankId, setSelectedRankId] = useState('president'); 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, clubMembers] = await Promise.all([ fetchFounderClubContext(clubId, currentUser?.uid, { allowDemoPreview: isDemoPreview, }), fetchClubMembers(clubId, currentUser?.uid, { allowDemoPreview: isDemoPreview, }), ]); // Authorization Guard: Owner and Admin can access Ranks & Roles; Members cannot if (!isDemoPreview && !ctx.permissions.canManageSettings) { setUnauthorized(true); setLoading(false); return; } setContext(ctx); setMembers(clubMembers); } catch (err) { setError(sanitizePortalError(err, "Ranks and roles couldn't be loaded.")); } finally { setLoading(false); } } useEffect(() => { void loadData(); }, [clubId]); async function handleUpdateMemberRank(memberUid: string, newRank: string) { await updateMemberRank(clubId, memberUid, newRank); setMembers((prev) => prev.map((m) => (m.uid === memberUid ? { ...m, displayRank: newRank } : m)) ); } function handleTogglePermission(rankId: string, capability: ClubCapability) { setPermissions((prev) => ({ ...prev, [rankId]: { ...prev[rankId], [capability]: !prev[rankId]?.[capability], }, })); } 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')} />
); } if (error || !context) { return (
router.push('/clubs')} onRetry={loadData} />
); } return (
{/* Header */}
Governance & Authority // {context.club.name}

Ranks & Roles

Administrative role authority vs honorary squad rank designations.

{/* Roles and Ranks Summary Component */} {/* Interactive Visual Hierarchy Builder Tree */} {/* Interactive Permission Toggle Switches Matrix */} {/* Per-rank capability permissions have no server-side persistence contract yet. The enforced authorization model is role-based (Owner / Admin) via getPortalPermissions in lib/club-data.ts, not this rank matrix. Rendering the matrix read-only (persistenceAvailable={false}) keeps it truthful: it shows the standard governance conventions without claiming a save that does not occur. See website/docs/backend-contracts/club-rank-permissions.md for the proposed contract that would flip persistenceAvailable to true. */}
); }