373 lines
16 KiB
TypeScript
373 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 | Promise<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);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
|
|
// 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);
|
|
setIsSubmitting(false);
|
|
}
|
|
}, [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 = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
// Prevent duplicate submissions while a create is in flight.
|
|
if (isSubmitting) return;
|
|
|
|
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',
|
|
};
|
|
|
|
setIsSubmitting(true);
|
|
try {
|
|
await onCreateRide(newRide);
|
|
onClose();
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
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 & 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 & 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"
|
|
disabled={isSubmitting}
|
|
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 disabled:opacity-60 disabled:cursor-not-allowed"
|
|
>
|
|
{isSubmitting ? 'Publishing…' : 'Publish Group Ride'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|