96 lines
5.1 KiB
TypeScript
96 lines
5.1 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { onAuthStateChanged } from 'firebase/auth';
|
|
import { useRouter, useSearchParams } from 'next/navigation';
|
|
import { firebaseAuth } from '../../lib/firebase-client';
|
|
import { fetchFounderClubContext, sanitizePortalError, type FounderClubContext } from '../../lib/club-data';
|
|
import { FounderPortalShell } from './founder-portal-shell';
|
|
import { FounderApplications } from './founder-applications';
|
|
import { FounderWall } from './founder-wall';
|
|
import { FounderMembers } from './founder-members';
|
|
import { OverviewSkeleton } from './skeleton-state';
|
|
import { SafeErrorState } from './safe-error-state';
|
|
|
|
type ReviewArea = 'applications' | 'wall' | 'members';
|
|
|
|
export function FounderModerationPage({ clubId }: { clubId: string }) {
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
const demo = process.env.NODE_ENV === 'development' && searchParams?.get('preview') === 'demo';
|
|
const [context, setContext] = useState<FounderClubContext | null>(null);
|
|
const [area, setArea] = useState<ReviewArea>('applications');
|
|
const [loading, setLoading] = useState(true);
|
|
const [access, setAccess] = useState<'sign-in' | 'restricted' | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [retry, setRetry] = useState(0);
|
|
const [pendingCount, setPendingCount] = useState(0);
|
|
|
|
useEffect(() => {
|
|
let generation = 0;
|
|
let disposed = false;
|
|
const unsubscribe = onAuthStateChanged(firebaseAuth, async (user) => {
|
|
const current = ++generation;
|
|
setContext(null); setPendingCount(0); setLoading(true); setAccess(null); setError(null);
|
|
if (!demo && (!user || user.isAnonymous)) {
|
|
setAccess('sign-in'); setLoading(false); return;
|
|
}
|
|
try {
|
|
const resolved = await fetchFounderClubContext(clubId, user?.uid, { allowDemoPreview: demo });
|
|
if (disposed || current !== generation) return;
|
|
if (!resolved.permissions.canManageSettings && !resolved.isDemoFixture) setAccess('restricted');
|
|
else setContext(resolved);
|
|
} catch (err) {
|
|
if (!disposed && current === generation) setError(sanitizePortalError(err, 'Club moderation could not be loaded.'));
|
|
} finally {
|
|
if (!disposed && current === generation) setLoading(false);
|
|
}
|
|
});
|
|
return () => { disposed = true; generation++; unsubscribe(); };
|
|
}, [clubId, demo, retry]);
|
|
|
|
if (loading) return <div className="min-h-screen bg-brand-bg p-6"><OverviewSkeleton /></div>;
|
|
if (access) return <SafeErrorState title={access === 'sign-in' ? 'Authentication Required' : 'Management Access Restricted'}
|
|
message={access === 'sign-in' ? 'Sign in to review club content.' : 'Only club Owners and Admins can moderate this club.'}
|
|
actionLabel={access === 'sign-in' ? 'Sign In' : 'Return to Clubs'}
|
|
onAction={() => router.push(access === 'sign-in' ? '/login' : '/clubs')} />;
|
|
if (error || !context) return <SafeErrorState title="Club moderation unavailable"
|
|
message={error || undefined} onRetry={() => setRetry((value) => value + 1)}
|
|
actionLabel="Return to Clubs" onAction={() => router.push('/clubs')} />;
|
|
|
|
return <FounderPortalShell club={context.club} permissions={context.permissions}
|
|
activeNav="moderation" pendingApplicationsCount={pendingCount}>
|
|
<div className="space-y-6">
|
|
<div>
|
|
<p className="font-mono text-xs uppercase tracking-wider text-brand-cyan">Club management</p>
|
|
<h1 className="font-headline text-3xl font-bold text-on-surface">Moderation</h1>
|
|
<p className="mt-2 text-sm text-on-surface-variant">Review membership requests, Wall posts, and member access.</p>
|
|
</div>
|
|
<p className="rounded-xl border border-brand-border bg-surface-container-low p-4 text-sm text-on-surface-variant">
|
|
Post reports are not collected yet. Choose Wall posts to review recent content.
|
|
</p>
|
|
<div role="tablist" aria-label="Moderation areas" className="flex flex-wrap gap-2">
|
|
{([
|
|
['applications', 'Applications'],
|
|
['wall', 'Wall posts'],
|
|
['members', 'Members'],
|
|
] as const).map(([id, label]) => <button key={id} type="button" role="tab"
|
|
id={`moderation-${id}`} aria-controls="moderation-panel" aria-selected={area === id}
|
|
onClick={() => setArea(id)}
|
|
className={`rounded-xl border px-4 py-2 text-sm font-semibold ${area === id
|
|
? 'border-brand-cyan bg-brand-cyan/15 text-brand-cyan'
|
|
: 'border-brand-border bg-surface-container text-on-surface-variant hover:text-on-surface'}`}>
|
|
{label}
|
|
</button>)}
|
|
</div>
|
|
<div id="moderation-panel" role="tabpanel" aria-labelledby={`moderation-${area}`}>
|
|
{area === 'applications' && <FounderApplications club={context.club} permissions={context.permissions}
|
|
callerUid={context.currentUser.uid} isDemoFixture={context.isDemoFixture}
|
|
onApplicationsCountChange={setPendingCount} />}
|
|
{area === 'wall' && <FounderWall context={context} />}
|
|
{area === 'members' && <FounderMembers context={context} />}
|
|
</div>
|
|
</div>
|
|
</FounderPortalShell>;
|
|
}
|