245 lines
9.6 KiB
TypeScript
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>
|
|
);
|
|
}
|