178 lines
7.0 KiB
TypeScript
178 lines
7.0 KiB
TypeScript
'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 { RolesAndRanksSummary } from '../../../../../components/founder-portal/roles-and-ranks-summary';
|
|
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<FounderClubContext | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [unauthenticated, setUnauthenticated] = useState(false);
|
|
|
|
async function loadData() {
|
|
setLoading(true);
|
|
setError(null);
|
|
setUnauthenticated(false);
|
|
|
|
try {
|
|
await firebaseAuth.authStateReady();
|
|
const currentUser = firebaseAuth.currentUser;
|
|
const isDemoPreview = searchParams?.get('preview') === 'demo';
|
|
|
|
if (!isDemoPreview && !currentUser) {
|
|
setUnauthenticated(true);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
const ctx = await fetchFounderClubContext(clubId, currentUser?.uid, {
|
|
allowDemoPreview: isDemoPreview,
|
|
});
|
|
|
|
setContext(ctx);
|
|
} catch (err) {
|
|
setError(sanitizePortalError(err, "Ranks and roles couldn't be loaded."));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
void loadData();
|
|
}, [clubId]);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen bg-brand-bg telemetry-grid-bg p-6">
|
|
<div className="max-w-5xl mx-auto">
|
|
<OverviewSkeleton />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (unauthenticated) {
|
|
return (
|
|
<div className="min-h-screen bg-brand-bg telemetry-grid-bg flex items-center justify-center p-6">
|
|
<div className="max-w-5xl mx-auto w-full flex items-center justify-center">
|
|
<SafeErrorState
|
|
title="Authentication Required"
|
|
message="Sign in with your rider account to access club ranks and roles."
|
|
actionLabel="Sign In"
|
|
onAction={() => router.push('/login')}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error || !context) {
|
|
return (
|
|
<div className="min-h-screen bg-brand-bg telemetry-grid-bg flex items-center justify-center p-6">
|
|
<div className="max-w-5xl mx-auto w-full flex items-center justify-center">
|
|
<SafeErrorState
|
|
title="Ranks & Roles Unavailable"
|
|
message={error ?? "Ranks and roles couldn't be loaded."}
|
|
actionLabel="Return to Clubs"
|
|
onAction={() => router.push('/clubs')}
|
|
onRetry={loadData}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<FounderPortalShell
|
|
club={context.club}
|
|
permissions={context.permissions}
|
|
activeNav="ranks_roles"
|
|
pendingApplicationsCount={0}
|
|
upcomingRidesCount={0}
|
|
>
|
|
<div className="max-w-6xl mx-auto space-y-6 pb-12">
|
|
{/* Header */}
|
|
<section className="p-6 rounded-2xl glass-card border border-brand-border/60 relative overflow-hidden flex flex-col md:flex-row md:items-end justify-between gap-4 shadow-lg">
|
|
<div className="absolute top-0 left-0 right-0 h-0.5 bg-gradient-to-r from-brand-cyan/60 via-brand-cyan/20 to-transparent" />
|
|
<div>
|
|
<div className="flex items-center gap-3">
|
|
<span className="font-mono text-xs text-brand-cyan tracking-widest bg-brand-cyan/10 px-2.5 py-0.5 rounded-full border border-brand-cyan/30 font-medium">
|
|
Governance & Authority
|
|
</span>
|
|
<span className="font-mono text-xs text-slate-400 tracking-wider">
|
|
// {context.club.name}
|
|
</span>
|
|
</div>
|
|
<h1 className="font-headline font-bold text-2xl sm:text-3xl text-white mt-2 tracking-tight">
|
|
Ranks & Roles
|
|
</h1>
|
|
<p className="font-body text-sm text-slate-400 mt-1">
|
|
Administrative role authority vs honorary squad rank designations.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Roles and Ranks Summary Component */}
|
|
<RolesAndRanksSummary
|
|
ownerName={context.ownerProfile?.displayName || context.club.founderName || 'Club Founder'}
|
|
memberCount={context.club.memberCount}
|
|
/>
|
|
|
|
{/* Rank Hierarchy Detail Table */}
|
|
<section className="rounded-2xl glass-card border border-brand-border/60 p-5 sm:p-6 space-y-4 shadow-lg">
|
|
<div className="border-b border-brand-border/60 pb-3 flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<span className="material-symbols-outlined text-brand-cyan text-lg">military_tech</span>
|
|
<h2 className="font-headline text-base font-bold text-white">
|
|
Squad Rank Structure
|
|
</h2>
|
|
</div>
|
|
<span className="font-mono text-[11px] text-slate-400">
|
|
6 Recognized Titles
|
|
</span>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
{[
|
|
{ rank: 'President', desc: 'Club founder and chief organizational executive.', level: 'Rank 1' },
|
|
{ rank: 'Vice President', desc: 'Senior operational deputy and squadron coordinator.', level: 'Rank 2' },
|
|
{ rank: 'Road Captain', desc: 'Leads pace line, group positioning, and on-road safety.', level: 'Rank 3' },
|
|
{ rank: 'Road Marshal', desc: 'Rear sweep and group communications integrity.', level: 'Rank 4' },
|
|
{ rank: 'Senior Rider', desc: 'Veteran rider with 1,000+ verified club group miles.', level: 'Rank 5' },
|
|
{ rank: 'Member', desc: 'Verified active rider in regular squadron standing.', level: 'Rank 6' },
|
|
].map((r) => (
|
|
<div key={r.rank} className="p-4 rounded-xl bg-surface-container-low border border-brand-border/60 space-y-1.5">
|
|
<div className="flex items-center justify-between">
|
|
<span className="font-headline font-semibold text-white text-xs">{r.rank}</span>
|
|
<span className="font-mono text-[10px] text-brand-cyan px-2 py-0.5 rounded bg-brand-cyan/10 border border-brand-cyan/30">
|
|
{r.level}
|
|
</span>
|
|
</div>
|
|
<p className="font-body text-xs text-slate-400 leading-relaxed">{r.desc}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</FounderPortalShell>
|
|
);
|
|
}
|