MotorSignal-App/website/app/clubs/[clubId]/manage/rides/page.tsx

249 lines
9.7 KiB
TypeScript

'use client';
import React, { useEffect, useState, use } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import {
fetchFounderClubContext,
FounderClubContext,
fetchClubRides,
fetchClubApplications,
sanitizePortalError,
} from '../../../../../lib/club-data';
import { ClubUpcomingRide } from '../../../../../components/founder-portal/types';
import { FounderPortalShell } from '../../../../../components/founder-portal/founder-portal-shell';
import { FounderRides } from '../../../../../components/founder-portal/founder-rides';
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 ClubFounderRidesPage({ params }: PageProps) {
const { clubId } = use(params);
const router = useRouter();
const searchParams = useSearchParams();
const [context, setContext] = useState<FounderClubContext | null>(null);
const [upcomingRides, setUpcomingRides] = useState<ClubUpcomingRide[]>([]);
const [pastRides, setPastRides] = useState<ClubUpcomingRide[]>([]);
const [pendingApplicationsCount, setPendingApplicationsCount] = useState(0);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [unauthenticated, setUnauthenticated] = useState(false);
const [unauthorized, setUnauthorized] = useState(false);
const isDemoPreview = searchParams?.get('preview') === 'demo';
async function loadData() {
setLoading(true);
setError(null);
setUnauthenticated(false);
setUnauthorized(false);
try {
await firebaseAuth.authStateReady();
const currentUser = firebaseAuth.currentUser;
// 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 Rides management; Members cannot
if (!isDemoPreview && (!ctx.permissions.canManageSettings && !ctx.permissions.canPlanRides)) {
setUnauthorized(true);
setLoading(false);
return;
}
setContext(ctx);
// Fetch pending applications count for sidebar badge
try {
const apps = await fetchClubApplications(ctx.club.id, currentUser?.uid, {
allowDemoPreview: isDemoPreview,
});
setPendingApplicationsCount(apps.length);
} catch {
setPendingApplicationsCount(0);
}
// In demo preview mode: load fixture rides for UI/design validation
if (isDemoPreview) {
const ridesResult = await fetchClubRides(ctx.club.id, currentUser?.uid, {
allowDemoPreview: true,
});
setUpcomingRides(ridesResult.upcomingRides);
setPastRides(ridesResult.pastRides);
}
// In production mode, do not attempt a broad client-side clubId query.
// GroupRide.clubId is now persisted, but Firestore intentionally does not authorize
// Club-wide discovery until mutual-block privacy can be preserved.
} catch (err) {
setError(sanitizePortalError(err, "Club rides 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-surface flex items-center justify-center p-6">
<SafeErrorState
title="Authentication Required"
message="Sign in with your rider account to access club rides."
actionLabel="Sign In"
onAction={() => router.push('/login')}
/>
</div>
);
}
// Unauthorized (Member or non-admin) safe error state
if (unauthorized) {
return (
<div className="min-h-screen bg-surface flex items-center justify-center p-6">
<SafeErrorState
title="Management Access Restricted"
message="You must be an Owner or Admin of this Club to manage Club Rides."
actionLabel="Return to Clubs"
onAction={() => router.push('/clubs')}
/>
</div>
);
}
// Error or missing context
if (error || !context) {
return (
<div className="min-h-screen bg-surface flex items-center justify-center p-6">
<SafeErrorState
title="Rides Unavailable"
message={error ?? "Club rides couldn't be loaded."}
actionLabel="Return to Clubs"
onAction={() => router.push('/clubs')}
onRetry={loadData}
/>
</div>
);
}
// Normal production operation: truthful discovery-pending state.
// GroupRide.clubId exists, but no privacy-preserving Club-wide read path is available.
// Do not describe this state as "No rides".
if (!isDemoPreview) {
return (
<FounderPortalShell
club={context.club}
permissions={context.permissions}
activeNav="rides"
pendingApplicationsCount={pendingApplicationsCount}
upcomingRidesCount={0}
>
<div className="space-y-6" aria-label="Club Rides Management">
{/* Header Section */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<div className="flex items-center gap-2.5">
<h1 className="font-headline text-xl sm:text-2xl font-bold text-white tracking-tight">
Club Rides
</h1>
<span className="font-mono text-xs px-2.5 py-0.5 rounded-full bg-amber-400/10 text-amber-400 border border-amber-400/30 font-semibold">
DISCOVERY PENDING
</span>
</div>
<p className="font-body text-xs text-slate-400 mt-1">
Schedule, coordinate, and review group rides and road captain assignments.
</p>
</div>
<div
className="flex items-center gap-2 px-3.5 py-2 rounded-xl bg-surface-container/50 border border-brand-border/40 text-slate-400 font-body text-xs cursor-not-allowed select-none"
title="Founder Portal ride planning is not wired yet"
aria-disabled="true"
>
<span className="material-symbols-outlined text-[17px] text-slate-500">add_road</span>
<span className="text-slate-400">Plan Club Ride</span>
<span className="font-mono text-[9px] px-1.5 py-0.5 rounded bg-surface-container text-slate-400 border border-brand-border/60">
SOON
</span>
</div>
</div>
{/* Pending Backend Integration Notice */}
<div className="rounded-2xl glass-card border border-brand-border/60 p-6 sm:p-8 text-center space-y-4 max-w-2xl mx-auto my-8">
<div className="w-12 h-12 rounded-2xl bg-amber-400/10 border border-amber-400/30 flex items-center justify-center text-amber-400 mx-auto">
<span className="material-symbols-outlined text-[28px]">route</span>
</div>
<div className="space-y-2">
<h2 className="font-headline text-lg sm:text-xl font-bold text-white">
Club Rides Discovery Pending
</h2>
<p className="font-body text-xs sm:text-sm text-slate-400 max-w-lg mx-auto leading-relaxed">
Club-affiliated Group Rides can now be created in the mobile application. Club-wide Founder Portal discovery remains disabled until a read path can preserve mutual-block privacy.
</p>
</div>
<div className="pt-2 border-t border-brand-border/40 text-left text-xs text-slate-400 space-y-2 bg-surface-container-low/60 rounded-xl p-4">
<div className="flex items-start gap-2">
<span className="material-symbols-outlined text-[16px] text-brand-cyan shrink-0 mt-0.5">info</span>
<span>
<strong>Architecture Note:</strong> Group rides now link to clubs through <code className="font-mono text-[11px] text-brand-cyan">GroupRide.clubId == club.id</code>. Club-wide discovery is a separate deferred capability.
</span>
</div>
<div className="flex items-start gap-2">
<span className="material-symbols-outlined text-[16px] text-brand-cyan shrink-0 mt-0.5">shield</span>
<span>
<strong>Privacy Invariant:</strong> Live location is shared only with eligible ride participants while sharing is enabled. Club membership alone never grants access to rider location.
</span>
</div>
</div>
</div>
</div>
</FounderPortalShell>
);
}
// Explicit Development Demo Preview: Renders full interactive UI with fixtures
return (
<FounderPortalShell
club={context.club}
permissions={context.permissions}
activeNav="rides"
pendingApplicationsCount={pendingApplicationsCount}
upcomingRidesCount={upcomingRides.length}
>
<FounderRides
club={context.club}
permissions={context.permissions}
upcomingRides={upcomingRides}
pastRides={pastRides}
isLoading={false}
error={null}
/>
</FounderPortalShell>
);
}