Files
edr-platform/apps/edr-freight-web/backoffice/src/shared/components/login/Login.tsx
2026-08-10 12:00:29 +03:00

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)}
/>
)}
</>
);
};