feat(ui): Integrate AuraUI Visitor Check-In
Some checks are pending
Deploy to Production / deploy (push) Waiting to run
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run

- Implemented AuraUI 'Sign Up 1' as new Visitor Check-In flow
- Integrated high-tech split-screen layout with 'Secure Facility' branding
- Moved camera and form logic to self-contained VisitorCheckIn component
- Updated VisitorKioskPage to route to new experience
This commit is contained in:
fullsizemalt 2025-12-19 17:37:43 -08:00
parent d44238417b
commit 2a607b23b5
2 changed files with 466 additions and 287 deletions

View file

@ -0,0 +1,450 @@
import { useRef, useState } from "react";
import { Link } from "react-router-dom";
import {
CheckCircle,
User,
Mail,
FileText,
Camera,
ArrowRight,
ArrowLeft,
Building,
Phone,
Shield,
Trash2,
Loader2,
AlertTriangle
} from "lucide-react";
import { visitorsApi } from "@/lib/visitorsApi";
const commonStyles = {
inputWrapper:
"relative group flex items-center w-full rounded-full overflow-hidden shadow-sm transition-all duration-300",
input:
"w-full py-3.5 pl-12 pr-4 text-sm text-slate-900 placeholder-transparent bg-white/70 backdrop-blur-md border border-slate-200 rounded-full focus:outline-none focus:border-cyan-500 focus:ring-2 focus:ring-cyan-500 peer transition-all duration-300 disabled:opacity-50",
inputIcon:
"absolute left-4 text-slate-500 peer-focus:text-cyan-500 transition-colors w-5 h-5",
label:
"absolute left-12 text-slate-500 text-sm transition-all duration-200 peer-placeholder-shown:top-3.5 peer-placeholder-shown:text-slate-400 peer-placeholder-shown:text-base peer-focus:top-1.5 peer-focus:text-xs peer-focus:text-cyan-600",
button:
"w-full py-3.5 px-6 font-semibold rounded-full bg-gradient-to-r from-cyan-600 to-blue-600 text-white shadow-lg hover:opacity-90 hover:shadow-cyan-500/30 transition-all duration-300 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed",
link: "font-medium text-cyan-600 hover:text-cyan-700 transition-colors hover:underline",
};
interface VisitorCheckInProps {
onBack: () => void;
onSuccess: (data: { badgeNumber: string; visitId: string; message: string }) => void;
}
const VisitorCheckIn = ({ onBack, onSuccess }: VisitorCheckInProps) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [formData, setFormData] = useState({
name: '',
company: '',
email: '',
phone: '',
purpose: '',
type: 'VISITOR' as const,
ndaAccepted: false
});
// Camera State
const [capturedPhoto, setCapturedPhoto] = useState<string | null>(null);
const [showCamera, setShowCamera] = useState(false);
const videoRef = useRef<HTMLVideoElement>(null);
const streamRef = useRef<MediaStream | null>(null);
const startCamera = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'user', width: 640, height: 480 }
});
streamRef.current = stream;
if (videoRef.current) {
videoRef.current.srcObject = stream;
}
setShowCamera(true);
setError(null);
} catch (err) {
console.error('Camera access denied:', err);
setError('Camera access denied. Please allow camera permissions.');
}
};
const capturePhoto = () => {
if (videoRef.current) {
const canvas = document.createElement('canvas');
canvas.width = videoRef.current.videoWidth;
canvas.height = videoRef.current.videoHeight;
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.drawImage(videoRef.current, 0, 0);
const photoDataUrl = canvas.toDataURL('image/jpeg', 0.8);
setCapturedPhoto(photoDataUrl);
stopCamera();
}
}
};
const stopCamera = () => {
if (streamRef.current) {
streamRef.current.getTracks().forEach(track => track.stop());
streamRef.current = null;
}
setShowCamera(false);
};
const retakePhoto = () => {
setCapturedPhoto(null);
startCamera();
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setLoading(true);
try {
// Validate required fields
if (!formData.name || !formData.purpose) {
throw new Error("Name and Purpose are required.");
}
if (!formData.ndaAccepted) {
throw new Error("You must accept the NDA to check in.");
}
// 1. Create Visitor
const visitorData: any = {
name: formData.name,
purpose: formData.purpose,
type: formData.type,
company: formData.company || undefined,
email: formData.email || undefined,
phone: formData.phone || undefined,
photoUrl: capturedPhoto || undefined,
ndaAccepted: formData.ndaAccepted
};
// Remove empty strings
Object.keys(visitorData).forEach(key =>
(visitorData[key] === undefined || visitorData[key] === '') && delete visitorData[key]
);
const visitor = await visitorsApi.create(visitorData);
// 2. Check In
const result = await visitorsApi.checkIn(visitor.id, {
ndaAccepted: formData.ndaAccepted
});
onSuccess({
badgeNumber: result.badgeNumber,
visitId: result.visitId,
message: `Welcome, ${visitor.name}!`
});
} catch (err: any) {
console.error(err);
setError(err.response?.data?.error || err.message || 'Failed to check in');
} finally {
setLoading(false);
}
};
return (
<section className="relative bg-white min-h-screen">
<div className="grid grid-cols-1 lg:grid-cols-2 min-h-screen">
{/* Left Hero Section - Fixed/Sticky on Desktop */}
<div className="relative flex items-end px-6 pb-16 pt-20 lg:pt-60 lg:pb-24 bg-slate-900 lg:sticky lg:top-0 lg:h-screen overflow-hidden">
<img
src="https://images.unsplash.com/photo-1565553642973-6afe791aee33?q=80&w=2623&auto=format&fit=crop"
alt="Modern Facility Hallway"
className="absolute inset-0 w-full h-full object-cover opacity-40 mix-blend-overlay"
/>
<div className="absolute inset-0 bg-gradient-to-t from-slate-950 via-slate-900/80 to-transparent" />
{/* Animated Particles/Orbs could go here for more 'Aura' */}
<div className="relative z-10 max-w-lg">
<button
onClick={onBack}
className="absolute -top-40 left-0 lg:hidden text-white/50 hover:text-white flex items-center gap-2 mb-4"
>
<ArrowLeft size={18} /> Back
</button>
<div className="flex items-center gap-2 mb-6">
<span className="badge badge-accent bg-cyan-500/10 text-cyan-400 border-cyan-500/20 shadow-[0_0_15px_rgba(6,182,212,0.3)]">SECURE FACILITY</span>
</div>
<h3 className="text-4xl sm:text-5xl font-bold text-white drop-shadow-2xl leading-tight">
Welcome to <br />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 to-blue-500 animate-pulse-slow">
777 Wolfpack
</span>
</h3>
<p className="mt-4 text-lg text-slate-300 leading-relaxed">
Please complete the check-in process to receive your digital visitor badge and safety clearance.
</p>
<ul className="mt-10 grid grid-cols-1 sm:grid-cols-2 gap-x-8 gap-y-5">
{[
"Digital NDA Signature",
"Safety Briefing",
"Instant Badge Issuance",
"Escort Assignment",
].map((item, i) => (
<li
key={i}
className="flex items-center gap-3 text-slate-200 text-lg font-medium"
>
<CheckCircle className="text-cyan-500 w-5 h-5 flex-shrink-0" />
{item}
</li>
))}
</ul>
</div>
</div>
{/* Right Form Section - Scrollable */}
<div className="flex items-center justify-center px-6 py-16 lg:py-24 bg-gradient-to-br from-white via-slate-50 to-white relative">
<button
onClick={onBack}
className="absolute top-8 left-8 hidden lg:flex items-center gap-2 text-slate-400 hover:text-slate-600 transition-colors"
>
<ArrowLeft size={20} /> Back
</button>
<div className="w-full max-w-md bg-white/80 backdrop-blur-xl shadow-2xl rounded-3xl p-8 lg:p-10 border border-white/50 ring-1 ring-slate-100">
<h2 className="text-3xl font-bold text-slate-900 sm:text-4xl tracking-tight">
Visitor <span className="text-cyan-600">Check-In</span>
</h2>
<p className="mt-2 text-sm text-slate-600">
Create your visitor profile for today.
</p>
{error && (
<div className="mt-6 bg-red-50 text-red-600 px-4 py-3 rounded-xl text-sm flex items-start gap-3 border border-red-100">
<AlertTriangle className="w-5 h-5 flex-shrink-0 mt-0.5" />
<span>{error}</span>
</div>
)}
{/* Form */}
<form className="mt-8 space-y-5" onSubmit={handleSubmit}>
{/* Name */}
<div className={commonStyles.inputWrapper}>
<User className={commonStyles.inputIcon} />
<input
type="text"
placeholder="Full Name *"
className={`${commonStyles.input} peer`}
id="fullname"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
disabled={loading}
/>
<label htmlFor="fullname" className={commonStyles.label}>
Full Name
</label>
</div>
{/* Company */}
<div className={commonStyles.inputWrapper}>
<Building className={commonStyles.inputIcon} />
<input
type="text"
placeholder="Company / Organization"
className={`${commonStyles.input} peer`}
id="company"
value={formData.company}
onChange={(e) => setFormData({ ...formData, company: e.target.value })}
disabled={loading}
/>
<label htmlFor="company" className={commonStyles.label}>
Company
</label>
</div>
<div className="grid grid-cols-2 gap-4">
{/* Email */}
<div className={commonStyles.inputWrapper}>
<Mail className={commonStyles.inputIcon} />
<input
type="email"
placeholder="Email"
className={`${commonStyles.input} peer`}
id="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
disabled={loading}
/>
<label htmlFor="email" className={commonStyles.label}>
Email
</label>
</div>
{/* Phone */}
<div className={commonStyles.inputWrapper}>
<Phone className={commonStyles.inputIcon} />
<input
type="tel"
placeholder="Phone"
className={`${commonStyles.input} peer`}
id="phone"
value={formData.phone}
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
disabled={loading}
/>
<label htmlFor="phone" className={commonStyles.label}>
Phone
</label>
</div>
</div>
{/* Purpose */}
<div className={commonStyles.inputWrapper}>
<FileText className={commonStyles.inputIcon} />
<input
type="text"
placeholder="Purpose of Visit *"
className={`${commonStyles.input} peer`}
id="purpose"
value={formData.purpose}
onChange={(e) => setFormData({ ...formData, purpose: e.target.value })}
required
disabled={loading}
/>
<label htmlFor="purpose" className={commonStyles.label}>
Purpose of Visit
</label>
</div>
{/* Visitor Type Selector */}
<div className={commonStyles.inputWrapper}>
<Shield className={commonStyles.inputIcon} />
<select
className={`${commonStyles.input} peer appearance-none`}
value={formData.type}
onChange={(e) => setFormData({ ...formData, type: e.target.value as any })}
disabled={loading}
>
<option value="VISITOR">Standard Visitor</option>
<option value="CONTRACTOR">Contractor</option>
<option value="INSPECTOR">Inspector</option>
<option value="VENDOR">Vendor</option>
<option value="DELIVERY">Delivery</option>
</select>
<label className={commonStyles.label}>
Visit Type
</label>
</div>
{/* Photo Capture */}
<div className="relative group rounded-3xl overflow-hidden shadow-inner bg-slate-50 border border-slate-200 hover:border-cyan-400/50 transition-colors">
{!showCamera && !capturedPhoto && (
<button
type="button"
onClick={startCamera}
className="w-full h-32 flex flex-col items-center justify-center gap-2 text-slate-400 hover:text-cyan-600 transition-colors"
disabled={loading}
>
<Camera className="w-8 h-8 opacity-50 group-hover:opacity-100" />
<span className="text-sm font-medium">Take Security Photo (Optional)</span>
</button>
)}
{showCamera && (
<div className="relative aspect-[4/3] bg-black">
<video
ref={videoRef}
autoPlay
playsInline
muted
className="w-full h-full object-cover"
/>
<div className="absolute bottom-4 left-0 right-0 flex justify-center gap-4 px-4">
<button
type="button"
onClick={stopCamera}
className="px-4 py-2 bg-slate-800/80 text-white rounded-full text-sm hover:bg-slate-700 backdrop-blur-md"
>
Cancel
</button>
<button
type="button"
onClick={capturePhoto}
className="px-6 py-2 bg-cyan-500 text-white rounded-full text-sm font-bold shadow-lg shadow-cyan-500/30 hover:bg-cyan-400"
>
Capture
</button>
</div>
</div>
)}
{capturedPhoto && (
<div className="relative aspect-[4/3] bg-black group">
<img
src={capturedPhoto}
alt="Captured"
className="w-full h-full object-cover opacity-90"
/>
<div className="absolute inset-0 flex items-center justify-center bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity">
<button
type="button"
onClick={retakePhoto}
className="px-4 py-2 bg-red-500/90 text-white rounded-full text-sm font-medium flex items-center gap-2 hover:bg-red-500"
>
<Trash2 size={16} /> Retake
</button>
</div>
<div className="absolute top-2 right-2">
<span className="bg-emerald-500 text-white text-xs px-2 py-1 rounded-full flex items-center gap-1 shadow-sm">
<CheckCircle size={10} /> Photo Ready
</span>
</div>
</div>
)}
</div>
{/* NDA Checkbox */}
<label className="flex items-start gap-3 p-4 bg-slate-50 rounded-2xl cursor-pointer hover:bg-slate-100 transition-colors border border-transparent hover:border-slate-200">
<div className={`mt-0.5 w-5 h-5 rounded-full border flex items-center justify-center transition-all ${formData.ndaAccepted ? 'bg-cyan-500 border-cyan-500 text-white' : 'bg-white border-slate-300'}`}>
{formData.ndaAccepted && <CheckCircle size={14} />}
</div>
<input
type="checkbox"
checked={formData.ndaAccepted}
onChange={e => setFormData({ ...formData, ndaAccepted: e.target.checked })}
className="sr-only"
disabled={loading}
/>
<div className="text-sm">
<span className="text-slate-700 font-medium">Agreement Required</span>
<p className="text-slate-500 text-xs mt-0.5">I agree to the 777 Wolfpack Non-Disclosure Agreement and safety policies.</p>
</div>
</label>
<button
type="submit"
disabled={loading}
className={commonStyles.button}
>
{loading ? (
<>
<Loader2 className="animate-spin" size={20} /> Processing...
</>
) : (
<>
Start Check-In <ArrowRight size={18} />
</>
)}
</button>
</form>
</div>
</div>
</div>
</section>
);
};
export default VisitorCheckIn;

