178 lines
6.9 KiB
TypeScript
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>
|
|
);
|
|
}
|