feat(ui): Refactor Kiosk Landing Page with AuraUI Shell
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

- Extracted VisitorKioskShell from VisitorCheckIn
- Updated VisitorKioskPage to use shell for Home, Returning, and Success modes
- Implemented light-mode glassmorphism styles for Kiosk buttons
- Ensured consistent user experience across the entire Kiosk flow
This commit is contained in:
fullsizemalt 2025-12-19 17:53:56 -08:00
parent 2a607b23b5
commit df9432ac1a
2 changed files with 482 additions and 495 deletions

View file

@ -1,4 +1,4 @@
import { useRef, useState } from "react";
import { useRef, useState, ReactNode } from "react";
import { Link } from "react-router-dom";
import {
CheckCircle,
@ -31,6 +31,93 @@ const commonStyles = {
link: "font-medium text-cyan-600 hover:text-cyan-700 transition-colors hover:underline",
};
interface VisitorKioskShellProps {
children: ReactNode;
onBack?: () => void;
title?: ReactNode;
subtitle?: string;
}
export const VisitorKioskShell = ({ children, onBack, title, subtitle }: VisitorKioskShellProps) => {
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" />
<div className="relative z-10 max-w-lg">
{onBack && (
<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 Content 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">
{onBack && (
<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">
{(title || subtitle) && (
<div className="mb-8">
{title && <h2 className="text-3xl font-bold text-slate-900 sm:text-4xl tracking-tight">{title}</h2>}
{subtitle && <p className="mt-2 text-sm text-slate-600">{subtitle}</p>}
</div>
)}
{children}
</div>
</div>
</div>
</section>
);
};
interface VisitorCheckInProps {
onBack: () => void;
onSuccess: (data: { badgeNumber: string; visitId: string; message: string }) => void;
@ -153,297 +240,234 @@ const VisitorCheckIn = ({ onBack, onSuccess }: VisitorCheckInProps) => {
};
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"
<VisitorKioskShell
onBack={onBack}
title={
<>
Visitor <span className="text-cyan-600">Check-In</span>
</>
}
subtitle="Create your visitor profile for today."
>
{error && (
<div className="mb-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="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}
/>
<div className="absolute inset-0 bg-gradient-to-t from-slate-950 via-slate-900/80 to-transparent" />
<label htmlFor="fullname" className={commonStyles.label}>
Full Name
</label>
</div>
{/* Animated Particles/Orbs could go here for more 'Aura' */}
{/* 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="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 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>
{/* 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"
{/* 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}
>
<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>
<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>
</div>
</section>
{/* 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>
</VisitorKioskShell>
);
};

View file

@ -1,9 +1,8 @@
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { QRCodeSVG as QRCode } from 'qrcode.react';
import { User, Clock, CheckCircle, XCircle, UserPlus, LogOut, Search, Shield, AlertTriangle, Download, LayoutDashboard } from 'lucide-react';
import { User, CheckCircle, XCircle, UserPlus, LogOut, Search, AlertTriangle } from 'lucide-react';
import { visitorsApi, Visitor, ActiveVisitor } from '../lib/visitorsApi';
import VisitorCheckIn from '../components/aura/VisitorCheckIn';
import VisitorCheckIn, { VisitorKioskShell } from '../components/aura/VisitorCheckIn';
type KioskMode = 'home' | 'new-visitor' | 'returning' | 'check-in' | 'check-out' | 'success';
@ -11,7 +10,6 @@ export default function VisitorKioskPage() {
const [mode, setMode] = useState<KioskMode>('home');
const [activeVisitors, setActiveVisitors] = useState<ActiveVisitor[]>([]);
const [searchResults, setSearchResults] = useState<Visitor[]>([]);
const [selectedVisitor, setSelectedVisitor] = useState<Visitor | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
@ -84,7 +82,6 @@ export default function VisitorKioskPage() {
const resetToHome = () => {
setMode('home');
setSelectedVisitor(null);
setSearchQuery('');
setSearchResults([]);
setError(null);
@ -112,227 +109,193 @@ export default function VisitorKioskPage() {
);
}
// Dynamic Title and Subtitle based on mode
let shellTitle = "Welcome";
let shellSubtitle = "Please select an option to continue";
if (mode === 'returning') {
shellTitle = "Returning Visitor";
shellSubtitle = "Quick check-in for registered visitors";
} else if (mode === 'success') {
shellTitle = "Success";
shellSubtitle = "Action completed successfully";
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 flex flex-col">
{/* Header */}
<header className="bg-slate-800/50 border-b border-white/10 p-4">
<div className="flex items-center justify-between max-w-4xl mx-auto">
<div className="flex items-center gap-3">
<Shield className="text-emerald-400" size={28} />
<VisitorKioskShell
onBack={mode !== 'home' ? resetToHome : undefined}
title={mode !== 'success' ? shellTitle : undefined}
subtitle={mode !== 'success' ? shellSubtitle : undefined}
>
{/* Error Alert */}
{error && (
<div className="mb-6 bg-red-50 text-red-600 border border-red-100 rounded-xl p-4 flex items-center gap-3">
<AlertTriangle className="text-red-500" size={20} />
<span>{error}</span>
<button onClick={() => setError(null)} className="ml-auto text-red-400 hover:text-red-500">
<XCircle size={18} />
</button>
</div>
)}
{/* Home Screen */}
{mode === 'home' && (
<div className="space-y-4">
<button
onClick={() => setMode('new-visitor')}
className="w-full bg-white hover:bg-slate-50 border border-slate-200 hover:border-cyan-400 rounded-2xl p-6 text-left transition-all group shadow-sm hover:shadow-md flex items-center gap-5"
>
<div className="w-14 h-14 rounded-full bg-cyan-50 text-cyan-500 flex items-center justify-center group-hover:scale-110 transition-transform">
<UserPlus size={28} />
</div>
<div>
<h1 className="text-xl font-bold text-white">Visitor Check-In</h1>
<p className="text-xs text-slate-400">777 Wolfpack Facility</p>
<h3 className="text-lg font-bold text-slate-800 group-hover:text-cyan-600 transition-colors">First Time Visitor</h3>
<p className="text-sm text-slate-500">Register and check in for your first visit</p>
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 text-slate-400 text-sm">
<Clock size={16} />
<span>{new Date().toLocaleTimeString()}</span>
</button>
<button
onClick={() => setMode('returning')}
className="w-full bg-white hover:bg-slate-50 border border-slate-200 hover:border-blue-400 rounded-2xl p-6 text-left transition-all group shadow-sm hover:shadow-md flex items-center gap-5"
>
<div className="w-14 h-14 rounded-full bg-blue-50 text-blue-500 flex items-center justify-center group-hover:scale-110 transition-transform">
<User size={28} />
</div>
<Link
to="/"
className="flex items-center gap-2 text-slate-400 hover:text-emerald-400 text-sm transition-colors px-3 py-1.5 rounded-lg hover:bg-white/5"
<div>
<h3 className="text-lg font-bold text-slate-800 group-hover:text-blue-600 transition-colors">Returning Visitor</h3>
<p className="text-sm text-slate-500">Quick check-in for registered visitors</p>
</div>
</button>
{/* Check Out Section */}
{activeVisitors.length > 0 && (
<div className="mt-8 pt-8 border-t border-slate-100">
<h3 className="text-sm font-semibold text-slate-400 uppercase tracking-wider mb-4 flex items-center justify-center gap-2">
<LogOut size={16} />
Check Out ({activeVisitors.length} on-site)
</h3>
<div className="grid gap-2 max-h-48 overflow-y-auto pr-1">
{activeVisitors.map(visitor => (
<button
key={visitor.logId}
onClick={() => handleCheckOut(visitor)}
className="w-full bg-slate-50 hover:bg-red-50 hover:border-red-200 border border-transparent rounded-xl p-3 flex items-center justify-between transition-colors group"
>
<div className="text-left">
<span className="text-slate-700 font-medium group-hover:text-red-700">{visitor.name}</span>
{visitor.company && (
<span className="text-slate-400 ml-2 text-xs">({visitor.company})</span>
)}
</div>
<span className="text-xs bg-white text-slate-500 px-2 py-1 rounded border border-slate-100 group-hover:border-red-100">
#{visitor.badgeNumber}
</span>
</button>
))}
</div>
</div>
)}
</div>
)}
{/* Returning Visitor Search */}
{mode === 'returning' && (
<div>
<div className="flex gap-2 mb-6">
<div className="flex-1 relative">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400" size={20} />
<input
type="text"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleSearch()}
className="w-full bg-slate-50 border border-slate-200 rounded-xl pl-12 pr-4 py-3 text-slate-900 text-lg focus:border-cyan-500 outline-none focus:ring-2 focus:ring-cyan-500/20 transition-all"
placeholder="Search by name..."
autoFocus
/>
</div>
<button
onClick={handleSearch}
disabled={loading}
className="bg-blue-600 hover:bg-blue-700 px-6 py-3 rounded-xl text-white font-medium shadow-md shadow-blue-500/20 transition-all"
>
<LayoutDashboard size={16} />
<span className="hidden sm:inline">Dashboard</span>
</Link>
Search
</button>
</div>
</div>
</header>
{/* Main Content */}
<main className="flex-1 flex items-center justify-center p-8">
<div className="w-full max-w-2xl">
{/* Error Alert */}
{error && (
<div className="mb-6 bg-red-500/10 border border-red-500/20 rounded-xl p-4 flex items-center gap-3">
<AlertTriangle className="text-red-400" size={20} />
<span className="text-red-400">{error}</span>
<button onClick={() => setError(null)} className="ml-auto text-red-400 hover:text-red-300">
<XCircle size={18} />
</button>
</div>
)}
{/* Home Screen */}
{mode === 'home' && (
<div className="text-center">
<h2 className="text-3xl font-bold text-white mb-2">Welcome</h2>
<p className="text-slate-400 mb-8">Please select an option to continue</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{searchResults.length > 0 && (
<div className="space-y-3">
{searchResults.map(visitor => (
<button
onClick={() => setMode('new-visitor')}
className="bg-emerald-500/10 hover:bg-emerald-500/20 border border-emerald-500/30 rounded-2xl p-8 text-left transition-all group"
key={visitor.id}
onClick={() => handleReturningVisitorCheckIn(visitor)}
className="w-full bg-white hover:bg-cyan-50 border border-slate-100 hover:border-cyan-200 rounded-xl p-4 flex items-center justify-between transition-all group shadow-sm"
>
<UserPlus className="text-emerald-400 mb-4" size={48} />
<h3 className="text-xl font-bold text-white mb-2">First Time Visitor</h3>
<p className="text-sm text-slate-400">Register and check in for your first visit</p>
</button>
<button
onClick={() => setMode('returning')}
className="bg-blue-500/10 hover:bg-blue-500/20 border border-blue-500/30 rounded-2xl p-8 text-left transition-all group"
>
<User className="text-blue-400 mb-4" size={48} />
<h3 className="text-xl font-bold text-white mb-2">Returning Visitor</h3>
<p className="text-sm text-slate-400">Quick check-in for registered visitors</p>
</button>
</div>
{/* Check Out Section */}
{activeVisitors.length > 0 && (
<div className="mt-8 pt-8 border-t border-white/10">
<h3 className="text-lg font-bold text-white mb-4 flex items-center justify-center gap-2">
<LogOut size={20} />
Check Out ({activeVisitors.length} visitors on-site)
</h3>
<div className="grid gap-3 max-h-48 overflow-y-auto">
{activeVisitors.map(visitor => (
<button
key={visitor.logId}
onClick={() => handleCheckOut(visitor)}
className="bg-slate-800 hover:bg-slate-700 rounded-xl p-4 flex items-center justify-between transition-colors"
>
<div className="text-left">
<span className="text-white font-medium">{visitor.name}</span>
{visitor.company && (
<span className="text-slate-400 ml-2 text-sm">({visitor.company})</span>
)}
</div>
<span className="text-xs bg-emerald-500/20 text-emerald-400 px-2 py-1 rounded">
Badge: {visitor.badgeNumber}
</span>
</button>
))}
</div>
</div>
)}
</div>
)}
{/* Returning Visitor Search */}
{mode === 'returning' && (
<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">Find Your Record</h2>
<div className="flex gap-3 mb-6">
<div className="flex-1 relative">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400" size={20} />
<input
type="text"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleSearch()}
className="w-full bg-slate-800 border border-slate-700 rounded-xl pl-12 pr-4 py-3 text-white text-lg focus:border-emerald-500 outline-none"
placeholder="Search by name, email, or company..."
/>
</div>
<button
onClick={handleSearch}
disabled={loading}
className="bg-blue-500 hover:bg-blue-600 px-6 py-3 rounded-xl text-white font-medium"
>
Search
</button>
</div>
{searchResults.length > 0 && (
<div className="space-y-3">
{searchResults.map(visitor => (
<button
key={visitor.id}
onClick={() => handleReturningVisitorCheckIn(visitor)}
className="w-full bg-slate-800 hover:bg-slate-700 rounded-xl p-4 flex items-center justify-between transition-colors text-left"
>
<div>
<div className="text-white font-medium">{visitor.name}</div>
<div className="text-sm text-slate-400">
{visitor.company && `${visitor.company}`}
{visitor.type}
</div>
</div>
<CheckCircle className="text-emerald-400" size={24} />
</button>
))}
</div>
)}
{searchQuery && searchResults.length === 0 && !loading && (
<div className="text-center py-8 text-slate-400">
No visitors found. Try a different search or register as a new visitor.
</div>
)}
</div>
)}
{/* Success Screen */}
{mode === 'success' && successData && (
<div className="text-center py-12">
<div className="w-24 h-24 mx-auto mb-6 bg-emerald-500/20 rounded-full flex items-center justify-center">
<CheckCircle className="text-emerald-400" size={48} />
</div>
<h2 className="text-3xl font-bold text-white mb-2">{successData.message}</h2>
{successData.badgeNumber && ( // Only show QR for Check-IN, not Check-OUT
<div className="mt-8 space-y-6">
<div className="inline-block bg-white p-8 rounded-3xl shadow-2xl">
<QRCode
size={320}
style={{ height: 320, width: 320 }}
value={`${window.location.origin}/badges/${successData.visitId || 'demo-token'}`}
/>
</div>
<div>
<p className="text-white font-bold text-lg mb-1">Scan for Digital Badge</p>
<p className="text-slate-400 text-sm">Use your phone to carry your badge with you.</p>
<div className="text-slate-800 font-bold group-hover:text-cyan-700">{visitor.name}</div>
<div className="text-sm text-slate-500 group-hover:text-cyan-600/80">
{visitor.company && `${visitor.company}`}
{visitor.type}
</div>
</div>
<div className="bg-slate-800 rounded-2xl p-4 inline-block">
<p className="text-slate-400 text-xs mb-1">Badge Number</p>
<p className="text-2xl font-bold font-mono text-emerald-400">{successData.badgeNumber}</p>
</div>
<div className="pt-8">
<button
onClick={resetToHome}
className="bg-emerald-600 hover:bg-emerald-700 text-white font-bold text-lg px-8 py-4 rounded-xl transition-colors shadow-lg hover:shadow-emerald-500/20"
>
I have my badge
</button>
</div>
</div>
)}
{!successData.badgeNumber && ( // Auto-redirect for Check-OUT only
<p className="mt-6 text-slate-400">Returning to home in 5 seconds...</p>
)}
<CheckCircle className="text-gray-200 group-hover:text-cyan-500 transition-colors" size={24} />
</button>
))}
</div>
)}
{searchQuery && searchResults.length === 0 && !loading && (
<div className="text-center py-12 text-slate-400 bg-slate-50 rounded-xl border border-dashed border-slate-200">
No visitors found. <br />Try a different search or register as new.
</div>
)}
</div>
</main>
)}
{/* Footer */}
<footer className="bg-slate-800/50 border-t border-white/10 p-4 text-center text-slate-500 text-sm flex flex-col md:flex-row justify-center items-center gap-4">
<p>By signing in, you agree to follow all facility rules and regulations.</p>
<div className="hidden md:block w-px h-4 bg-white/10" />
<a
href="/visitorkiosk.apk"
className="text-emerald-500/80 hover:text-emerald-400 font-medium transition-colors flex items-center gap-2 px-3 py-1 rounded-lg hover:bg-emerald-500/10"
download
title="Download the Android App"
>
<Download size={14} />
Download Kiosk App
</a>
</footer>
</div>
{/* Success Screen */}
{mode === 'success' && successData && (
<div className="text-center py-6">
<div className="w-20 h-20 mx-auto mb-6 bg-emerald-100 text-emerald-600 rounded-full flex items-center justify-center animate-bounce-slow">
<CheckCircle size={40} />
</div>
<h2 className="text-2xl font-bold text-slate-900 mb-2">{successData.message}</h2>
{successData.badgeNumber && ( // Only show QR for Check-IN, not Check-OUT
<div className="mt-8 space-y-6">
<div className="inline-block bg-white p-4 rounded-3xl shadow-lg border border-slate-100">
<QRCode
size={240}
style={{ height: 240, width: 240 }}
value={`${window.location.origin}/badges/${successData.visitId || 'demo-token'}`}
/>
</div>
<div>
<p className="text-slate-800 font-bold text-lg mb-1">Scan for Digital Badge</p>
<p className="text-slate-500 text-sm">Use your phone to carry your badge with you.</p>
</div>
<div className="bg-slate-50 border border-slate-200 rounded-2xl p-4 inline-block">
<p className="text-slate-400 text-xs mb-1 uppercase tracking-wider font-semibold">Badge Number</p>
<p className="text-3xl font-mono font-bold text-slate-800">{successData.badgeNumber}</p>
</div>
<div className="pt-4">
<button
onClick={resetToHome}
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white font-bold text-lg px-8 py-4 rounded-xl transition-colors shadow-lg hover:shadow-emerald-500/20"
>
I have my badge
</button>
</div>
</div>
)}
{!successData.badgeNumber && ( // Auto-redirect for Check-OUT only
<p className="mt-6 text-slate-400">Returning to home in 5 seconds...</p>
)}
</div>
)}
</VisitorKioskShell>
);
}