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

256 lines
11 KiB
TypeScript

'use client';
import React, { useState, useEffect } from 'react';
interface JoinRideModalProps {
isOpen: boolean;
onClose: () => void;
onJoinSuccess: (code: string, rideTitle: string) => void;
}
export function JoinRideModal({ isOpen, onClose, onJoinSuccess }: JoinRideModalProps) {
const [code, setCode] = useState('');
const [isCameraActive, setIsCameraActive] = useState(false);
const [errorMsg, setErrorMsg] = useState<string | null>(null);
const [successInfo, setSuccessInfo] = useState<{ title: string; lead: string } | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
// Reset state when modal is opened/closed
useEffect(() => {
if (isOpen) {
setCode('');
setErrorMsg(null);
setSuccessInfo(null);
setIsCameraActive(false);
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 = (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);
// Simulated lookup
setTimeout(() => {
setIsSubmitting(false);
let resolvedTitle = 'Squad Formation Ride';
let resolvedLead = 'ApexLead';
if (cleanCode.includes('APEX') || cleanCode === 'CA-84' || cleanCode === 'APEX84') {
resolvedTitle = 'Bay Area Dawn Patrol';
resolvedLead = 'ApexLead';
} else if (cleanCode.includes('MALIBU') || cleanCode === 'MALIBU7') {
resolvedTitle = 'Malibu Canyons Fast Pace';
resolvedLead = 'Dan_KTM';
} else if (cleanCode.includes('DRAGON') || cleanCode === 'DRAGON318') {
resolvedTitle = 'Tail of the Dragon Run';
resolvedLead = 'Sweep_Marshal';
} else {
resolvedTitle = `Squad Run #${cleanCode}`;
resolvedLead = 'Pack Marshal';
}
setSuccessInfo({ title: resolvedTitle, lead: resolvedLead });
onJoinSuccess(cleanCode, resolvedTitle);
}, 400);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
processCode(code);
};
const handleSimulateScan = () => {
setCode('APEX84');
processCode('APEX84');
};
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">
Telemetry Linked
</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">
Lead Marshal: <span className="text-on-surface font-bold">{successInfo.lead}</span>.
Your telemetry stream is now synchronized with this pack.
</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"
>
Open Pack Radar
</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 encrypted 6-character ride code generated by your Lead Pace Setter or scan their cockpit radar QR display.
</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>
{/* Divider */}
<div className="flex items-center gap-space-md text-on-surface-variant font-code-mono text-code-mono justify-center text-xs">
<span className="h-px bg-outline-variant flex-1"></span>
<span>Or Scan Optical QR</span>
<span className="h-px bg-outline-variant flex-1"></span>
</div>
{/* Optical Lens / Camera Simulation */}
{isCameraActive ? (
<div
className="relative bg-black rounded-xl h-44 overflow-hidden border border-primary-container flex flex-col items-center justify-center p-3"
data-testid="radar-lens-viewfinder"
>
{/* Viewfinder crosshairs */}
<div className="absolute inset-4 border border-secondary-fixed/40 rounded pointer-events-none">
<div className="absolute top-0 left-0 w-3 h-3 border-t-2 border-l-2 border-secondary-fixed"></div>
<div className="absolute top-0 right-0 w-3 h-3 border-t-2 border-r-2 border-secondary-fixed"></div>
<div className="absolute bottom-0 left-0 w-3 h-3 border-b-2 border-l-2 border-secondary-fixed"></div>
<div className="absolute bottom-0 right-0 w-3 h-3 border-b-2 border-r-2 border-secondary-fixed"></div>
{/* Animated scanline */}
<div className="w-full h-0.5 bg-gradient-to-r from-transparent via-primary-container to-transparent animate-pulse mt-12"></div>
</div>
<span className="font-code-mono text-xs text-secondary-fixed mb-2 relative z-10">
Radar Optical Lens Active
</span>
<button
type="button"
onClick={handleSimulateScan}
className="relative z-10 px-3 py-1.5 rounded-lg bg-primary-container text-on-primary-container font-code-mono text-xs hover:bg-primary-fixed transition-colors"
>
Simulate QR Scan (APEX84)
</button>
<button
type="button"
onClick={() => setIsCameraActive(false)}
className="mt-2 text-on-surface-variant hover:text-on-surface text-xs font-code-mono underline"
>
Switch back to manual entry
</button>
</div>
) : (
<div
onClick={() => setIsCameraActive(true)}
className="bg-surface-container hover:bg-surface-container-high border border-outline-variant/30 h-32 rounded-xl flex flex-col items-center justify-center gap-2 text-on-surface-variant hover:text-primary transition-all cursor-pointer group"
data-testid="activate-radar-lens-button"
>
<span className="material-symbols-outlined text-[32px] group-hover:scale-110 transition-transform text-primary-fixed-dim">
photo_camera
</span>
<span className="font-code-mono text-code-mono">Activate Radar Optical Lens</span>
</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>
);
}