mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 21:50:57 +00:00
333 lines
14 KiB
TypeScript
333 lines
14 KiB
TypeScript
"use client";
|
|
|
|
import type React from "react";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { Button } from "@/shared/common/ui/button";
|
|
import { Input } from "@/shared/common/ui/input";
|
|
import {
|
|
ArrowLeft,
|
|
Eye,
|
|
EyeOff,
|
|
Lock,
|
|
Mail,
|
|
Phone,
|
|
User,
|
|
} from "lucide-react";
|
|
import { useAuthUser } from "@/shared/hooks/useAuthUser";
|
|
import { useTenantConfig } from "@/layout/components/TenantConfig";
|
|
import {
|
|
isValidEmail,
|
|
isValidEthiopianPhoneNum,
|
|
} from "@/record-management/common/editor/Utils";
|
|
import { Link } from "react-router-dom";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useAuth } from "@/shared/context/AuthContext";
|
|
import OTPModal from "./OTPModal";
|
|
import { getRememberMePreference } from "@/shared/utils/authPersistence";
|
|
|
|
export const Login = () => {
|
|
const { login: authLogin, isLoggingIn } = useAuthUser();
|
|
const { config } = useTenantConfig();
|
|
const detectLoginMethod = (value: string): "email" | "phone" | "username" => {
|
|
if (value.includes("@") && value.includes(".com")) return "email";
|
|
if (/^\+?\d+$/.test(value)) return "phone";
|
|
return "username";
|
|
};
|
|
const [otpVerify, setOtpVerify] = useState("");
|
|
const { showOtpModal, setShowOtpModal, setRememberMePreference } = useAuth();
|
|
|
|
const [identifier, setIdentifier] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [passwordType, setPasswordType] = useState("password");
|
|
const [phoneError, setPhoneError] = useState("");
|
|
const [rememberMe, setRememberMe] = useState(false);
|
|
const { t } = useTranslation();
|
|
|
|
useEffect(() => {
|
|
const savedRememberMe = getRememberMePreference();
|
|
setRememberMe(savedRememberMe);
|
|
setRememberMePreference(savedRememberMe);
|
|
}, [setRememberMePreference]);
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setPhoneError("");
|
|
try {
|
|
let processedIdentifier = identifier;
|
|
const loginMethod = detectLoginMethod(identifier);
|
|
if (loginMethod === "phone") {
|
|
if (!isValidEthiopianPhoneNum(processedIdentifier)) {
|
|
setPhoneError(t("auth.err"));
|
|
return;
|
|
}
|
|
|
|
if (processedIdentifier.startsWith("0")) {
|
|
processedIdentifier = "+251" + processedIdentifier.slice(1);
|
|
}
|
|
} else if (loginMethod === "email") {
|
|
if (!isValidEmail(processedIdentifier)) {
|
|
setPhoneError(t("auth.invalidEmail"));
|
|
return;
|
|
}
|
|
}
|
|
// The backend identifies users via the email field regardless of login method
|
|
const payload: { email: string; password: string } = {
|
|
email: processedIdentifier,
|
|
password,
|
|
};
|
|
setOtpVerify(processedIdentifier);
|
|
setRememberMePreference(rememberMe);
|
|
authLogin({ payload, rememberMeValue: rememberMe });
|
|
} catch (error) {
|
|
console.error("Login failed:", error);
|
|
}
|
|
};
|
|
|
|
const getInputIcon = () => {
|
|
const method = detectLoginMethod(identifier);
|
|
switch (method) {
|
|
case "email":
|
|
return (
|
|
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400 dark:text-gray-500" />
|
|
);
|
|
case "phone":
|
|
return (
|
|
<Phone className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400 dark:text-gray-500" />
|
|
);
|
|
case "username":
|
|
return (
|
|
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400 dark:text-gray-500" />
|
|
);
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 p-4 md:p-6 lg:p-8 flex items-center justify-center">
|
|
<Link
|
|
to="/"
|
|
className="fixed top-4 left-4 md:top-6 md:left-6 z-50 flex items-center gap-2 px-4 py-2 bg-white dark:bg-gray-800 rounded-full shadow-md hover:shadow-lg transition-all duration-300 hover:scale-105 text-gray-700 dark:text-gray-200 hover:text-primary group"
|
|
>
|
|
<ArrowLeft className="w-5 h-5 text-gray-600 dark:text-gray-400 transition-transform group-hover:-translate-x-1" />
|
|
<span className="text-sm font-medium hidden sm:inline">
|
|
{t("Back to Home")}
|
|
</span>
|
|
</Link>
|
|
|
|
<div className="w-full max-w-6xl bg-white dark:bg-gray-800 rounded-2xl shadow-2xl overflow-hidden flex flex-col lg:flex-row">
|
|
{/* Left Panel - Login Form */}
|
|
<div className="lg:w-1/2 w-full p-6 sm:p-8 md:p-10 lg:p-12 xl:p-16 flex flex-col justify-center">
|
|
<div className="mb-8">
|
|
<img
|
|
src={config.logo || "/assets/smart-office-logo.svg"}
|
|
alt="Smart Office Logo"
|
|
className="h-10 md:h-12 mb-8"
|
|
/>
|
|
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-3">
|
|
{t("auth.welcomeBack")}
|
|
</h1>
|
|
<p className="text-base text-gray-500 dark:text-gray-400">
|
|
{t("auth.enterCredentials")}
|
|
</p>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
<div className="space-y-4">
|
|
{/* Identifier Input */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
{t("auth.email") +
|
|
" / " +
|
|
t("auth.phoneNumber") +
|
|
" / " +
|
|
t("auth.username")}
|
|
</label>
|
|
<div className="relative">
|
|
{getInputIcon()}
|
|
<Input
|
|
type="text"
|
|
placeholder={
|
|
t("auth.email") +
|
|
" / " +
|
|
t("auth.phoneNumber") +
|
|
" / " +
|
|
t("auth.username")
|
|
}
|
|
className="h-12 rounded-lg border-2 border-gray-200 dark:border-gray-600 px-4 text-sm pl-11 pr-4 focus:border-primary transition-colors text-gray-900 dark:text-white bg-white dark:bg-gray-700 placeholder-gray-500 dark:placeholder-gray-400"
|
|
value={identifier}
|
|
onChange={(e) => {
|
|
setIdentifier(e.target.value.trim());
|
|
if (phoneError) setPhoneError("");
|
|
}}
|
|
required
|
|
/>
|
|
</div>
|
|
{phoneError && (
|
|
<p className="text-red-500 dark:text-red-400 text-sm mt-2 flex items-center gap-1">
|
|
<span className="w-1 h-1 bg-red-500 dark:bg-red-400 rounded-full"></span>
|
|
{phoneError}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Password Input */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
{t("auth.password")}
|
|
</label>
|
|
<div className="relative">
|
|
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400 dark:text-gray-500" />
|
|
<Input
|
|
type={passwordType}
|
|
placeholder={t("auth.password")}
|
|
className="h-12 rounded-lg border-2 border-gray-200 dark:border-gray-600 px-4 text-sm pl-11 pr-4 focus:border-primary transition-colors text-gray-900 dark:text-white bg-white dark:bg-gray-700 placeholder-gray-500 dark:placeholder-gray-400"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
required
|
|
/>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() =>
|
|
setPasswordType(
|
|
passwordType === "password" ? "text" : "password",
|
|
)
|
|
}
|
|
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
|
|
>
|
|
{passwordType === "password" ? (
|
|
<Eye className="w-5 h-5" />
|
|
) : (
|
|
<EyeOff className="w-5 h-5" />
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Remember Me & Forgot Password */}
|
|
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3">
|
|
<label className="flex items-center gap-2 cursor-pointer group">
|
|
<input
|
|
type="checkbox"
|
|
className="w-4 h-4 accent-primary cursor-pointer bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-500"
|
|
checked={rememberMe}
|
|
onChange={(e) => setRememberMe(e.target.checked)}
|
|
/>
|
|
<span className="text-sm text-gray-600 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-gray-200 transition-colors">
|
|
{t("auth.rememberMe")}
|
|
</span>
|
|
</label>
|
|
<Link
|
|
to="/forgot-password"
|
|
className="text-sm text-primary hover:text-primary-700 font-medium transition-colors"
|
|
>
|
|
{t("auth.forgotPassword")}
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Submit Button */}
|
|
<Button
|
|
type="submit"
|
|
className="w-full h-12 bg-primary hover:bg-primary-700 text-white text-base font-medium rounded-lg shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100"
|
|
disabled={isLoggingIn}
|
|
>
|
|
{isLoggingIn ? (
|
|
<span className="flex items-center gap-2">
|
|
<svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
|
|
<circle
|
|
className="opacity-25"
|
|
cx="12"
|
|
cy="12"
|
|
r="10"
|
|
stroke="currentColor"
|
|
strokeWidth="4"
|
|
fill="none"
|
|
/>
|
|
<path
|
|
className="opacity-75"
|
|
fill="currentColor"
|
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
/>
|
|
</svg>
|
|
{t("auth.loggingIn")}
|
|
</span>
|
|
) : (
|
|
t("auth.login")
|
|
)}
|
|
</Button>
|
|
|
|
{/* Sign Up Link */}
|
|
<div className="text-center pt-2 space-y-2">
|
|
<div>
|
|
<span className="text-sm text-gray-600 dark:text-gray-400">
|
|
{t("registration.auth.externalUserPrompt")}{" "}
|
|
</span>
|
|
<Link
|
|
to="/external-portal/signin"
|
|
className="text-sm text-primary hover:text-primary-700 font-medium transition-colors"
|
|
>
|
|
{t("registration.auth.signInWithProvider")}
|
|
</Link>
|
|
</div>
|
|
<div>
|
|
<span className="text-sm text-gray-600 dark:text-gray-400">
|
|
{t("Don't have an account?")}{" "}
|
|
</span>
|
|
<Link
|
|
to="/external-portal/signup"
|
|
className="text-sm text-primary hover:text-primary-700 font-medium transition-colors"
|
|
>
|
|
{t("Sign Up")}
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
{/* Right Panel - Marketing Content */}
|
|
<div className="hidden lg:flex lg:w-1/2 bg-gradient-to-br from-primary to-primary-700 text-white flex-col justify-center p-10 xl:p-16 relative overflow-hidden">
|
|
<div className="absolute top-0 right-0 w-64 h-64 bg-white/10 rounded-full blur-3xl"></div>
|
|
<div className="absolute bottom-0 left-0 w-96 h-96 bg-white/5 rounded-full blur-3xl"></div>
|
|
|
|
<div className="relative z-10">
|
|
<h2 className="text-3xl xl:text-4xl font-bold mb-4 leading-tight">
|
|
{t("auth.welcomeHeadline")} <br /> {t("auth.paperlessOffice")}
|
|
</h2>
|
|
<p className="text-base xl:text-lg mb-8 text-white/90 leading-relaxed">
|
|
{t("auth.welcomeSubtext")}
|
|
</p>
|
|
|
|
{/* Dashboard Preview */}
|
|
<div className="relative w-full max-w-lg mx-auto mt-12">
|
|
<div className="relative rounded-2xl overflow-hidden shadow-2xl border-4 border-white/20">
|
|
<img
|
|
src={
|
|
config.dashboardPreviewImage ||
|
|
"/assets/MainDashboard.png"
|
|
}
|
|
alt="Main Dashboard"
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{showOtpModal && (
|
|
<OTPModal
|
|
isOpen={true}
|
|
onClose={() => setShowOtpModal(false)}
|
|
identifier={otpVerify}
|
|
identifierType={detectLoginMethod(otpVerify)}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
};
|