feat(ui): Refactor Kiosk Landing Page with AuraUI Shell
- 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:
parent
2a607b23b5
commit
df9432ac1a
2 changed files with 482 additions and 495 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState, ReactNode } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
|
|
@ -31,6 +31,93 @@ const commonStyles = {
|
||||||
link: "font-medium text-cyan-600 hover:text-cyan-700 transition-colors hover:underline",
|
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 {
|
interface VisitorCheckInProps {
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
onSuccess: (data: { badgeNumber: string; visitId: string; message: string }) => void;
|
onSuccess: (data: { badgeNumber: string; visitId: string; message: string }) => void;
|
||||||
|
|
@ -153,297 +240,234 @@ const VisitorCheckIn = ({ onBack, onSuccess }: VisitorCheckInProps) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative bg-white min-h-screen">
|
<VisitorKioskShell
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 min-h-screen">
|
onBack={onBack}
|
||||||
{/* Left Hero Section - Fixed/Sticky on Desktop */}
|
title={
|
||||||
<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
|
Visitor <span className="text-cyan-600">Check-In</span>
|
||||||
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"
|
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">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<button
|
{/* Email */}
|
||||||
onClick={onBack}
|
<div className={commonStyles.inputWrapper}>
|
||||||
className="absolute -top-40 left-0 lg:hidden text-white/50 hover:text-white flex items-center gap-2 mb-4"
|
<Mail className={commonStyles.inputIcon} />
|
||||||
>
|
<input
|
||||||
<ArrowLeft size={18} /> Back
|
type="email"
|
||||||
</button>
|
placeholder="Email"
|
||||||
|
className={`${commonStyles.input} peer`}
|
||||||
<div className="flex items-center gap-2 mb-6">
|
id="email"
|
||||||
<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>
|
value={formData.email}
|
||||||
</div>
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||||
<h3 className="text-4xl sm:text-5xl font-bold text-white drop-shadow-2xl leading-tight">
|
disabled={loading}
|
||||||
Welcome to <br />
|
/>
|
||||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 to-blue-500 animate-pulse-slow">
|
<label htmlFor="email" className={commonStyles.label}>
|
||||||
777 Wolfpack
|
Email
|
||||||
</span>
|
</label>
|
||||||
</h3>
|
</div>
|
||||||
<p className="mt-4 text-lg text-slate-300 leading-relaxed">
|
{/* Phone */}
|
||||||
Please complete the check-in process to receive your digital visitor badge and safety clearance.
|
<div className={commonStyles.inputWrapper}>
|
||||||
</p>
|
<Phone className={commonStyles.inputIcon} />
|
||||||
<ul className="mt-10 grid grid-cols-1 sm:grid-cols-2 gap-x-8 gap-y-5">
|
<input
|
||||||
{[
|
type="tel"
|
||||||
"Digital NDA Signature",
|
placeholder="Phone"
|
||||||
"Safety Briefing",
|
className={`${commonStyles.input} peer`}
|
||||||
"Instant Badge Issuance",
|
id="phone"
|
||||||
"Escort Assignment",
|
value={formData.phone}
|
||||||
].map((item, i) => (
|
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
||||||
<li
|
disabled={loading}
|
||||||
key={i}
|
/>
|
||||||
className="flex items-center gap-3 text-slate-200 text-lg font-medium"
|
<label htmlFor="phone" className={commonStyles.label}>
|
||||||
>
|
Phone
|
||||||
<CheckCircle className="text-cyan-500 w-5 h-5 flex-shrink-0" />
|
</label>
|
||||||
{item}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Form Section - Scrollable */}
|
{/* Purpose */}
|
||||||
<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">
|
<div className={commonStyles.inputWrapper}>
|
||||||
<button
|
<FileText className={commonStyles.inputIcon} />
|
||||||
onClick={onBack}
|
<input
|
||||||
className="absolute top-8 left-8 hidden lg:flex items-center gap-2 text-slate-400 hover:text-slate-600 transition-colors"
|
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
|
<option value="VISITOR">Standard Visitor</option>
|
||||||
</button>
|
<option value="CONTRACTOR">Contractor</option>
|
||||||
|
<option value="INSPECTOR">Inspector</option>
|
||||||
<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">
|
<option value="VENDOR">Vendor</option>
|
||||||
<h2 className="text-3xl font-bold text-slate-900 sm:text-4xl tracking-tight">
|
<option value="DELIVERY">Delivery</option>
|
||||||
Visitor <span className="text-cyan-600">Check-In</span>
|
</select>
|
||||||
</h2>
|
<label className={commonStyles.label}>
|
||||||
<p className="mt-2 text-sm text-slate-600">
|
Visit Type
|
||||||
Create your visitor profile for today.
|
</label>
|
||||||
</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>
|
||||||
</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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { QRCodeSVG as QRCode } from 'qrcode.react';
|
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 { 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';
|
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 [mode, setMode] = useState<KioskMode>('home');
|
||||||
const [activeVisitors, setActiveVisitors] = useState<ActiveVisitor[]>([]);
|
const [activeVisitors, setActiveVisitors] = useState<ActiveVisitor[]>([]);
|
||||||
const [searchResults, setSearchResults] = useState<Visitor[]>([]);
|
const [searchResults, setSearchResults] = useState<Visitor[]>([]);
|
||||||
const [selectedVisitor, setSelectedVisitor] = useState<Visitor | null>(null);
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
@ -84,7 +82,6 @@ export default function VisitorKioskPage() {
|
||||||
|
|
||||||
const resetToHome = () => {
|
const resetToHome = () => {
|
||||||
setMode('home');
|
setMode('home');
|
||||||
setSelectedVisitor(null);
|
|
||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
setSearchResults([]);
|
setSearchResults([]);
|
||||||
setError(null);
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 flex flex-col">
|
<VisitorKioskShell
|
||||||
{/* Header */}
|
onBack={mode !== 'home' ? resetToHome : undefined}
|
||||||
<header className="bg-slate-800/50 border-b border-white/10 p-4">
|
title={mode !== 'success' ? shellTitle : undefined}
|
||||||
<div className="flex items-center justify-between max-w-4xl mx-auto">
|
subtitle={mode !== 'success' ? shellSubtitle : undefined}
|
||||||
<div className="flex items-center gap-3">
|
>
|
||||||
<Shield className="text-emerald-400" size={28} />
|
{/* 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>
|
<div>
|
||||||
<h1 className="text-xl font-bold text-white">Visitor Check-In</h1>
|
<h3 className="text-lg font-bold text-slate-800 group-hover:text-cyan-600 transition-colors">First Time Visitor</h3>
|
||||||
<p className="text-xs text-slate-400">777 Wolfpack Facility</p>
|
<p className="text-sm text-slate-500">Register and check in for your first visit</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex items-center gap-2 text-slate-400 text-sm">
|
<button
|
||||||
<Clock size={16} />
|
onClick={() => setMode('returning')}
|
||||||
<span>{new Date().toLocaleTimeString()}</span>
|
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>
|
</div>
|
||||||
<Link
|
<div>
|
||||||
to="/"
|
<h3 className="text-lg font-bold text-slate-800 group-hover:text-blue-600 transition-colors">Returning Visitor</h3>
|
||||||
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"
|
<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} />
|
Search
|
||||||
<span className="hidden sm:inline">Dashboard</span>
|
</button>
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Main Content */}
|
{searchResults.length > 0 && (
|
||||||
<main className="flex-1 flex items-center justify-center p-8">
|
<div className="space-y-3">
|
||||||
<div className="w-full max-w-2xl">
|
{searchResults.map(visitor => (
|
||||||
|
|
||||||
{/* 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">
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setMode('new-visitor')}
|
key={visitor.id}
|
||||||
className="bg-emerald-500/10 hover:bg-emerald-500/20 border border-emerald-500/30 rounded-2xl p-8 text-left transition-all group"
|
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>
|
<div>
|
||||||
<p className="text-white font-bold text-lg mb-1">Scan for Digital Badge</p>
|
<div className="text-slate-800 font-bold group-hover:text-cyan-700">{visitor.name}</div>
|
||||||
<p className="text-slate-400 text-sm">Use your phone to carry your badge with you.</p>
|
<div className="text-sm text-slate-500 group-hover:text-cyan-600/80">
|
||||||
|
{visitor.company && `${visitor.company} • `}
|
||||||
|
{visitor.type}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<CheckCircle className="text-gray-200 group-hover:text-cyan-500 transition-colors" size={24} />
|
||||||
<div className="bg-slate-800 rounded-2xl p-4 inline-block">
|
</button>
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</main>
|
)}
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Success Screen */}
|
||||||
<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">
|
{mode === 'success' && successData && (
|
||||||
<p>By signing in, you agree to follow all facility rules and regulations.</p>
|
<div className="text-center py-6">
|
||||||
<div className="hidden md:block w-px h-4 bg-white/10" />
|
<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">
|
||||||
<a
|
<CheckCircle size={40} />
|
||||||
href="/visitorkiosk.apk"
|
</div>
|
||||||
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"
|
<h2 className="text-2xl font-bold text-slate-900 mb-2">{successData.message}</h2>
|
||||||
download
|
|
||||||
title="Download the Android App"
|
{successData.badgeNumber && ( // Only show QR for Check-IN, not Check-OUT
|
||||||
>
|
<div className="mt-8 space-y-6">
|
||||||
<Download size={14} />
|
<div className="inline-block bg-white p-4 rounded-3xl shadow-lg border border-slate-100">
|
||||||
Download Kiosk App
|
<QRCode
|
||||||
</a>
|
size={240}
|
||||||
</footer>
|
style={{ height: 240, width: 240 }}
|
||||||
</div>
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue