169 lines
5.9 KiB
TypeScript
169 lines
5.9 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);
|
|
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 Ranks & Roles; Members cannot
|
|
if (!isDemoPreview && !ctx.permissions.canManageSettings) {
|
|
setUnauthorized(true);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
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>
|
|
);
|
|
}
|
|
|
|
// Unauthenticated safe error state
|
|
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>
|
|
);
|
|
}
|
|
|
|
// Unauthorized (Member or non-admin) safe error state
|
|
if (unauthorized) {
|
|
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="Management Access Restricted"
|
|
message="You must be an Owner or Admin of this Club to access Ranks & Roles."
|
|
actionLabel="Return to Clubs"
|
|
onAction={() => router.push('/clubs')}
|
|
/>
|
|
</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}
|
|
/>
|
|
</div>
|
|
</FounderPortalShell>
|
|
);
|
|
}
|