MotorSignal-App/website/components/group-rides/create-ride-modal.tsx

362 lines
16 KiB
TypeScript

'use client';
import React, { useState, useEffect } from 'react';
import { GroupRideSummary } from './types';
interface CreateRideModalProps {
isOpen: boolean;
onClose: () => void;
onCreateRide: (ride: GroupRideSummary) => void;
}
export function CreateRideModal({ isOpen, onClose, onCreateRide }: CreateRideModalProps) {
const [title, setTitle] = useState('');
const [region, setRegion] = useState('');
const [scheduledDate, setScheduledDate] = useState('Saturday');
const [scheduledTime, setScheduledTime] = useState('07:00 AM PST');
const [paceClassification, setPaceClassification] = useState<GroupRideSummary['paceClassification']>('Brisk Pace');
const [discipline, setDiscipline] = useState<NonNullable<GroupRideSummary['discipline']>>('Sport / Canyon');
const [joinPolicy, setJoinPolicy] = useState<GroupRideSummary['joinPolicy']>('requestToJoin');
const [formationProfile, setFormationProfile] = useState('Staggered Dual Column [B]');
const [distance, setDistance] = useState('50.0 Miles');
const [meetingPoint, setMeetingPoint] = useState('');
const [routeSnippet, setRouteSnippet] = useState('Skyline Blvd > Stage Rd > PCH Pacific Coast Hwy');
const [participantLimit, setParticipantLimit] = useState(12);
const [description, setDescription] = useState('');
const [errorMsg, setErrorMsg] = useState<string | null>(null);
// Reset form when modal opens
useEffect(() => {
if (isOpen) {
setTitle('');
setRegion('');
setScheduledDate('Saturday');
setScheduledTime('07:00 AM PST');
setPaceClassification('Brisk Pace');
setDiscipline('Sport / Canyon');
setJoinPolicy('requestToJoin');
setFormationProfile('Staggered Dual Column [B]');
setDistance('50.0 Miles');
setMeetingPoint('');
setRouteSnippet('Skyline Blvd > Stage Rd > PCH Pacific Coast Hwy');
setParticipantLimit(12);
setDescription('');
setErrorMsg(null);
}
}, [isOpen]);
// Handle ESC key press
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isOpen) {
onClose();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose]);
if (!isOpen) return null;
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!title.trim()) {
setErrorMsg('Please enter a ride title.');
return;
}
if (!region.trim()) {
setErrorMsg('Please enter a location or region.');
return;
}
const newRide: GroupRideSummary = {
id: `ride-${Date.now()}`,
title: title.trim(),
description: description.trim() || 'Custom squadron pack run with MotorSignal live telemetry.',
region: region.trim(),
scheduledDate,
scheduledTime,
paceClassification,
discipline,
formationProfile,
joinPolicy,
leadHandle: 'You (Organizer)',
leadAvatarText: 'YOU',
participantCount: 1,
participantLimit: Number(participantLimit) || 12,
distance: distance.trim() || '45.0 Miles',
meetingPoint: meetingPoint.trim() || 'Designated Rally Staging Point',
routeSnippet: routeSnippet.trim() || 'Custom GPX route uploaded by organizer.',
userStatus: 'joined',
status: 'scheduled',
};
onCreateRide(newRide);
onClose();
};
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="create-ride-title"
className="fixed inset-0 z-50 flex items-center justify-center p-space-md bg-surface-container-lowest/80 backdrop-blur-md overflow-y-auto"
onClick={(e) => {
if (e.target === e.currentTarget) onClose();
}}
>
<div className="bg-surface-container-low max-w-lg w-full rounded-2xl p-space-xl shadow-2xl relative border border-outline-variant/30 flex flex-col my-8">
{/* Header */}
<div className="flex items-center justify-between mb-space-md">
<div className="flex items-center gap-space-xs">
<span className="material-symbols-outlined text-primary-container text-[24px]">
add_circle
</span>
<h3 id="create-ride-title" className="font-headline-md text-headline-md text-on-surface">
Create Group Ride Run
</h3>
</div>
<button
onClick={onClose}
className="text-on-surface-variant hover:text-on-surface p-1 rounded-lg hover:bg-surface-container transition-colors"
aria-label="Close modal"
>
<span className="material-symbols-outlined text-[20px]">close</span>
</button>
</div>
<p className="font-body-sm text-body-sm text-on-surface-variant mb-space-md">
Organize a new squadron run with real-time radar tracking, safety spacing buffers, and synchronized telemetry check-ins.
</p>
{errorMsg && (
<div className="mb-space-md p-space-sm rounded-lg bg-error-container text-on-error-container font-code-mono text-code-mono flex items-center gap-2">
<span className="material-symbols-outlined text-[18px]">error</span>
<span>{errorMsg}</span>
</div>
)}
<form onSubmit={handleSubmit} className="flex flex-col gap-space-md">
{/* Title */}
<div>
<label htmlFor="ride-title-input" className="block font-label-caps text-label-caps text-on-surface-variant mb-1">
Ride Title *
</label>
<input
id="ride-title-input"
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="e.g. Sierra Foothills Canyon Run"
className="w-full bg-surface-container-lowest font-body-md text-body-md text-on-surface px-space-md py-2.5 rounded-xl border border-outline-variant/30 focus:border-primary-container outline-none"
/>
</div>
{/* Region & Distance Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-space-md">
<div>
<label htmlFor="ride-region-input" className="block font-label-caps text-label-caps text-on-surface-variant mb-1">
Region / City *
</label>
<input
id="ride-region-input"
type="text"
value={region}
onChange={(e) => setRegion(e.target.value)}
placeholder="e.g. Sacramento, CA"
className="w-full bg-surface-container-lowest font-body-md text-body-md text-on-surface px-space-md py-2.5 rounded-xl border border-outline-variant/30 focus:border-primary-container outline-none"
/>
</div>
<div>
<label htmlFor="ride-distance-input" className="block font-label-caps text-label-caps text-on-surface-variant mb-1">
Route Distance
</label>
<input
id="ride-distance-input"
type="text"
value={distance}
onChange={(e) => setDistance(e.target.value)}
placeholder="e.g. 58.4 Miles"
className="w-full bg-surface-container-lowest font-body-md text-body-md text-on-surface px-space-md py-2.5 rounded-xl border border-outline-variant/30 focus:border-primary-container outline-none"
/>
</div>
</div>
{/* Date & Time Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-space-md">
<div>
<label htmlFor="ride-date-input" className="block font-label-caps text-label-caps text-on-surface-variant mb-1">
Scheduled Day
</label>
<input
id="ride-date-input"
type="text"
value={scheduledDate}
onChange={(e) => setScheduledDate(e.target.value)}
placeholder="e.g. Saturday"
className="w-full bg-surface-container-lowest font-body-md text-body-md text-on-surface px-space-md py-2.5 rounded-xl border border-outline-variant/30 focus:border-primary-container outline-none"
/>
</div>
<div>
<label htmlFor="ride-time-input" className="block font-label-caps text-label-caps text-on-surface-variant mb-1">
Start Time
</label>
<input
id="ride-time-input"
type="text"
value={scheduledTime}
onChange={(e) => setScheduledTime(e.target.value)}
placeholder="e.g. 06:45 AM PST"
className="w-full bg-surface-container-lowest font-body-md text-body-md text-on-surface px-space-md py-2.5 rounded-xl border border-outline-variant/30 focus:border-primary-container outline-none"
/>
</div>
</div>
{/* Pace & Formation */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-space-md">
<div>
<label htmlFor="ride-pace-select" className="block font-label-caps text-label-caps text-on-surface-variant mb-1">
Pace Classification
</label>
<select
id="ride-pace-select"
value={paceClassification}
onChange={(e) => setPaceClassification(e.target.value as any)}
className="w-full bg-surface-container-lowest font-body-md text-body-md text-on-surface px-space-md py-2.5 rounded-xl border border-outline-variant/30 focus:border-primary-container outline-none"
>
<option value="Track Pace">Track Pace</option>
<option value="Brisk Pace">Brisk Pace</option>
<option value="Endurance / Chill">Endurance / Chill</option>
<option value="Weekend Tours">Weekend Tours</option>
</select>
</div>
<div>
<label htmlFor="ride-limit-input" className="block font-label-caps text-label-caps text-on-surface-variant mb-1">
Rider Limit
</label>
<input
id="ride-limit-input"
type="number"
min={2}
max={50}
value={participantLimit}
onChange={(e) => setParticipantLimit(Number(e.target.value))}
className="w-full bg-surface-container-lowest font-body-md text-body-md text-on-surface px-space-md py-2.5 rounded-xl border border-outline-variant/30 focus:border-primary-container outline-none"
/>
</div>
</div>
{/* Discipline & Privacy Controls */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-space-md">
<div>
<label htmlFor="ride-discipline-select" className="block font-label-caps text-label-caps text-on-surface-variant mb-1">
Discipline / Style
</label>
<select
id="ride-discipline-select"
value={discipline}
onChange={(e) => setDiscipline(e.target.value as any)}
className="w-full bg-surface-container-lowest font-body-md text-body-md text-on-surface px-space-md py-2.5 rounded-xl border border-outline-variant/30 focus:border-primary-container outline-none"
>
<option value="Sport / Canyon">Sport / Canyon</option>
<option value="Touring / ADV">Touring / ADV</option>
<option value="Track / Circuit">Track / Circuit</option>
<option value="Cruiser / Pack">Cruiser / Pack</option>
</select>
</div>
<div>
<label htmlFor="ride-privacy-select" className="block font-label-caps text-label-caps text-on-surface-variant mb-1">
Pack Privacy &amp; Access
</label>
<select
id="ride-privacy-select"
value={joinPolicy}
onChange={(e) => setJoinPolicy(e.target.value as any)}
className="w-full bg-surface-container-lowest font-body-md text-body-md text-on-surface px-space-md py-2.5 rounded-xl border border-outline-variant/30 focus:border-primary-container outline-none"
>
<option value="open">Public (Open to All Riders)</option>
<option value="requestToJoin">Moderated (Request Slot Required)</option>
<option value="inviteOnly">Private (Invite / Code Only)</option>
</select>
</div>
</div>
{/* Meeting Point */}
<div>
<label htmlFor="ride-meeting-input" className="block font-label-caps text-label-caps text-on-surface-variant mb-1">
Meeting Point / Staging Location
</label>
<input
id="ride-meeting-input"
type="text"
value={meetingPoint}
onChange={(e) => setMeetingPoint(e.target.value)}
placeholder="e.g. Chevron Staging Lot at Highway 49"
className="w-full bg-surface-container-lowest font-body-md text-body-md text-on-surface px-space-md py-2.5 rounded-xl border border-outline-variant/30 focus:border-primary-container outline-none"
/>
</div>
{/* Route Upload or Waypoint Selection */}
<div>
<label htmlFor="ride-route-input" className="block font-label-caps text-label-caps text-on-surface-variant mb-1">
Route Upload or Waypoint Selection
</label>
<div className="flex gap-2">
<input
id="ride-route-input"
type="text"
value={routeSnippet}
onChange={(e) => setRouteSnippet(e.target.value)}
placeholder="e.g. Skyline Blvd > Stage Rd > Pescadero Creek"
className="flex-1 bg-surface-container-lowest font-body-md text-body-md text-on-surface px-space-md py-2.5 rounded-xl border border-outline-variant/30 focus:border-primary-container outline-none"
/>
<button
type="button"
onClick={() => setRouteSnippet('Custom GPX Track Loaded (34.2 mi • 82 curves)')}
className="px-3 py-2 rounded-xl bg-surface-container hover:bg-surface-container-high border border-outline-variant/30 text-xs font-mono text-primary-container transition flex items-center gap-1"
title="Simulate GPX Upload"
>
<span className="material-symbols-outlined text-[16px]">upload_file</span>
<span>GPX</span>
</button>
</div>
</div>
{/* Description */}
<div>
<label htmlFor="ride-desc-input" className="block font-label-caps text-label-caps text-on-surface-variant mb-1">
Description &amp; Safety Briefing
</label>
<textarea
id="ride-desc-input"
rows={3}
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Formation rules, fuel stop locations, radio channels..."
className="w-full bg-surface-container-lowest font-body-md text-body-md text-on-surface px-space-md py-2 rounded-xl border border-outline-variant/30 focus:border-primary-container outline-none resize-none"
/>
</div>
{/* Buttons */}
<div className="flex items-center justify-end gap-space-sm pt-space-sm">
<button
type="button"
onClick={onClose}
className="px-space-lg py-2.5 rounded-xl bg-surface-container hover:bg-surface-container-high text-on-surface font-headline-sm text-headline-sm transition-colors"
>
Cancel
</button>
<button
type="submit"
className="px-space-xl py-2.5 rounded-xl bg-primary-container text-on-primary-container font-headline-sm text-headline-sm hover:shadow-lg transition-all"
>
Publish Group Ride
</button>
</div>
</form>
</div>
</div>
);
}