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

178 lines
6.9 KiB
TypeScript

'use client';
import React, { useState, useEffect } from 'react';
interface JoinRideModalProps {
isOpen: boolean;
onClose: () => void;
onJoinSuccess: (code: string) => Promise<{ title: string; lead: string; status: 'joined' | 'requested' }>;
}
export function JoinRideModal({ isOpen, onClose, onJoinSuccess }: JoinRideModalProps) {
const [code, setCode] = useState('');
const [errorMsg, setErrorMsg] = useState<string | null>(null);
const [successInfo, setSuccessInfo] = useState<{ title: string; lead: string; status: 'joined' | 'requested' } | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
// Reset state when modal is opened/closed
useEffect(() => {
if (isOpen) {
setCode('');
setErrorMsg(null);
setSuccessInfo(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 handleCodeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const raw = e.target.value.toUpperCase().replace(/[^A-Z0-9_-]/g, '');
setCode(raw);
if (errorMsg) setErrorMsg(null);
};
const processCode = async (rideCode: string) => {
const cleanCode = rideCode.trim().toUpperCase();
if (!cleanCode || cleanCode.length < 4) {
setErrorMsg('Please enter a valid ride code (at least 4 characters).');
return;
}
setIsSubmitting(true);
setErrorMsg(null);
try {
setSuccessInfo(await onJoinSuccess(cleanCode));
} catch (error) {
setErrorMsg(error instanceof Error ? error.message : 'Could not join this ride.');
} finally {
setIsSubmitting(false);
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
void processCode(code);
};
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="join-pack-title"
className="fixed inset-0 z-50 flex items-center justify-center p-space-md bg-surface-container-lowest/80 backdrop-blur-md"
onClick={(e) => {
if (e.target === e.currentTarget) onClose();
}}
>
<div className="bg-surface-container-low max-w-md w-full rounded-2xl p-space-xl shadow-2xl relative border border-outline-variant/30 flex flex-col">
{/* 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]">
qr_code_scanner
</span>
<h3 id="join-pack-title" className="font-headline-md text-headline-md text-on-surface">
Join Pack Formation
</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>
{/* Success State */}
{successInfo ? (
<div className="flex flex-col items-center text-center py-4">
<div className="w-14 h-14 rounded-full bg-secondary-fixed/20 text-secondary-fixed flex items-center justify-center mb-3">
<span className="material-symbols-outlined text-[32px]">check_circle</span>
</div>
<span className="font-label-caps text-label-caps text-secondary-fixed mb-1">
{successInfo.status === 'joined' ? 'Ride Joined' : 'Request Sent'}
</span>
<h4 className="font-headline-md text-headline-md text-on-surface mb-1">
{successInfo.title}
</h4>
<p className="font-body-sm text-body-sm text-on-surface-variant mb-6">
Ride lead: <span className="text-on-surface font-bold">{successInfo.lead}</span>.
{successInfo.status === 'joined' ? ' You are on the rider list.' : ' The organizer must approve your request.'}
</p>
<button
onClick={onClose}
className="w-full py-3 rounded-xl bg-primary-container text-on-primary-container font-headline-sm text-headline-sm hover:shadow-lg transition-all"
>
Done
</button>
</div>
) : (
/* Input / Scanner Flow */
<form onSubmit={handleSubmit} className="flex flex-col">
<p className="font-body-sm text-body-sm text-on-surface-variant mb-space-md">
Enter the ride code to find a public group ride and join or request a slot.
</p>
{/* Error Message */}
{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>
)}
{/* Code Input */}
<div className="flex flex-col gap-space-md mb-space-lg">
<div className="relative">
<input
aria-label="Ride Code"
type="text"
maxLength={10}
value={code}
onChange={handleCodeChange}
placeholder="e.g. APEX84"
className="w-full bg-surface-container-lowest font-code-mono text-headline-md text-center tracking-widest text-primary px-space-md py-3 rounded-xl outline-none border border-outline-variant/30 focus:border-primary-container focus:ring-1 focus:ring-primary-container placeholder:text-on-surface-variant/40"
autoFocus
/>
</div>
</div>
{/* Submit Button */}
<button
type="submit"
disabled={isSubmitting}
className="w-full py-3.5 rounded-xl bg-gradient-to-r from-primary-container to-primary-fixed text-on-primary-container font-headline-sm text-headline-sm hover:shadow-[0_0_20px_rgba(0,242,254,0.3)] transition-all flex items-center justify-center gap-2 disabled:opacity-50"
>
{isSubmitting ? (
<>
<span className="material-symbols-outlined text-[20px] animate-spin">refresh</span>
Linking Telemetry...
</>
) : (
<>
<span className="material-symbols-outlined text-[20px]">sync_alt</span>
Connect Squad Telemetry
</>
)}
</button>
</form>
)}
</div>
</div>
);
}