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

223 lines
8.9 KiB
TypeScript

'use client';
import React from 'react';
import { ClubUpcomingRide, UserPortalPermissions } from './types';
import { EmptyState } from './empty-state';
interface UpcomingRidesSectionProps {
rides: ClubUpcomingRide[];
permissions: UserPortalPermissions;
onPlanRide?: () => void;
onManageRide?: (rideId: string) => void;
onViewRoster?: (rideId: string) => void;
onViewRoute?: (routeId?: string) => void;
}
export function ClubRideCard({
ride,
permissions,
onManageRide,
onViewRoster,
onViewRoute,
}: {
ride: ClubUpcomingRide;
permissions: UserPortalPermissions;
onManageRide?: (rideId: string) => void;
onViewRoster?: (rideId: string) => void;
onViewRoute?: (routeId?: string) => void;
}) {
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">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 */}
<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
onClick={() => onManageRide?.(ride.id)}
className="h-9 px-3.5 rounded-xl bg-primary-container text-on-primary-container font-body text-xs font-bold hover:brightness-110 shadow-sm transition-all cursor-pointer"
>
Manage Ride
</button>
) : (
<button
onClick={() => onViewRoster?.(ride.id)}
className="h-9 px-3.5 rounded-xl bg-surface-container border border-brand-border/60 text-slate-200 font-body text-xs font-medium hover:bg-surface-container-high transition-colors cursor-pointer"
>
View Ride
</button>
)}
<button
onClick={() => onViewRoster?.(ride.id)}
className="h-9 px-3.5 rounded-xl bg-surface-container border border-brand-border/60 text-slate-200 font-body text-xs font-medium hover:bg-surface-container-high transition-colors cursor-pointer"
>
View Roster ({ride.confirmedCount})
</button>
</div>
<button
onClick={() => onViewRoute?.(ride.routeId ?? undefined)}
className="h-9 px-3.5 rounded-xl bg-surface-container border border-brand-border/60 text-slate-300 hover:text-white font-body text-xs flex items-center gap-1.5 transition-colors cursor-pointer"
>
<span className="material-symbols-outlined text-[15px] text-brand-cyan">map</span>
<span>View Route</span>
</button>
</div>
</div>
);
}
export function UpcomingRidesSection({
rides,
permissions,
onPlanRide,
onManageRide,
onViewRoster,
onViewRoute,
}: UpcomingRidesSectionProps) {
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 && (
<button
onClick={() => onViewRoster?.('all')}
className="font-mono text-xs text-brand-cyan hover:underline flex items-center gap-1 cursor-pointer"
>
<span>Full Ride Schedule</span>
<span className="material-symbols-outlined text-[13px]">arrow_forward</span>
</button>
)}
</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={permissions.canPlanRides ? 'Plan Club Ride' : undefined}
onAction={permissions.canPlanRides ? onPlanRide : 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}
onManageRide={onManageRide}
onViewRoster={onViewRoster}
onViewRoute={onViewRoute}
/>
))}
</div>
)}
</section>
);
}