Files
edr-platform/apps/edr-freight-web/backoffice/src/shared/components/SetPassword.tsx
natib21 22cf8eabd1 fix ui
2026-07-14 07:59:17 +00:00

345 lines
14 KiB
TypeScript

import { useState, useEffect, useMemo } from "react";
import { useSearchParams } from "react-router-dom";
import { Input } from "@/shared/common/ui/input";
import { Button } from "@/shared/common/ui/button";
import { toast } from "sonner";
import { useAuthUser } from "../hooks/useAuthUser";
import { useErrorHandler } from "../hooks/useErrorHandler";
import { Lock, Mail, Eye, EyeOff, RefreshCw, ArrowLeft } from "lucide-react";
import { motion } from "framer-motion";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
const SetPasswordPage = () => {
const navigate = useNavigate();
const { t } = useTranslation();
const { handleError } = useErrorHandler(t);
const [searchParams] = useSearchParams();
const {
setPassword,
isSettingPassword,
setPasswordError,
setPasswordSuccess,
resendVerificationCode,
isResendingCode,
resendError,
resendSuccess,
} = useAuthUser();
const email = searchParams.get("email");
const verificationCode = searchParams.get("verificationCode") || "";
const phoneNumber = searchParams.get("phoneNumber");
const userId = searchParams.get("userId");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [resendAttempted, setResendAttempted] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitAttempted, setSubmitAttempted] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [error, setError] = useState("");
// Password validation rules
const getPasswordValidation = (password: string) => {
return {
minLength: password.length >= 8,
hasLowercase: /[a-z]/.test(password),
hasUppercase: /[A-Z]/.test(password),
hasNumber: /\d/.test(password),
hasSymbol: /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password),
};
};
const passwordValidation = useMemo(
() => getPasswordValidation(newPassword),
[newPassword]
);
const isPasswordStrong = Object.values(passwordValidation).every(Boolean);
const isPasswordMatch = confirmPassword.length > 0 && newPassword === confirmPassword;
const canSubmit = Boolean(newPassword && confirmPassword && isPasswordStrong && isPasswordMatch);
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const password = e.target.value;
setNewPassword(password);
if (error) setError("");
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setSubmitAttempted(true);
setError("");
if (!newPassword || !confirmPassword) {
setError("Please fill in both fields");
return;
}
// Validate password strength
if (!isPasswordStrong) {
const missingRequirements = [];
if (!passwordValidation.minLength) missingRequirements.push("at least 8 characters");
if (!passwordValidation.hasLowercase) missingRequirements.push("one lowercase letter");
if (!passwordValidation.hasUppercase) missingRequirements.push("one uppercase letter");
if (!passwordValidation.hasNumber) missingRequirements.push("one number");
if (!passwordValidation.hasSymbol) missingRequirements.push("one symbol (!@#$%^&*...)");
setError(`Password must contain: ${missingRequirements.join(", ")}`);
return;
}
if (!isPasswordMatch) {
setError("Passwords don't match");
return;
}
// Prepare payload - include userId and email when available
const payload: any = {
verificationCode,
newPassword,
confirmPassword,
};
if (userId) {
payload.userId = userId;
}
if (email) {
payload.email = email;
} else if (phoneNumber) {
// Apply the same phone number formatting logic
let processedPhoneNumber = phoneNumber;
if (processedPhoneNumber.startsWith("0")) {
processedPhoneNumber = "+251" + processedPhoneNumber.slice(1);
}
payload.phoneNumber = processedPhoneNumber;
}
setIsSubmitting(true);
setPassword(payload);
};
const handleResendCode = () => {
if (!phoneNumber && !email) {
toast.error("User ID is missing. Cannot resend verification code.");
return;
}
setResendAttempted(true);
resendVerificationCode({ phoneNumber: phoneNumber || "", email: email || undefined });
};
useEffect(() => {
if (setPasswordSuccess) {
toast.success("Password updated successfully! You can now login");
setIsSubmitting(false);
navigate("/login");
}
}, [setPasswordSuccess, navigate]);
useEffect(() => {
if (!submitAttempted) return;
if (setPasswordError?.message) {
setIsSubmitting(false);
// Use handleError hook to process backend errors
handleError(setPasswordError);
}
}, [setPasswordError?.message, handleError, submitAttempted]);
useEffect(() => {
if (resendSuccess && resendAttempted) {
toast.success("Verification code resent successfully!");
setResendAttempted(false);
}
if (resendError && resendAttempted) {
// Use handleError hook to process backend errors
handleError(resendError);
setResendAttempted(false);
}
}, [resendSuccess, resendError, resendAttempted, handleError]);
return (
<div
className="flex items-center justify-center bg-gray-100 p-4"
style={{ height: "100vh" }}
>
<div className="w-full max-w-md bg-white rounded-xl shadow-lg p-8">
<div className="mb-8 text-center">
<img
src="/assets/smart-office-logo.svg"
alt="Smart Office Logo"
className="h-10 mb-6 mx-auto"
/>
<h1 className="text-2xl font-bold text-gray-900 mb-2">
Set Password
</h1>
<p className="text-sm text-gray-500">
Set a new password for your account
</p>
{email && (
<p className="text-xs text-gray-400 mt-1">Email: {email}</p>
)}
{phoneNumber && (
<p className="text-xs text-gray-400 mt-1">Phone: {phoneNumber}</p>
)}
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-4">
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Lock className="h-5 w-5 text-gray-400" />
</div>
<Input
type={showPassword ? "text" : "password"}
placeholder="New Password"
className="h-10 rounded-md border px-4 text-sm ps-10 text-black placeholder:text-gray-500"
value={newPassword}
onChange={handlePasswordChange}
required
/>
<div
className="absolute inset-y-0 right-0 pr-3 flex items-center cursor-pointer"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeOff className="h-4 w-4 text-gray-400" />
) : (
<Eye className="h-4 w-4 text-gray-400" />
)}
</div>
</div>
{/* Password strength indicator */}
{newPassword && (
<div className="space-y-2 p-3 bg-gray-50 rounded-md border border-gray-200">
<p className="text-xs font-medium text-gray-700">Password Requirements:</p>
<div className="space-y-1">
<div className="flex items-center gap-2">
<div className={`w-4 h-4 rounded-full flex items-center justify-center text-xs ${passwordValidation.minLength ? 'bg-primary-500' : 'bg-gray-300'}`}>
{passwordValidation.minLength && <span className="text-white"></span>}
</div>
<span className={`text-xs ${passwordValidation.minLength ? 'text-primary-700' : 'text-gray-600'}`}>
At least 8 characters
</span>
</div>
<div className="flex items-center gap-2">
<div className={`w-4 h-4 rounded-full flex items-center justify-center text-xs ${passwordValidation.hasUppercase ? 'bg-primary-500' : 'bg-gray-300'}`}>
{passwordValidation.hasUppercase && <span className="text-white"></span>}
</div>
<span className={`text-xs ${passwordValidation.hasUppercase ? 'text-primary-700' : 'text-gray-600'}`}>
One uppercase letter (A-Z)
</span>
</div>
<div className="flex items-center gap-2">
<div className={`w-4 h-4 rounded-full flex items-center justify-center text-xs ${passwordValidation.hasLowercase ? 'bg-primary-500' : 'bg-gray-300'}`}>
{passwordValidation.hasLowercase && <span className="text-white"></span>}
</div>
<span className={`text-xs ${passwordValidation.hasLowercase ? 'text-primary-700' : 'text-gray-600'}`}>
One lowercase letter (a-z)
</span>
</div>
<div className="flex items-center gap-2">
<div className={`w-4 h-4 rounded-full flex items-center justify-center text-xs ${passwordValidation.hasNumber ? 'bg-primary-500' : 'bg-gray-300'}`}>
{passwordValidation.hasNumber && <span className="text-white"></span>}
</div>
<span className={`text-xs ${passwordValidation.hasNumber ? 'text-primary-700' : 'text-gray-600'}`}>
One number (0-9)
</span>
</div>
<div className="flex items-center gap-2">
<div className={`w-4 h-4 rounded-full flex items-center justify-center text-xs ${passwordValidation.hasSymbol ? 'bg-primary-500' : 'bg-gray-300'}`}>
{passwordValidation.hasSymbol && <span className="text-white"></span>}
</div>
<span className={`text-xs ${passwordValidation.hasSymbol ? 'text-primary-700' : 'text-gray-600'}`}>
One symbol (!@#$%^&*...)
</span>
</div>
</div>
</div>
)}
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Lock className="h-5 w-5 text-gray-400" />
</div>
<Input
type={showConfirmPassword ? "text" : "password"}
placeholder="Confirm Password"
className="h-10 rounded-md border px-4 text-sm ps-10 text-black placeholder:text-gray-500"
value={confirmPassword}
onChange={(e) => {
setConfirmPassword(e.target.value);
if (error) setError("");
}}
required
/>
<div
className="absolute inset-y-0 right-0 pr-3 flex items-center cursor-pointer"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
>
{showConfirmPassword ? (
<EyeOff className="h-4 w-4 text-gray-400" />
) : (
<Eye className="h-4 w-4 text-gray-400" />
)}
</div>
</div>
{error && <p className="text-sm text-red-500 mt-1">{error}</p>}
</div>
<div className="flex flex-col gap-4">
<Button
type="submit"
className="w-full h-10 bg-primary hover:bg-primary-300 text-white text-sm"
disabled={isSettingPassword || isSubmitting || !canSubmit}
>
{isSettingPassword ? (
<span className="flex items-center justify-center">
<RefreshCw className="animate-spin h-5 w-5 mr-2" />
Setting Password...
</span>
) : (
"Set Password"
)}
</Button>
<Button
type="button"
variant="outline"
className="w-full h-10 text-sm"
onClick={() => navigate("/")}
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Login
</Button>
</div>
</form>
<div className="mt-6 text-center">
<button
type="button"
onClick={handleResendCode}
disabled={isResendingCode || !email || !phoneNumber || isSubmitting}
className="text-sm text-primary-600 hover:text-primary-800 font-medium flex items-center justify-center space-x-1 mx-auto"
>
{isResendingCode ? (
<>
<RefreshCw className="animate-spin h-4 w-4 text-primary-600 mr-2" />
<span>Sending...</span>
</>
) : (
<>
<Mail className="h-4 w-4 mr-2" />
<span>Resend Verification Code</span>
</>
)}
</button>
</div>
</div>
</div>
);
};
export default SetPasswordPage;