MotorSignal-App/website/components/founder-portal/club-ride-card.tsx

245 lines
9.6 KiB
TypeScript

'use client';
import React from 'react';
import Link from 'next/link';
import { useParams, useSearchParams } from 'next/navigation';
import { ClubUpcomingRide, UserPortalPermissions } from './types';
import { EmptyState } from './empty-state';
interface UpcomingRidesSectionProps {
rides: ClubUpcomingRide[];
permissions: UserPortalPermissions;
clubId?: string;
onPlanRide?: () => void;
onManageRide?: (rideId: string) => void;
onViewRoster?: (rideId: string) => void;
onViewRoute?: (routeId?: string) => void;
}
export function ClubRideCard({
ride,
permissions,
clubId,
onManageRide,
onViewRoster,
onViewRoute,
}: {
ride: ClubUpcomingRide;
permissions: UserPortalPermissions;
clubId?: string;
onManageRide?: (rideId: string) => void;
onViewRoster?: (rideId: string) => void;
onViewRoute?: (routeId?: string) => void;
}) {
const params = useParams();
const targetClubId = clubId || (typeof params?.clubId === 'string' ? params.clubId : '');
const capacityPercent = ride.capacity ? Math.min(100, Math.round((ride.confirmedCount / ride.capacity) * 100)) : 75;
return (
<div className="rounded-2xl glass-card border border-brand-border/60 p-5 sm:p-6 flex flex-col justify-between transition-all hover:border-brand-cyan/50 shadow-lg">
<div>
{/* Top Metadata Row */}
<div className="flex flex-wrap items-start justify-between gap-2 pb-2">
<div>
<div className="flex items-center gap-2 font-mono text-xs text-brand-cyan">
<span className="w-2 h-2 rounded-full bg-brand-cyan animate-pulse" />
<span>{ride.scheduledStartAt}</span>
<span className="text-slate-500">•</span>
<span className="text-slate-400">Ride ID: {ride.rideIdentifier}</span>
</div>
<h3 className="font-headline text-lg sm:text-xl font-bold text-white mt-1">
{ride.title}
</h3>
</div>
<span className="px-2.5 py-1 rounded-lg bg-surface-container-high text-slate-300 font-mono text-[11px] font-medium border border-brand-border/60">
{ride.paceClassification}
</span>
</div>
{/* Attributes Row */}
<div className="grid grid-cols-3 gap-2 py-2.5 bg-surface-container-low/80 rounded-xl px-3.5 my-3 border border-brand-border/60">
<div>
<div className="font-mono text-[10px] text-slate-400">Straight-line distance</div>
<div className="font-headline text-sm font-semibold text-slate-100 mt-0.5">
{ride.distanceMiles ? `${ride.distanceMiles} mi` : '--'}
</div>
</div>
<div>
<div className="font-mono text-[10px] text-slate-400">Duration</div>
<div className="font-headline text-sm font-semibold text-slate-100 mt-0.5">
{ride.durationString ?? '--'}
</div>
</div>
<div>
<div className="font-mono text-[10px] text-slate-400">Pace Classification</div>
<div className="font-headline text-sm font-semibold text-brand-cyan mt-0.5">
{ride.paceClassification}
</div>
</div>
</div>
{/* Roster & Road Captain Details */}
<div className="space-y-2">
<div className="flex items-center justify-between font-mono text-xs">
<span className="text-slate-400">Riders: {ride.confirmedCount} going</span>
{ride.pendingCount > 0 ? (
<span className="text-amber-400 font-medium">{ride.pendingCount} requests pending</span>
) : ride.capacity ? (
<span className="text-brand-cyan font-medium">
{Math.max(0, ride.capacity - ride.confirmedCount)} open spots
</span>
) : null}
</div>
<div className="w-full h-1.5 rounded bg-surface-container-high overflow-hidden" aria-hidden="true">
<div
className={`h-full rounded transition-all duration-300 ${
ride.pendingCount > 0 ? 'bg-amber-400' : 'bg-brand-cyan'
}`}
style={{ width: `${capacityPercent}%` }}
/>
</div>
<div className="flex flex-wrap items-center justify-between pt-1 font-body text-xs text-slate-300">
<div className="flex items-center gap-1.5">
<span className="text-slate-400">Road Captain:</span>
<span className="text-slate-100 font-medium">{ride.roadCaptainName}</span>
{ride.roadCaptainMotorcycle && (
<span className="font-mono text-[11px] text-slate-400">
({ride.roadCaptainMotorcycle})
</span>
)}
</div>
{ride.sweepStatus === 'needs_assignment' ? (
<div className="font-mono text-xs text-error font-medium">Sweep: Needs Assignment</div>
) : ride.briefingTime ? (
<div className="font-mono text-xs text-brand-cyan">Briefing: {ride.briefingTime}</div>
) : null}
</div>
</div>
{/* Section 15: Verbatim Live Location Privacy Policy Notice */}
<div className="mt-3.5 p-2.5 rounded-xl bg-surface-container/60 border border-brand-border/60 text-slate-400 font-body text-[11px] flex items-start gap-2">
<span className="material-symbols-outlined text-[15px] text-brand-cyan shrink-0 mt-0.5">
info
</span>
<span>
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>
{/* Actions: Disabled until backend club-ride management ships */}
<div className="mt-4 pt-3 border-t border-brand-border/60 flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
{permissions.canPlanRides ? (
<button
type="button"
disabled
title="Ride management is planned for a future release"
className="h-9 px-3.5 rounded-xl bg-surface-container/50 border border-brand-border/40 text-slate-500 font-body text-xs font-medium cursor-not-allowed flex items-center justify-center"
>
Manage Ride
</button>
) : (
<button
type="button"
disabled
title="Ride details are planned for a future release"
className="h-9 px-3.5 rounded-xl bg-surface-container/50 border border-brand-border/40 text-slate-500 font-body text-xs font-medium cursor-not-allowed flex items-center justify-center"
>
View Ride
</button>
)}
<button
type="button"
disabled
title="Member roster view is planned for a future release"
className="h-9 px-3.5 rounded-xl bg-surface-container/50 border border-brand-border/40 text-slate-500 font-body text-xs font-medium cursor-not-allowed flex items-center justify-center"
>
View Roster ({ride.confirmedCount})
</button>
</div>
<button
type="button"
disabled
title="Route navigation is planned for a future release"
className="h-9 px-3.5 rounded-xl bg-surface-container/50 border border-brand-border/40 text-slate-500 font-body text-xs flex items-center gap-1.5 cursor-not-allowed select-none"
>
<span className="material-symbols-outlined text-[15px] text-slate-600">map</span>
<span>View Route</span>
</button>
</div>
</div>
);
}
export function UpcomingRidesSection({
rides,
permissions,
clubId,
onPlanRide,
onManageRide,
onViewRoster,
onViewRoute,
}: UpcomingRidesSectionProps) {
const params = useParams();
const searchParams = useSearchParams();
const isDemo = searchParams?.get('preview') === 'demo';
const queryParam = isDemo ? '?preview=demo' : '';
const targetClubId = clubId || (typeof params?.clubId === 'string' ? params.clubId : '');
return (
<section className="space-y-4" aria-label="Upcoming Club Rides">
<div className="flex items-center justify-between">
<div>
<h2 className="font-headline text-base font-semibold text-on-surface tracking-tight">
Upcoming Club Rides
</h2>
<p className="font-body text-xs text-outline">
Scheduled Club Rides, routes, participants, and ride assignments.
</p>
</div>
{rides.length > 0 && (
<Link
href={`/clubs/${targetClubId}/manage/rides${queryParam}`}
className="font-mono text-xs text-brand-cyan hover:text-white flex items-center gap-1 transition-colors"
>
<span>Full Ride Schedule</span>
<span className="material-symbols-outlined text-[13px]">arrow_forward</span>
</Link>
)}
</div>
{rides.length === 0 ? (
<EmptyState
icon="route"
title="No Club Rides are currently scheduled."
description="Plan a group route, designate pacelines, and invite club riders."
actionLabel={undefined}
className="py-10"
/>
) : (
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
{rides.map((ride) => (
<ClubRideCard
key={ride.id}
ride={ride}
permissions={permissions}
clubId={targetClubId}
onManageRide={onManageRide}
onViewRoster={onViewRoster}
onViewRoute={onViewRoute}
/>
))}
</div>
)}
</section>
);
}