458 lines
21 KiB
TypeScript
458 lines
21 KiB
TypeScript
import { Button } from "./components/ui/button";
|
|
import { Input } from "./components/ui/input";
|
|
import { Label } from "./components/ui/label";
|
|
import { Checkbox } from "./components/ui/checkbox";
|
|
import { InputOTP, InputOTPGroup, InputOTPSlot } from "./components/ui/input-otp";
|
|
import { Mail, Lock, Eye, EyeOff, Search, Users, Calendar, TrendingUp, Clock, User } from "lucide-react";
|
|
import { useState } from "react";
|
|
import { motion, AnimatePresence } from "motion/react";
|
|
import FlightBookingRafiki from "./imports/FlightBookingRafiki";
|
|
import Sidebar from "./components/Sidebar";
|
|
import Header from "./components/Header";
|
|
import Dashboard from "./components/Dashboard";
|
|
import RedemptionsPage from "./components/RedemptionsPage";
|
|
import StaffManagementPage from "./components/StaffManagementPage";
|
|
import SupportPage from "./components/SupportPage";
|
|
import BookingManagementPage from "./components/BookingManagementPage";
|
|
import RecurringBlockPage from "./components/RecurringBlockPage";
|
|
import NotificationsPage from "./components/NotificationsPage";
|
|
|
|
export default function App() {
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
const [rememberMe, setRememberMe] = useState(false);
|
|
const [showForgotPassword, setShowForgotPassword] = useState(false);
|
|
const [showOTPVerification, setShowOTPVerification] = useState(false);
|
|
const [showResetPassword, setShowResetPassword] = useState(false);
|
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
|
const [otpValue, setOtpValue] = useState("");
|
|
const [newPassword, setNewPassword] = useState("");
|
|
const [confirmPassword, setConfirmPassword] = useState("");
|
|
const [showPasswordPolicies, setShowPasswordPolicies] = useState(false);
|
|
const [activeNavItem, setActiveNavItem] = useState("dashboard");
|
|
|
|
// Password validation functions
|
|
const validatePassword = (password) => {
|
|
return {
|
|
hasMinLength: password.length >= 8,
|
|
hasUppercase: /[A-Z]/.test(password),
|
|
hasLowercase: /[a-z]/.test(password),
|
|
hasNumber: /[0-9]/.test(password),
|
|
hasSpecialChar: /[!@#$%^&*(),.?":{}|<>]/.test(password)
|
|
};
|
|
};
|
|
|
|
const passwordValidation = validatePassword(newPassword);
|
|
|
|
// If user is logged in, show dashboard
|
|
if (isLoggedIn) {
|
|
return (
|
|
<div className="min-h-screen bg-gray-50">
|
|
{/* Fixed Sidebar */}
|
|
<Sidebar
|
|
activeItem={activeNavItem}
|
|
onItemSelect={setActiveNavItem}
|
|
/>
|
|
|
|
{/* Main Content with left margin for sidebar */}
|
|
<div className="ml-[280px]">
|
|
{/* Header with notifications and profile */}
|
|
<Header onNavigateToNotifications={() => setActiveNavItem("notifications")} />
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4, ease: "easeInOut" }}
|
|
>
|
|
{activeNavItem === "dashboard" && <Dashboard />}
|
|
{(activeNavItem === "booking-table" || activeNavItem === "booking-calendar") && (
|
|
<BookingManagementPage
|
|
activeView={activeNavItem}
|
|
onNavigateToRecurringBlock={() => setActiveNavItem("recurring-block")}
|
|
/>
|
|
)}
|
|
{activeNavItem === "recurring-block" && (
|
|
<RecurringBlockPage onNavigateBack={() => setActiveNavItem("booking-calendar")} />
|
|
)}
|
|
{activeNavItem === "redemptions" && <RedemptionsPage />}
|
|
{activeNavItem === "staff" && <StaffManagementPage />}
|
|
{activeNavItem === "support" && <SupportPage />}
|
|
{activeNavItem === "notifications" && <NotificationsPage />}
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen flex">
|
|
{/* Left Panel - Welcome Section */}
|
|
<div className="flex-1 bg-gray-50 flex flex-col items-center justify-center p-12">
|
|
<div className="max-w-md text-center">
|
|
<div className="mb-8">
|
|
<div className="w-full h-64 mb-8 flex items-center justify-center">
|
|
<FlightBookingRafiki />
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<p className="text-gray-600 text-lg">Welcome to</p>
|
|
<h1 className="text-4xl text-gray-900 font-medium">CityCards</h1>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Panel - Login Section */}
|
|
<div className="flex-1 bg-white flex items-center justify-center p-12">
|
|
<div className="w-full max-w-md">
|
|
<AnimatePresence mode="wait">
|
|
{!showForgotPassword && !showOTPVerification && !showResetPassword ? (
|
|
<motion.div
|
|
key="signin"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -20 }}
|
|
transition={{ duration: 0.3, ease: "easeInOut" }}
|
|
>
|
|
<div className="mb-8">
|
|
<h2 className="text-3xl text-gray-900 font-bold">Sign In to Access Your Account</h2>
|
|
</div>
|
|
|
|
<form className="space-y-6">
|
|
{/* Email Field */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="email" className="text-base font-medium text-gray-900">Email</Label>
|
|
<div className="relative">
|
|
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" />
|
|
<Input
|
|
id="email"
|
|
type="email"
|
|
placeholder="Enter your email address"
|
|
className="pl-10 bg-white border border-gray-200 rounded-lg h-12 font-normal"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Password Field */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="password" className="text-base font-medium text-gray-900">Password</Label>
|
|
<div className="relative">
|
|
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" />
|
|
<Input
|
|
id="password"
|
|
type={showPassword ? "text" : "password"}
|
|
placeholder="Enter your password"
|
|
className="pl-10 pr-10 bg-white border border-gray-200 rounded-lg h-12 font-normal"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
|
>
|
|
{showPassword ? (
|
|
<EyeOff className="h-5 w-5" />
|
|
) : (
|
|
<Eye className="h-5 w-5" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Remember Me and Forgot Password */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="remember"
|
|
checked={rememberMe}
|
|
onCheckedChange={setRememberMe}
|
|
/>
|
|
<Label htmlFor="remember" className="text-sm font-normal text-gray-600">
|
|
Remember me
|
|
</Label>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowForgotPassword(true)}
|
|
className="text-sm font-normal text-blue-600 hover:text-blue-700"
|
|
>
|
|
Forgot Password?
|
|
</button>
|
|
</div>
|
|
|
|
{/* Sign In Button */}
|
|
<Button
|
|
type="submit"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
setIsLoggedIn(true);
|
|
}}
|
|
className="w-full h-12 bg-gray-900 hover:bg-gray-800 text-white rounded-lg font-medium"
|
|
>
|
|
Sign In
|
|
</Button>
|
|
</form>
|
|
</motion.div>
|
|
) : showForgotPassword && !showOTPVerification && !showResetPassword ? (
|
|
<motion.div
|
|
key="forgot"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -20 }}
|
|
transition={{ duration: 0.3, ease: "easeInOut" }}
|
|
className="text-center"
|
|
>
|
|
<div className="mb-8">
|
|
<h1 className="text-3xl font-bold text-gray-900 mb-4">Forgot Password</h1>
|
|
<p className="text-base font-normal text-gray-600 leading-relaxed">
|
|
Forgot your password? Don't worry — just enter your email and we'll help you reset it.
|
|
</p>
|
|
</div>
|
|
|
|
<form className="space-y-6">
|
|
{/* Email Field */}
|
|
<div className="space-y-2 text-left">
|
|
<Label htmlFor="forgot-email" className="text-base font-medium text-gray-900">
|
|
Email
|
|
</Label>
|
|
<div className="relative">
|
|
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" />
|
|
<Input
|
|
id="forgot-email"
|
|
type="email"
|
|
placeholder="Enter your email address"
|
|
className="pl-10 bg-gray-50 border border-gray-200 rounded-lg h-12 font-normal"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Continue Button */}
|
|
<Button
|
|
type="submit"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
setShowOTPVerification(true);
|
|
}}
|
|
className="w-full h-12 bg-gray-600 hover:bg-gray-700 text-white rounded-lg font-medium mt-8"
|
|
>
|
|
Continue
|
|
</Button>
|
|
</form>
|
|
|
|
{/* Back to Sign In */}
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowForgotPassword(false)}
|
|
className="text-sm font-normal text-blue-600 hover:text-blue-700 mt-6"
|
|
>
|
|
Back to Sign In
|
|
</button>
|
|
</motion.div>
|
|
) : showOTPVerification && !showResetPassword ? (
|
|
<motion.div
|
|
key="otp"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -20 }}
|
|
transition={{ duration: 0.3, ease: "easeInOut" }}
|
|
className="text-center"
|
|
>
|
|
<div className="mb-8">
|
|
<h1 className="text-3xl font-bold text-gray-900 mb-4">Verify OTP</h1>
|
|
<p className="text-base font-normal text-gray-600 leading-relaxed">
|
|
We've sent an OTP to your registered email. Please enter it below, or use the reset link included in the email.
|
|
</p>
|
|
</div>
|
|
|
|
<form className="space-y-6">
|
|
{/* OTP Input */}
|
|
<div className="flex justify-center">
|
|
<InputOTP
|
|
maxLength={4}
|
|
value={otpValue}
|
|
onChange={(value) => setOtpValue(value)}
|
|
className="gap-4"
|
|
>
|
|
<InputOTPGroup className="gap-4">
|
|
<InputOTPSlot
|
|
index={0}
|
|
className="w-12 h-12 text-lg font-medium border-2 border-gray-200 rounded-lg bg-white focus:border-gray-400 focus:ring-0"
|
|
/>
|
|
<InputOTPSlot
|
|
index={1}
|
|
className="w-12 h-12 text-lg font-medium border-2 border-gray-200 rounded-lg bg-white focus:border-gray-400 focus:ring-0"
|
|
/>
|
|
<InputOTPSlot
|
|
index={2}
|
|
className="w-12 h-12 text-lg font-medium border-2 border-gray-200 rounded-lg bg-white focus:border-gray-400 focus:ring-0"
|
|
/>
|
|
<InputOTPSlot
|
|
index={3}
|
|
className="w-12 h-12 text-lg font-medium border-2 border-gray-200 rounded-lg bg-white focus:border-gray-400 focus:ring-0"
|
|
/>
|
|
</InputOTPGroup>
|
|
</InputOTP>
|
|
</div>
|
|
|
|
{/* Continue Button */}
|
|
<Button
|
|
type="submit"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
setShowResetPassword(true);
|
|
}}
|
|
className="w-full h-12 bg-gray-600 hover:bg-gray-700 text-white rounded-lg font-medium mt-8"
|
|
>
|
|
Continue
|
|
</Button>
|
|
</form>
|
|
|
|
{/* Back to Forgot Password */}
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowOTPVerification(false)}
|
|
className="text-sm font-normal text-blue-600 hover:text-blue-700 mt-6"
|
|
>
|
|
Back to Forgot Password
|
|
</button>
|
|
</motion.div>
|
|
) : (
|
|
<motion.div
|
|
key="reset"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -20 }}
|
|
transition={{ duration: 0.3, ease: "easeInOut" }}
|
|
className="text-center"
|
|
>
|
|
<div className="mb-8">
|
|
<h1 className="text-3xl font-bold text-gray-900 mb-4">Reset Password</h1>
|
|
</div>
|
|
|
|
<form className="space-y-6">
|
|
{/* New Password Field */}
|
|
<div className="space-y-2 text-left">
|
|
<Label htmlFor="new-password" className="text-base font-medium text-gray-900">
|
|
New password
|
|
</Label>
|
|
<div className="relative">
|
|
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" />
|
|
<Input
|
|
id="new-password"
|
|
type={showPassword ? "text" : "password"}
|
|
placeholder="Enter your new password"
|
|
value={newPassword}
|
|
onChange={(e) => setNewPassword(e.target.value)}
|
|
onFocus={() => setShowPasswordPolicies(true)}
|
|
className="pl-10 pr-10 bg-gray-50 border border-gray-200 rounded-lg h-12 font-normal"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
|
>
|
|
{showPassword ? (
|
|
<EyeOff className="h-5 w-5" />
|
|
) : (
|
|
<Eye className="h-5 w-5" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Password Policies */}
|
|
{showPasswordPolicies && (
|
|
<motion.div
|
|
initial={{ opacity: 0, height: 0 }}
|
|
animate={{ opacity: 1, height: "auto" }}
|
|
exit={{ opacity: 0, height: 0 }}
|
|
transition={{ duration: 0.2 }}
|
|
className="text-left bg-gray-50 p-4 rounded-lg space-y-2"
|
|
>
|
|
<div className="space-y-1">
|
|
<div className={`flex items-center text-sm ${passwordValidation.hasMinLength ? 'text-green-600' : 'text-gray-600'}`}>
|
|
<div className={`w-1.5 h-1.5 rounded-full mr-2 ${passwordValidation.hasMinLength ? 'bg-green-600' : 'bg-gray-400'}`}></div>
|
|
Contains at least 8 characters
|
|
</div>
|
|
<div className={`flex items-center text-sm ${passwordValidation.hasUppercase ? 'text-green-600' : 'text-gray-600'}`}>
|
|
<div className={`w-1.5 h-1.5 rounded-full mr-2 ${passwordValidation.hasUppercase ? 'bg-green-600' : 'bg-gray-400'}`}></div>
|
|
At least one uppercase letter (A-Z)
|
|
</div>
|
|
<div className={`flex items-center text-sm ${passwordValidation.hasLowercase ? 'text-green-600' : 'text-gray-600'}`}>
|
|
<div className={`w-1.5 h-1.5 rounded-full mr-2 ${passwordValidation.hasLowercase ? 'bg-green-600' : 'bg-gray-400'}`}></div>
|
|
At least one lowercase letter (a-z)
|
|
</div>
|
|
<div className={`flex items-center text-sm ${passwordValidation.hasNumber ? 'text-green-600' : 'text-gray-600'}`}>
|
|
<div className={`w-1.5 h-1.5 rounded-full mr-2 ${passwordValidation.hasNumber ? 'bg-green-600' : 'bg-gray-400'}`}></div>
|
|
At least one number (0-9)
|
|
</div>
|
|
<div className={`flex items-center text-sm ${passwordValidation.hasSpecialChar ? 'text-green-600' : 'text-gray-600'}`}>
|
|
<div className={`w-1.5 h-1.5 rounded-full mr-2 ${passwordValidation.hasSpecialChar ? 'bg-green-600' : 'bg-gray-400'}`}></div>
|
|
At least one special character (e.g., !, @, #, $, %, ^, &, *)
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
|
|
{/* Confirm Password Field */}
|
|
<div className="space-y-2 text-left">
|
|
<Label htmlFor="confirm-password" className="text-base font-medium text-gray-900">
|
|
Confirm Password
|
|
</Label>
|
|
<div className="relative">
|
|
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" />
|
|
<Input
|
|
id="confirm-password"
|
|
type={showConfirmPassword ? "text" : "password"}
|
|
placeholder="Confirm your new password"
|
|
value={confirmPassword}
|
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
className="pl-10 pr-10 bg-gray-50 border border-gray-200 rounded-lg h-12 font-normal"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
|
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
|
>
|
|
{showConfirmPassword ? (
|
|
<EyeOff className="h-5 w-5" />
|
|
) : (
|
|
<Eye className="h-5 w-5" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
{confirmPassword && newPassword !== confirmPassword && (
|
|
<p className="text-sm text-red-600">Passwords do not match</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Continue Button */}
|
|
<Button
|
|
type="submit"
|
|
disabled={!Object.values(passwordValidation).every(Boolean) || newPassword !== confirmPassword}
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
if (Object.values(passwordValidation).every(Boolean) && newPassword === confirmPassword) {
|
|
setIsLoggedIn(true);
|
|
}
|
|
}}
|
|
className="w-full h-12 bg-gray-600 hover:bg-gray-700 disabled:bg-gray-300 disabled:cursor-not-allowed text-white rounded-lg font-medium mt-8"
|
|
>
|
|
Continue
|
|
</Button>
|
|
</form>
|
|
|
|
{/* Back to OTP */}
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowResetPassword(false)}
|
|
className="text-sm font-normal text-blue-600 hover:text-blue-700 mt-6"
|
|
>
|
|
Back to OTP Verification
|
|
</button>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|