View file

@ -1,8 +1,9 @@
import { useState, useEffect, useRef } from 'react';
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { QRCodeSVG as QRCode } from 'qrcode.react';
import { User, Building, Clock, CheckCircle, XCircle, UserPlus, LogOut, Search, Shield, AlertTriangle, Camera, Trash2, Download, LayoutDashboard } from 'lucide-react';
import { User, Clock, CheckCircle, XCircle, UserPlus, LogOut, Search, Shield, AlertTriangle, Download, LayoutDashboard } from 'lucide-react';
import { visitorsApi, Visitor, ActiveVisitor } from '../lib/visitorsApi';
import VisitorCheckIn from '../components/aura/VisitorCheckIn';
type KioskMode = 'home' | 'new-visitor' | 'returning' | 'check-in' | 'check-out' | 'success';
@ -16,67 +17,6 @@ export default function VisitorKioskPage() {
const [error, setError] = useState<string | null>(null);
const [successData, setSuccessData] = useState<{ badgeNumber?: string; message: string; visitId?: string } | null>(null);
// Form state for new visitor
const [formData, setFormData] = useState({
name: '',
company: '',
email: '',
phone: '',
type: 'VISITOR' as const,
purpose: '',
ndaAccepted: false
});
// Photo capture state
const [capturedPhoto, setCapturedPhoto] = useState<string | null>(null);
const [showCamera, setShowCamera] = useState(false);
const videoRef = useRef<HTMLVideoElement>(null);
const streamRef = useRef<MediaStream | null>(null);
const startCamera = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'user', width: 640, height: 480 }
});
streamRef.current = stream;
if (videoRef.current) {
videoRef.current.srcObject = stream;
}
setShowCamera(true);
} catch (err) {
console.error('Camera access denied:', err);
setError('Camera access denied. Please allow camera permissions.');
}
};
const capturePhoto = () => {
if (videoRef.current) {
const canvas = document.createElement('canvas');
canvas.width = videoRef.current.videoWidth;
canvas.height = videoRef.current.videoHeight;
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.drawImage(videoRef.current, 0, 0);
const photoDataUrl = canvas.toDataURL('image/jpeg', 0.8);
setCapturedPhoto(photoDataUrl);
stopCamera();
}
}
};
const stopCamera = () => {
if (streamRef.current) {
streamRef.current.getTracks().forEach(track => track.stop());
streamRef.current = null;
}
setShowCamera(false);
};
const retakePhoto = () => {
setCapturedPhoto(null);
startCamera();
};
useEffect(() => {
loadActiveVisitors();
}, []);
@ -104,46 +44,6 @@ export default function VisitorKioskPage() {
}
};
const handleNewVisitorSubmit = async () => {
if (!formData.name || !formData.purpose) {
setError('Name and purpose are required');
return;
}
setLoading(true);
setError(null);
try {
// Only include non-empty fields to avoid validation errors on optional fields
const visitorData: Record<string, any> = {
name: formData.name,
purpose: formData.purpose,
type: formData.type
};
if (formData.company) visitorData.company = formData.company;
if (formData.email) visitorData.email = formData.email;
if (formData.phone) visitorData.phone = formData.phone;
if (capturedPhoto) visitorData.photoUrl = capturedPhoto;
const visitor = await visitorsApi.create(visitorData);
const result = await visitorsApi.checkIn(visitor.id, {
ndaAccepted: formData.ndaAccepted
});
setSuccessData({
badgeNumber: result.badgeNumber,
visitId: result.visitId,
message: `Welcome, ${visitor.name}!`
});
setMode('success');
loadActiveVisitors();
} catch (err: any) {
setError(err.response?.data?.error || 'Failed to check in');
} finally {
setLoading(false);
}
};
const handleReturningVisitorCheckIn = async (visitor: Visitor) => {
setLoading(true);
setError(null);
@ -189,17 +89,6 @@ export default function VisitorKioskPage() {
setSearchResults([]);
setError(null);
setSuccessData(null);
setFormData({
name: '',
company: '',
email: '',
phone: '',
type: 'VISITOR',
purpose: '',
ndaAccepted: false
});
setCapturedPhoto(null);
stopCamera();
};
// Auto-reset success screen after 5 seconds ONLY for check-out (no badge number)
@ -210,6 +99,19 @@ export default function VisitorKioskPage() {
}
}, [mode, successData]);
if (mode === 'new-visitor') {
return (
<VisitorCheckIn
onBack={resetToHome}
onSuccess={(data) => {
setSuccessData(data);
setMode('success');
loadActiveVisitors();
}}
/>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 flex flex-col">
{/* Header */}
@ -310,179 +212,6 @@ export default function VisitorKioskPage() {
</div>
)}
{/* New Visitor Form */}
{mode === 'new-visitor' && (
<div>
<button onClick={resetToHome} className="text-slate-400 hover:text-white mb-6 flex items-center gap-2">
Back
</button>
<h2 className="text-2xl font-bold text-white mb-6">New Visitor Registration</h2>
<div className="space-y-4">
<div>
<label className="block text-sm text-slate-400 mb-1">Full Name *</label>
<input
type="text"
value={formData.name}
onChange={e => setFormData({ ...formData, name: e.target.value })}
className="w-full bg-slate-800 border border-slate-700 rounded-xl px-4 py-3 text-white text-lg focus:border-emerald-500 outline-none"
placeholder="John Smith"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm text-slate-400 mb-1">Company</label>
<input
type="text"
value={formData.company}
onChange={e => setFormData({ ...formData, company: e.target.value })}
className="w-full bg-slate-800 border border-slate-700 rounded-xl px-4 py-3 text-white focus:border-emerald-500 outline-none"
/>
</div>
<div>
<label className="block text-sm text-slate-400 mb-1">Visit Type</label>
<select
value={formData.type}
onChange={e => setFormData({ ...formData, type: e.target.value as any })}
className="w-full bg-slate-800 border border-slate-700 rounded-xl px-4 py-3 text-white focus:border-emerald-500 outline-none"
>
<option value="VISITOR">Visitor</option>
<option value="CONTRACTOR">Contractor</option>
<option value="INSPECTOR">Inspector</option>
<option value="VENDOR">Vendor</option>
<option value="DELIVERY">Delivery</option>
</select>
</div>
</div>
<div>
<label className="block text-sm text-slate-400 mb-1">Purpose of Visit *</label>
<input
type="text"
value={formData.purpose}
onChange={e => setFormData({ ...formData, purpose: e.target.value })}
className="w-full bg-slate-800 border border-slate-700 rounded-xl px-4 py-3 text-white focus:border-emerald-500 outline-none"
placeholder="Meeting with..."
/>
</div>
{/* Photo Capture Section */}
<div className="border border-dashed border-slate-600 rounded-xl p-4">
<label className="block text-sm text-slate-400 mb-3">Photo (Optional)</label>
{!capturedPhoto && !showCamera && (
<button
type="button"
onClick={startCamera}
className="w-full bg-slate-700 hover:bg-slate-600 text-white py-4 rounded-xl flex items-center justify-center gap-3 transition-colors"
>
<Camera size={24} />
<span>Take Photo</span>
</button>
)}
{showCamera && (
<div className="space-y-3">
<div className="relative rounded-xl overflow-hidden bg-black aspect-[4/3]">
<video
ref={videoRef}
autoPlay
playsInline
muted
className="w-full h-full object-cover"
/>
</div>
<div className="flex gap-3">
<button
type="button"
onClick={stopCamera}
className="flex-1 bg-slate-700 hover:bg-slate-600 text-white py-3 rounded-xl transition-colors"
>
Cancel
</button>
<button
type="button"
onClick={capturePhoto}
className="flex-1 bg-emerald-500 hover:bg-emerald-600 text-white py-3 rounded-xl flex items-center justify-center gap-2 transition-colors"
>
<Camera size={20} />
Capture
</button>
</div>
</div>
)}
{capturedPhoto && (
<div className="space-y-3">
<div className="relative rounded-xl overflow-hidden bg-black aspect-[4/3]">
<img
src={capturedPhoto}
alt="Captured"
className="w-full h-full object-cover"
/>
<div className="absolute top-2 right-2">
<span className="bg-emerald-500 text-white text-xs px-2 py-1 rounded-full flex items-center gap-1">
<CheckCircle size={12} />
Photo Captured
</span>
</div>
</div>
<button
type="button"
onClick={retakePhoto}
className="w-full bg-slate-700 hover:bg-slate-600 text-white py-3 rounded-xl flex items-center justify-center gap-2 transition-colors"
>
<Trash2 size={18} />
Retake Photo
</button>
</div>
)}
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm text-slate-400 mb-1">Email</label>
<input
type="email"
value={formData.email}
onChange={e => setFormData({ ...formData, email: e.target.value })}
className="w-full bg-slate-800 border border-slate-700 rounded-xl px-4 py-3 text-white focus:border-emerald-500 outline-none"
/>
</div>
<div>
<label className="block text-sm text-slate-400 mb-1">Phone</label>
<input
type="tel"
value={formData.phone}
onChange={e => setFormData({ ...formData, phone: e.target.value })}
className="w-full bg-slate-800 border border-slate-700 rounded-xl px-4 py-3 text-white focus:border-emerald-500 outline-none"
/>
</div>
</div>
<label className="flex items-center gap-3 p-4 bg-slate-800/50 rounded-xl cursor-pointer">
<input
type="checkbox"
checked={formData.ndaAccepted}
onChange={e => setFormData({ ...formData, ndaAccepted: e.target.checked })}
className="w-5 h-5 rounded"
/>
<span className="text-white">I agree to the facility Non-Disclosure Agreement</span>
</label>
<button
onClick={handleNewVisitorSubmit}
disabled={loading}
className="w-full bg-emerald-500 hover:bg-emerald-600 disabled:opacity-50 text-white font-bold py-4 rounded-xl transition-colors text-lg"
>
{loading ? 'Processing...' : 'Check In'}
</button>
</div>
</div>
)}
{/* Returning Visitor Search */}
{mode === 'returning' && (
<div>