From 7da4bf9f2b728d72a5e981670001c094af04f409 Mon Sep 17 00:00:00 2001 From: hamzaobed7 Date: Wed, 5 Aug 2026 23:27:34 +0300 Subject: [PATCH] fixed owner and customer register make reuseable component and completed it --- app/HelperFunction/UploadImage.js | 20 + app/auth/choose-role/error.js | 2 +- app/auth/choose-role/page.js | 2 + app/components/{ => Animation}/Background.js | 0 .../Animation/HeaderLoginAnimation.js | 70 +++ .../Animation/OwnerPageAnimation.js | 26 + app/components/HavAccount.js | 17 + .../LoginComponent/CreateNewAccount.js | 17 + app/components/LoginComponent/HeaderLogin.js | 45 ++ app/components/LoginComponent/Login.jsx | 127 ++++ app/components/LoginComponent/LoginForm.js | 124 ++++ app/components/LoginComponent/OtpComponent.js | 145 +++++ .../LoginComponent/PolicyAndPrivecy.js | 21 + app/components/LoginComponent/Tabs.js | 36 ++ .../OwnerComponents/ButtomStepOne.js | 25 + .../OwnerComponents/ButtomStepTwo.js | 27 + app/components/OwnerComponents/FormOwner.js | 6 + .../OwnerComponents/HeaderOfForm.js | 44 ++ .../OwnerComponents/HeaderOfSteps.js | 28 + app/components/OwnerComponents/OtpDialog.js | 104 ++++ app/components/OwnerComponents/Owner.js | 170 ++++++ .../OwnerComponents/OwnerFormStepOne.js | 271 ++++++++ .../OwnerComponents/OwnerFormStepTwo.js | 12 + app/components/OwnerComponents/PolicyAgree.js | 31 + app/components/OwnerComponents/UploadImage.js | 85 +++ app/components/home/HomeClient.jsx | 2 - app/login/error.js | 2 +- app/login/page.js | 578 +----------------- app/register/owner/error.js | 12 +- app/register/owner/page.js | 570 +---------------- app/register/tenant/page.js | 407 ++---------- app/services/AuthService.js | 68 ++- app/utils/api.js | 8 +- app/validations/OwnerValidatoin.js | 54 ++ public/NotUsed.txt | 169 +++++ 35 files changed, 1815 insertions(+), 1510 deletions(-) create mode 100644 app/HelperFunction/UploadImage.js rename app/components/{ => Animation}/Background.js (100%) create mode 100644 app/components/Animation/HeaderLoginAnimation.js create mode 100644 app/components/Animation/OwnerPageAnimation.js create mode 100644 app/components/HavAccount.js create mode 100644 app/components/LoginComponent/CreateNewAccount.js create mode 100644 app/components/LoginComponent/HeaderLogin.js create mode 100644 app/components/LoginComponent/Login.jsx create mode 100644 app/components/LoginComponent/LoginForm.js create mode 100644 app/components/LoginComponent/OtpComponent.js create mode 100644 app/components/LoginComponent/PolicyAndPrivecy.js create mode 100644 app/components/LoginComponent/Tabs.js create mode 100644 app/components/OwnerComponents/ButtomStepOne.js create mode 100644 app/components/OwnerComponents/ButtomStepTwo.js create mode 100644 app/components/OwnerComponents/FormOwner.js create mode 100644 app/components/OwnerComponents/HeaderOfForm.js create mode 100644 app/components/OwnerComponents/HeaderOfSteps.js create mode 100644 app/components/OwnerComponents/OtpDialog.js create mode 100644 app/components/OwnerComponents/Owner.js create mode 100644 app/components/OwnerComponents/OwnerFormStepOne.js create mode 100644 app/components/OwnerComponents/OwnerFormStepTwo.js create mode 100644 app/components/OwnerComponents/PolicyAgree.js create mode 100644 app/components/OwnerComponents/UploadImage.js create mode 100644 app/validations/OwnerValidatoin.js create mode 100644 public/NotUsed.txt diff --git a/app/HelperFunction/UploadImage.js b/app/HelperFunction/UploadImage.js new file mode 100644 index 0000000..83e5b60 --- /dev/null +++ b/app/HelperFunction/UploadImage.js @@ -0,0 +1,20 @@ +import toast from "react-hot-toast"; + +export const handleImageUpload = (side, file,t,setIdImagePreviews,setIdImages) => { + if (!file) return; + if (!file.type.startsWith("image/")) { + toast.error(t("register.selectValidImage")); + return; + } + if (file.size > 5 * 1024 * 1024) { + toast.error(t("register.imageSizeLimit")); + return; + } + const reader = new FileReader(); + reader.onloadend = () => { + setIdImagePreviews((prev) => ({ ...prev, [side]: reader.result })); + }; + reader.readAsDataURL(file); + setIdImages((prev) => ({ ...prev, [side]: file })); + toast.success(t("register.imageUploadSuccess"), { style: { background: "#dcfce7", color: "#166534" } }); +}; diff --git a/app/auth/choose-role/error.js b/app/auth/choose-role/error.js index a88abe0..cef795b 100644 --- a/app/auth/choose-role/error.js +++ b/app/auth/choose-role/error.js @@ -6,7 +6,7 @@ import Link from 'next/link'; export default function Error({ error, reset }) { return ( -
+
diff --git a/app/auth/choose-role/page.js b/app/auth/choose-role/page.js index 026d4bd..aa65557 100644 --- a/app/auth/choose-role/page.js +++ b/app/auth/choose-role/page.js @@ -4,6 +4,7 @@ import { motion } from 'framer-motion'; import Link from 'next/link'; import { Home, Building, Briefcase, ArrowLeft } from 'lucide-react'; import { useTranslation } from 'react-i18next'; +import InteractiveBackground from '@/app/components/Animation/Background'; export default function ChooseRolePage() { const { t, i18n } = useTranslation(); @@ -38,6 +39,7 @@ export default function ChooseRolePage() { return (
+
{[...Array(20)].map((_, i) => ( ({ + id: i, + x: Math.random() * 100, + y: Math.random() * 100, + size: Math.random() * 3 + 1, + duration: Math.random() * 15 + 10, + delay: Math.random() * 5, +})); + +export const containerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { staggerChildren: 0.1, delayChildren: 0.2 }, + }, +}; + +export const itemVariants = { + hidden: { y: 20, opacity: 0 }, + visible: { + y: 0, + opacity: 1, + transition: { type: "spring", stiffness: 100 }, + }, +}; + +export default function HeaderAnimation() { + return ( + <> +
+ {particles.map((p) => ( + + ))} +
+ + {/* Glow orbs */} + + + + ); +} diff --git a/app/components/Animation/OwnerPageAnimation.js b/app/components/Animation/OwnerPageAnimation.js new file mode 100644 index 0000000..b17cfd3 --- /dev/null +++ b/app/components/Animation/OwnerPageAnimation.js @@ -0,0 +1,26 @@ +export const fadeInUp = { + initial: { opacity: 0, y: 20 }, + animate: { opacity: 1, y: 0 }, + transition: { duration: 0.5 }, +}; +export const staggerContainer = { + animate: { transition: { staggerChildren: 0.1 } }, +}; +import React from "react"; + +export const BackgroundElements = () => { + const circles = [ + { style: { top: "20%", right: "20%", width: "256px", height: "256px" }, className: "bg-amber-500/5" }, + { style: { bottom: "10%", left: "20%", width: "320px", height: "320px" }, className: "bg-blue-500/5" }, + { style: { bottom: "40%", left: "10%", width: "320px", height: "320px" }, className: "bg-blue-500/5" }, + { style: { bottom: "60%", left: "60%", width: "320px", height: "320px" }, className: "bg-blue-500/5" } + ]; + + return ( + <> + {circles.map((circle, index) => ( +
+ ))} + + ); +}; diff --git a/app/components/HavAccount.js b/app/components/HavAccount.js new file mode 100644 index 0000000..96b451e --- /dev/null +++ b/app/components/HavAccount.js @@ -0,0 +1,17 @@ +import { motion } from "framer-motion"; +import { useTranslation } from "react-i18next"; +import { fadeInUp } from "./Animation/OwnerPageAnimation"; +import Link from "next/link"; +export default function HavAccount() { + const { t } = useTranslation(); + return ( + <> + + {t("register.haveAccount")} + + {t("register.loginLink")} + + + + ); +} diff --git a/app/components/LoginComponent/CreateNewAccount.js b/app/components/LoginComponent/CreateNewAccount.js new file mode 100644 index 0000000..d95908c --- /dev/null +++ b/app/components/LoginComponent/CreateNewAccount.js @@ -0,0 +1,17 @@ +import { motion } from 'framer-motion'; +import { itemVariants } from '../Animation/HeaderLoginAnimation'; +import Link from 'next/link'; +import { useTranslation } from 'react-i18next'; +export default function CreateNewAccount() { +const {t}=useTranslation() + return ( + <> + + {t("noAccount")}{" "} + + {t("createNewAccount")} + + + + ); +} diff --git a/app/components/LoginComponent/HeaderLogin.js b/app/components/LoginComponent/HeaderLogin.js new file mode 100644 index 0000000..ce372a8 --- /dev/null +++ b/app/components/LoginComponent/HeaderLogin.js @@ -0,0 +1,45 @@ +import { motion } from "framer-motion"; +import { itemVariants } from "../Animation/HeaderLoginAnimation"; +import { ArrowLeft, KeyRound,Home } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import Link from "next/link"; +//Back Link +export function BackLink() { + const { t } = useTranslation(); + return ( + <> + + + + + + {t("backToHome")} + + + + ); +} + +export function HeaderForm({ step }) { + const { t } = useTranslation(); + return ( + <> +
+ + + + + + {step === "otp" ? : } + +

SweetHome

+

{step === "otp" ? t("enterOTP") : t("welcomeBack")}

+
+
+ + ); +} diff --git a/app/components/LoginComponent/Login.jsx b/app/components/LoginComponent/Login.jsx new file mode 100644 index 0000000..16cee72 --- /dev/null +++ b/app/components/LoginComponent/Login.jsx @@ -0,0 +1,127 @@ +"use client"; + +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { motion, AnimatePresence } from "framer-motion"; +import toast, { Toaster } from "react-hot-toast"; +import { useRouter } from "next/navigation"; +import AuthService from "../../services/AuthService"; +import InteractiveBackground from "../Animation/Background" +import { BackLink, HeaderForm } from "./HeaderLogin"; +import HeaderAnimation, { containerVariants, itemVariants } from "../Animation/HeaderLoginAnimation"; +import TabsLogin from "./Tabs"; +import LoginForm from "./LoginForm"; +import CreateNewAccount from "./CreateNewAccount"; +import PolicyAndPrivecy from "./PolicyAndPrivecy"; +import OtpComponent from "./OtpComponent"; + +export default function Login() { + const router = useRouter(); + const { t } = useTranslation(); + const [step, setStep] = useState("login"); + const [loginMethod, setLoginMethod] = useState("email"); + const [isLoading, setIsLoading] = useState(false); + const [isSuccess, setIsSuccess] = useState(false); + const [formData, setFormData] = useState({ + credential: "", //email or phoneNumber + password: "", + rememberMe: false, + }); + const [errors, setErrors] = useState({}); + const validateForm = () => { + const newErrors = {}; + if (!formData.credential) { + newErrors.credential = loginMethod === "email" ? t("emailRequired") : t("phoneRequired"); + } + if (!formData.password) { + newErrors.password = t("passwordRequired"); + } + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleLogin = async (e) => { + e.preventDefault(); + if (!validateForm()) return; + setIsLoading(true); + setErrors({}); + try { + const response = await AuthService.login({ + loginMethod, + credential: formData.credential, + password: formData.password, + }); + switch (response.type) { + case "SUCCESS": + toast.success(t("loginSuccess")); + router.push("/"); + break; + case "OTP_REQUIRED": + toast(t("otpRequired")); + setStep("otp"); + break; + default: + toast.error("Login failed the email or password is wrong "); + } + } catch (err) { + console.error("[Login] Error:", err); + toast.error(err.message || t("connectionError"), { + style: { background: "#fee2e2", color: "#991b1b" }, + }); + } finally { + setIsLoading(false); + } + }; + + // Auto-detect login method from input + const handleCredentialChange = (value) => { + setFormData({ ...formData, credential: value }); + if (errors.credential) setErrors({ ...errors, credential: null }); + }; + + const handelTabs = (type) => { + setLoginMethod(type); + setFormData({ ...formData, credential: "" }); + }; + return ( +
+ + + {/* Particles */} + + + {/* Back link */} + + + {/* Header */} + +
+ + {step === "login" ? ( + + {/* Login method tabs */} + + {/* Form Login */} + + + ) : ( + // /* OTP Verification Step */ + + )} + + +
+
+ +
+
+ ); +} diff --git a/app/components/LoginComponent/LoginForm.js b/app/components/LoginComponent/LoginForm.js new file mode 100644 index 0000000..3c2ec1d --- /dev/null +++ b/app/components/LoginComponent/LoginForm.js @@ -0,0 +1,124 @@ +import { EyeOff, Mail, Phone, Eye, LogIn, Lock, Loader2, CheckCircle } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { motion } from "framer-motion"; +import Link from "next/link"; +import { useState } from "react"; + +export default function LoginForm({ loginMethod, errors, formData, isLoading, isSuccess, setFormData, handleCredentialChange }) { + const { t } = useTranslation(); + const [showPassword, setShowPassword] = useState(false); + return ( + <> +
+ +
+
+ {loginMethod === "email" ? ( + + ) : ( + + )} +
+ handleCredentialChange(e.target.value)} + className={`w-full pr-12 pl-4 py-4 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${ + errors.credential ? "border-red-500" : "border-gray-700" + }`} + placeholder={loginMethod === "email" ? t("emailPlaceholder") : t("phonePlaceholder")} + dir="ltr" + /> +
+ {errors.credential && ( + + {errors.credential} + + )} +
+ + {/* Password */} +
+ +
+
+ +
+ { + setFormData({ + ...formData, + password: e.target.value, + }); + if (errors.password) setErrors({ ...errors, password: null }); + }} + className={`w-full pr-12 pl-12 py-4 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${ + errors.password ? "border-red-500" : "border-gray-700" + }`} + placeholder={t("passwordPlaceholder")} + /> + +
+ {errors.password && ( + + {errors.password} + + )} +
+ + {/* Remember + Forgot */} +
+ + + {t("forgotPassword")} + +
+ + {/* Submit */} + + + {isLoading ? ( + <> + + {t("loggingIn")}{" "} + + ) : isSuccess ? ( + <> + {t("success")}{" "} + + ) : ( + <> + {" "} + + {t("login")} + + )} + + + + ); +} diff --git a/app/components/LoginComponent/OtpComponent.js b/app/components/LoginComponent/OtpComponent.js new file mode 100644 index 0000000..e233033 --- /dev/null +++ b/app/components/LoginComponent/OtpComponent.js @@ -0,0 +1,145 @@ +"use client"; + +import { CheckCircle, KeyRound, Loader2, Shield } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { motion } from "framer-motion"; +import { sendEmailOTP, sendPhoneOTP, verifyEmail, verifyPhone, getOwnerByUserId, getCustomerByUserId } from "@/app/utils/api"; +import AuthService from "@/app/services/AuthService"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import toast from "react-hot-toast"; + +export default function OtpComponent({ isLoading, setIsLoading, isSuccess, setIsSuccess, loginMethod, formData, setStep }) { + const { t } = useTranslation(); + const router = useRouter(); + const [otpCode, setOtpCode] = useState(""); + const [otpError, setOtpError] = useState(""); + const handleVerifyOTP = async (e) => { + e.preventDefault(); + if (!otpCode || otpCode.length < 4) { + setOtpError(t("otpRequired")); + return; + } + setIsLoading(true); + setOtpError(""); + try { + const verifyFn = loginMethod === "email" ? verifyEmail : verifyPhone; + const result = await verifyFn(otpCode); + if (result?.ok) { + try { + await AuthService.cacheCurrentUser(getOwnerByUserId, getCustomerByUserId); + console.log("Decoded:", AuthService.decodeToken()); + console.log("User:", AuthService.getUser()); + } catch (err) { + console.warn("Failed to cache user", err); + } + } + setIsSuccess(true); + toast.success(t("verifySuccess")); + setTimeout(() => { + router.push("/"); + }, 1500); + } catch (err) { + console.error("[OTP] Error:", err); + setOtpError(err.message || t("verifyError")); + } finally { + setIsLoading(false); + } + }; + + const resendOTP = async () => { + try { + if (loginMethod === "email") { + await sendEmailOTP(); + } else { + await sendPhoneOTP(); + } + toast.success(t("resendSuccess"), { + style: { background: "#dcfce7", color: "#166534" }, + }); + } catch (err) { + console.error("[OTP] Resend failed:", err); + toast.error(t("resendFailed")); + } + }; + + return ( + +
+ +

+ {t("sendOTPTo")}{" "} + + {formData?.credential} + +

+
+
+ + { + setOtpCode(e.target.value); + if (otpError) setOtpError(""); + }} + className={`w-full px-4 py-4 bg-gray-800 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white text-center text-2xl tracking-[0.5em] placeholder-gray-500 transition-all ${ + otpError ? "border-red-500" : "border-gray-700" + }`} + placeholder="______" + maxLength={6} + dir="ltr" + /> + {otpError && ( + + {otpError} + + )} +
+ + + + {isLoading ? ( + <> + + {t("verifying")} + + ) : isSuccess ? ( + <> + + {t("success")} + + ) : ( + <> + + {t("verify")} + + )} + + + +
+ + +
+
+ ); +} diff --git a/app/components/LoginComponent/PolicyAndPrivecy.js b/app/components/LoginComponent/PolicyAndPrivecy.js new file mode 100644 index 0000000..b54a67b --- /dev/null +++ b/app/components/LoginComponent/PolicyAndPrivecy.js @@ -0,0 +1,21 @@ +import { motion } from "framer-motion"; +import { itemVariants } from "../Animation/HeaderLoginAnimation"; +import { useTranslation } from "react-i18next"; +import Link from "next/link"; +export default function PolicyAndPrivecy() { + const { t } = useTranslation(); + return ( + <> + + {t("agreeTerms")}{" "} + + {t("termsOfUse")} + {" "} + {t("and")}{" "} + + {t("privacyPolicy")} + + + + ); +} diff --git a/app/components/LoginComponent/Tabs.js b/app/components/LoginComponent/Tabs.js new file mode 100644 index 0000000..4eb3abb --- /dev/null +++ b/app/components/LoginComponent/Tabs.js @@ -0,0 +1,36 @@ +import { Mail, Phone } from "lucide-react"; +import { useTranslation } from "react-i18next"; + +export default function TabsLogin({ loginMethod, handelTabs }) { + const { t } = useTranslation(); + return ( + <> +
+ + +
+ + ); +} diff --git a/app/components/OwnerComponents/ButtomStepOne.js b/app/components/OwnerComponents/ButtomStepOne.js new file mode 100644 index 0000000..20ffbaa --- /dev/null +++ b/app/components/OwnerComponents/ButtomStepOne.js @@ -0,0 +1,25 @@ +import { useRouter } from "next/navigation"; +import { useTranslation } from "react-i18next"; +export default function ButtomStepOne({ type }) { + const router = useRouter(); + const { t } = useTranslation(); + return ( + <> + <> + + + + + ); +} diff --git a/app/components/OwnerComponents/ButtomStepTwo.js b/app/components/OwnerComponents/ButtomStepTwo.js new file mode 100644 index 0000000..17e1f72 --- /dev/null +++ b/app/components/OwnerComponents/ButtomStepTwo.js @@ -0,0 +1,27 @@ +import { Loader2 } from "lucide-react"; +import { useTranslation } from "react-i18next"; + +export default function ButtomStepTwo({ setStep, isLoading, formData, type }) { + const { t } = useTranslation(); + return ( + <> + + + + ); +} diff --git a/app/components/OwnerComponents/FormOwner.js b/app/components/OwnerComponents/FormOwner.js new file mode 100644 index 0000000..9371dd4 --- /dev/null +++ b/app/components/OwnerComponents/FormOwner.js @@ -0,0 +1,6 @@ + +export default function FormOwner(){ + return(<> + + ) +} \ No newline at end of file diff --git a/app/components/OwnerComponents/HeaderOfForm.js b/app/components/OwnerComponents/HeaderOfForm.js new file mode 100644 index 0000000..25bef45 --- /dev/null +++ b/app/components/OwnerComponents/HeaderOfForm.js @@ -0,0 +1,44 @@ +"use client"; + +import { useTranslation } from "react-i18next"; +import { motion } from "framer-motion"; +import { Building, Home } from "lucide-react"; + +const data = [ + { + colors: "from-amber-500 to-amber-600", + titleKey: "register.owner.step1Title", + title2Key: "register.owner.step1bTitle", + descKey: "register.owner.step1Desc", + desc2Key: "register.owner.step1bDesc", + icon: , + }, + { + colors: "from-blue-500 to-blue-600", + titleKey: "register.tenant.step1Title", + title2Key: "register.tenant.step2Title", + descKey: "register.tenant.step1Desc", + desc2Key: "register.tenant.step2Desc", + icon: , + }, +]; +export default function HeaderOfForm({ step = 1, num = 0 }) { + const { t } = useTranslation(); + const currentData = data[num] || data[0]; + return ( +
+ + + + {data[num].icon} + +

{t(step === 1 ? t(currentData.titleKey) : t(currentData.title2Key))}

+

{t(step === 1 ? t(currentData.descKey) : t(currentData.desc2Key))}

+
+
+ ); +} diff --git a/app/components/OwnerComponents/HeaderOfSteps.js b/app/components/OwnerComponents/HeaderOfSteps.js new file mode 100644 index 0000000..38427dd --- /dev/null +++ b/app/components/OwnerComponents/HeaderOfSteps.js @@ -0,0 +1,28 @@ +import Link from "next/link"; +import { motion } from "framer-motion"; +import { ArrowLeft } from "lucide-react"; +import { useTranslation } from "react-i18next"; + +export default function HeaderOfSteps({ step, colors, numberStep }) { + const { t } = useTranslation(); + return ( + <> +
+
+ + + + + {t("forgotPassword.backToLogin")} + + {t("register.stepOf2", { step: step })} +
+
+ {numberStep.map((s) => ( + = s ? colors : "bg-gray-800"}`} animate={{ scaleX: step >= s ? 1 : 0.5 }} /> + ))} +
+
+ + ); +} diff --git a/app/components/OwnerComponents/OtpDialog.js b/app/components/OwnerComponents/OtpDialog.js new file mode 100644 index 0000000..5e488b3 --- /dev/null +++ b/app/components/OwnerComponents/OtpDialog.js @@ -0,0 +1,104 @@ +import { KeyRound, Loader2, Shield } from "lucide-react"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { motion } from "framer-motion"; +import toast, { Toaster } from "react-hot-toast"; +import { sendEmailOTP, verifyEmail } from "@/app/utils/api"; +import AuthService from "@/app/services/AuthService"; +import { useRouter } from "next/navigation"; +export default function OtpDialog({ formData, setShowOtpModal, type }) { + const { t } = useTranslation(); + const router = useRouter(); + const [isLoading, setIsLoading] = useState(false); + const [otpCode, setOtpCode] = useState(""); + const handleVerifyOTP = async () => { + if (!otpCode || otpCode.length < 4) { + toast.error(t("register.otpRequired")); + return; + } + setIsLoading(true); + try { + const res = await verifyEmail(otpCode); + if (res.status === 200) { + AuthService.deleteToken(); + toast.success(res.message || t("register.emailVerified")); + setShowOtpModal(false); + setTimeout(() => router.push("/login"), 1500); + } else { + toast.error(res.message || res.data?.message || t("accountVerification.otpInvalid")); + } + } catch (err) { + toast.error(err.message || t("register.verificationError")); + } finally { + setIsLoading(false); + } + }; + + const handleResendOTP = async () => { + setIsLoading(true); + try { + await sendEmailOTP(); + toast.success(t("register.newOtpSent")); + } catch (err) { + toast.error(t("register.resendOtpFailed")); + } finally { + setIsLoading(false); + } + }; + + return ( + <> + + +
+
+ +
+

{t("register.otpTitle")}

+

{t("register.otpSentTo")}

+

{formData.email}

+
+
+ +
+
+ +
+ setOtpCode(e.target.value)} + className="w-full pr-12 pl-4 py-3 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 text-white text-center tracking-[0.5em] text-xl" + placeholder="------" + /> +
+
+
+ +
+ +
+
+ + ); +} diff --git a/app/components/OwnerComponents/Owner.js b/app/components/OwnerComponents/Owner.js new file mode 100644 index 0000000..52d8daa --- /dev/null +++ b/app/components/OwnerComponents/Owner.js @@ -0,0 +1,170 @@ +"use client"; + +import { useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { useTranslation } from "react-i18next"; +import toast, { Toaster } from "react-hot-toast"; +import { addOwner, loginWithEmail } from "../../utils/api"; +import AuthService from "../../services/AuthService"; +import { OwnerType } from "../../enums"; +import OtpDialog from "./OtpDialog"; +import HeaderOfSteps from "./HeaderOfSteps"; +import HeaderOfForm from "./HeaderOfForm"; +import { BackgroundElements, fadeInUp, staggerContainer } from "../Animation/OwnerPageAnimation"; +import { validateStep1, validateStep2 } from "../../validations/OwnerValidatoin"; +import OwnerFormStepOne from "./OwnerFormStepOne"; +import ButtomStepOne from "./ButtomStepOne"; +import ButtomStepTwo from "./ButtomStepTwo"; +import OwnerFormStepTwo from "./OwnerFormStepTwo"; +import InteractiveBackground from "../Animation/Background"; + +export default function Owner() { + const { t } = useTranslation(); + const [step, setStep] = useState(1); + const [showOtpModal, setShowOtpModal] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [formData, setFormData] = useState({ + firstName: "", + lastName: "", + email: "", + phone: "", + whatsapp: "", + phone2: "", + nationalNumber: "", + password: "", + confirmPassword: "", + ownerType: OwnerType.PERSON, + agreeTerms: false, + }); + const [idImages, setIdImages] = useState({ front: null, back: null, license: null }); + const [idImagePreviews, setIdImagePreviews] = useState({ front: "", back: "", license: "" }); + const [errors, setErrors] = useState({}); + + const isCompany = Number(formData.ownerType) === OwnerType.REAL_ESTATE_AGENCY; + + const handleNextStep = () => { + if (validateStep1(formData, setErrors, t)) { + setStep(2); + window.scrollTo({ top: 0, behavior: "smooth" }); + } else { + toast.error(t("validation.correctErrors")); + } + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + if (!validateStep2(idImages, setErrors, t)) { + toast.error(t("register.completeRequiredImages")); + return; + } + if (!formData.agreeTerms) { + toast.error(t("validation.agreeToTerms")); + return; + } + setIsLoading(true); + const payload = { + firstName: formData.firstName, + lastName: formData.lastName, + email: formData.email, + phoneNumber: formData.phone || "", + whatsAppNumber: formData.whatsapp, + phone: formData.phone2, + nationalNumber: formData.nationalNumber, + password: formData.password, + ownerType: formData.ownerType, + }; + + try { + const licenseImage = isCompany ? idImages.license : null; + const res = await addOwner(payload, idImages.front, idImages.back); + if (res.status === 200 || res.ok) { + toast.success(res.message || t("register.accountCreated"), { duration: 4000 }); + const loginRes = await loginWithEmail(formData.email, formData.password); + if (loginRes.status === 206) { + const otpToken = loginRes.data; + if (otpToken) AuthService.addToken(otpToken); + toast(loginRes.message || t("register.otpSentEmail"), { icon: "📧" }); + setShowOtpModal(true); + } else if (loginRes.status === 200) { + const loginToken = loginRes.data; + if (loginToken) AuthService.addToken(loginToken); + toast.success(loginRes.message || t("register.loginSuccess")); + } + } else { + toast.error(res.data[0].message || res.data?.message); + } + } catch (err) { + toast.error(err.message || t("register.registrationError")); + } finally { + setIsLoading(false); + } + }; + return ( +
+ +
+ + +
+ + + + +
+ { + e.preventDefault(); + handleNextStep(); + } + : handleSubmit + } + className="space-y-6" + > + {step === 1 && ( + + )} + {step === 2 && ( + <> + + + )} + + {step === 1 ? ( + <> + + + ) : ( + <> + + + )} + + +
+
+
+ {showOtpModal && } +
+ ); +} diff --git a/app/components/OwnerComponents/OwnerFormStepOne.js b/app/components/OwnerComponents/OwnerFormStepOne.js new file mode 100644 index 0000000..642518f --- /dev/null +++ b/app/components/OwnerComponents/OwnerFormStepOne.js @@ -0,0 +1,271 @@ +import { useTranslation } from "react-i18next"; +import { fadeInUp } from "../Animation/OwnerPageAnimation"; +import { motion, AnimatePresence } from "framer-motion"; +import { User, Mail, Phone, Lock, Eye, EyeOff, MessageCircle, Camera, X, CheckCircle, XCircle } from "lucide-react"; +import { CustomerTypeLabels, OwnerTypeLabels } from "@/app/enums"; +import { useRef, useState } from "react"; +import { handleImageUpload } from "@/app/HelperFunction/UploadImage"; +import UploadImage from "./UploadImage"; + +export default function OwnerFormStepOne({ formData, errors, setFormData, setErrors, isCompany, idImagePreviews, setIdImagePreviews, setIdImages, type }) { + const { t } = useTranslation(); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const fileInputLicenseRef = useRef(null); + return ( + <> + +
+ +
+
+ +
+ { + setFormData({ ...formData, firstName: e.target.value }); + setErrors({ ...errors, firstName: null }); + }} + className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${errors.firstName ? "border-red-500" : "border-gray-700"}`} + placeholder={t("register.firstName")} + /> +
+ {errors.firstName &&

{errors.firstName}

} +
+
+ + { + setFormData({ ...formData, lastName: e.target.value }); + setErrors({ ...errors, lastName: null }); + }} + className={`w-full px-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${errors.lastName ? "border-red-500" : "border-gray-700"}`} + placeholder={t("register.lastName")} + /> + {errors.lastName &&

{errors.lastName}

} +
+
+ + + +
+
+ +
+ { + setFormData({ ...formData, email: e.target.value }); + setErrors({ ...errors, email: null }); + }} + className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${errors.email ? "border-red-500" : "border-gray-700"}`} + placeholder={t("register.emailPlaceholder")} + /> +
+ {errors.email &&

{errors.email}

} +
+ + + +
+
+ +
+ { + setFormData({ ...formData, phone: e.target.value }); + setErrors({ ...errors, phone: null }); + }} + className="w-full pr-12 pl-4 py-3 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all" + placeholder={t("register.phoneOptionalPlaceholder")} + /> +
+ {errors.phone &&

{errors.phone}

} +
+ + + +
+
+ +
+ { + setFormData({ ...formData, whatsapp: e.target.value }); + setErrors({ ...errors, whatsapp: null }); + }} + className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${errors.whatsapp ? "border-red-500" : "border-gray-700"}`} + placeholder={t("register.whatsappPlaceholder")} + /> +
+ {errors.whatsapp &&

{errors.whatsapp}

} +
+ + + +
+
+ +
+ { + setFormData({ ...formData, phone2: e.target.value }); + setErrors({ ...errors, phone2: null }); + }} + className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${errors.phone2 ? "border-red-500" : "border-gray-700"}`} + placeholder={t("register.phoneSecondaryPlaceholder")} + maxLength={7} + /> +
+ {errors.phone2 &&

{errors.phone2}

} +
+ + + +
+
+ +
+ { + setFormData({ ...formData, nationalNumber: e.target.value }); + setErrors({ ...errors, nationalNumber: null }); + }} + className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${errors.nationalNumber ? "border-red-500" : "border-gray-700"}`} + placeholder={t("register.nationalNumberPlaceholder")} + /> +
+ {errors.nationalNumber &&

{errors.nationalNumber}

} +
+ + + {type === "owner" ? ( + <> + + + + ) : ( + <> + + + + + + )} + + + + {isCompany && } + + + + +
+
+ +
+ { + setFormData({ ...formData, password: e.target.value }); + setErrors({ ...errors, password: null }); + }} + className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${errors.password ? "border-red-500" : "border-gray-700"}`} + placeholder={t("register.passwordPlaceholder")} + /> + +
+ {errors.password &&

{errors.password}

} +
+ + + +
+
+ +
+ { + setFormData({ ...formData, confirmPassword: e.target.value }); + setErrors({ ...errors, confirmPassword: null }); + }} + className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${errors.confirmPassword ? "border-red-500" : "border-gray-700"}`} + placeholder={t("register.confirmPasswordPlaceholder")} + /> + + {formData.confirmPassword && ( +
+ {formData.password === formData.confirmPassword ? : } +
+ )} +
+ {errors.confirmPassword &&

{errors.confirmPassword}

} +
+ + ); +} diff --git a/app/components/OwnerComponents/OwnerFormStepTwo.js b/app/components/OwnerComponents/OwnerFormStepTwo.js new file mode 100644 index 0000000..c000f3a --- /dev/null +++ b/app/components/OwnerComponents/OwnerFormStepTwo.js @@ -0,0 +1,12 @@ +import Policy from "./PolicyAgree"; +import UploadImage from "./UploadImage"; +export default function OwnerFormStepTwo({idImagePreviews,errors,setIdImagePreviews,setIdImages,setFormData,formData}) { + + return ( + <> + + + + + ); +} diff --git a/app/components/OwnerComponents/PolicyAgree.js b/app/components/OwnerComponents/PolicyAgree.js new file mode 100644 index 0000000..9699202 --- /dev/null +++ b/app/components/OwnerComponents/PolicyAgree.js @@ -0,0 +1,31 @@ +import { motion } from "framer-motion"; +import { fadeInUp } from "../Animation/OwnerPageAnimation"; +import Link from "next/link"; +import { useTranslation } from "react-i18next"; +export default function Policy({ formData, setFormData, type }) { + const { t } = useTranslation(); + return ( + <> + + setFormData({ ...formData, agreeTerms: e.target.checked })} + className={`w-4 h-4 rounded border-gray-600 bg-white/5 ${type === "owner" ? `text-amber-500 focus:ring-amber-500` : ` text-blue-500 focus:ring-blue-500`} `} + required + /> + + + + ); +} diff --git a/app/components/OwnerComponents/UploadImage.js b/app/components/OwnerComponents/UploadImage.js new file mode 100644 index 0000000..23780ac --- /dev/null +++ b/app/components/OwnerComponents/UploadImage.js @@ -0,0 +1,85 @@ +"use client"; + +import { useTranslation } from "react-i18next"; +import { fadeInUp } from "../Animation/OwnerPageAnimation"; +import { handleImageUpload } from "@/app/HelperFunction/UploadImage"; +import { motion } from "framer-motion"; +import Image from "next/image"; +import { Camera, X } from "lucide-react"; +import { useRef } from "react"; + +export default function UploadImage({ idImagePreviews, type, errors, setIdImagePreviews, setIdImages }) { + const { t } = useTranslation(); + const fileInputRef = useRef(null); + const currentPreview = idImagePreviews?.[type]; + const handleRemoveImage = (e) => { + e.stopPropagation(); + setIdImages((prev) => ({ ...prev, [type]: null })); + setIdImagePreviews((prev) => ({ ...prev, [type]: "" })); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }; + + const getLabel = () => { + switch (type) { + case "front": + return t("register.owner.frontIdLabel"); + case "back": + return t("register.owner.backIdLabel"); + case "license": + return t("register.owner.licenseImageLabel"); + default: + return ""; + } + }; + + const getClickText = () => { + return type === "license" ? t("register.owner.clickUploadLicense") : t("register.owner.clickUploadImage"); + }; + + const getAltText = () => { + switch (type) { + case "front": + return t("register.uploadFrontAlt"); + case "back": + return t("register.uploadBackAlt"); + case "license": + return t("register.uploadLicenseAlt"); + default: + return "Preview Image"; + } + }; + + return ( + + + {type === "license" &&
{t("register.owner.licenseHint")}
} +
fileInputRef.current?.click()} + className={`relative border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all ${ + currentPreview ? "border-green-500 bg-green-500/10" : errors ? "border-red-500 bg-red-500/10" : "border-gray-700 hover:border-amber-500 hover:bg-white/5" + }`} + > + handleImageUpload(type, e.target.files?.[0], t, setIdImagePreviews, setIdImages)} className="hidden" /> + {currentPreview ? ( +
+ {getAltText()} + +
+ ) : ( + <> + +

{getClickText()}

+

JPEG, PNG, JPG • {t("register.upTo5MB")}

+ + )} +
+ {errors &&

{errors}

} +
+ ); +} diff --git a/app/components/home/HomeClient.jsx b/app/components/home/HomeClient.jsx index 1841fef..20269a5 100644 --- a/app/components/home/HomeClient.jsx +++ b/app/components/home/HomeClient.jsx @@ -14,10 +14,8 @@ export default function HomeClient() { const mapSectionRef = useRef(null); const [showMap, setShowMap] = useState(false); const [isScrolling, setIsScrolling] = useState(false); - const { filteredProperties, applyFilters, resetFilters } = useApplyFilters(); const { name, isOwner, isAuthenticated, role } = useAuth(); - console.log(isOwner, name, isAuthenticated, role); const handleSearch = (filters) => { applyFilters(filters); if (!showMap) { diff --git a/app/login/error.js b/app/login/error.js index a88abe0..cef795b 100644 --- a/app/login/error.js +++ b/app/login/error.js @@ -6,7 +6,7 @@ import Link from 'next/link'; export default function Error({ error, reset }) { return ( -
+
diff --git a/app/login/page.js b/app/login/page.js index 4158f11..63b9acb 100644 --- a/app/login/page.js +++ b/app/login/page.js @@ -1,574 +1,8 @@ -"use client"; - -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { motion, AnimatePresence } from "framer-motion"; -import toast, { Toaster } from "react-hot-toast"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; -import { Mail, Lock, Eye, EyeOff, ArrowLeft, LogIn, CheckCircle, Loader2, Home, Shield, Phone, KeyRound } from "lucide-react"; -import { loginWithEmail, loginWithPhone, sendEmailOTP, sendPhoneOTP, verifyEmail, verifyPhone, getOwnerByUserId, getCustomerByUserId } from "../utils/api"; -import AuthService from "../services/AuthService"; -import Background from "../components/Background"; - +import Login from "../components/LoginComponent/Login"; +export const metadata = { + title: "صفحة تسجيل الدخول ", + describtion: "صفحة تسجيل الدخول في Sweet Home", +}; export default function LoginPage() { - const router = useRouter(); - const { t } = useTranslation(); - - // Step: 'login' | 'otp' - const [step, setStep] = useState("login"); - const [loginMethod, setLoginMethod] = useState("email"); // 'email' | 'phone' - const [showPassword, setShowPassword] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [isSuccess, setIsSuccess] = useState(false); - - const [formData, setFormData] = useState({ - credential: "", - password: "", - rememberMe: false, - }); - - const [otpCode, setOtpCode] = useState(""); - const [otpError, setOtpError] = useState(""); - const [errors, setErrors] = useState({}); - - const validateForm = () => { - const newErrors = {}; - if (!formData.credential) { - newErrors.credential = loginMethod === "email" ? t("emailRequired") : t("phoneRequired"); - // } else if (loginMethod === 'email' && !isEmail(formData.credential)) { - // newErrors.credential = 'البريد الإلكتروني غير صالح'; - // } else if (loginMethod === 'phone' && !isPhoneNumber(formData.credential)) { - newErrors.credential = t("phoneInvalid"); - } - - if (!formData.password) { - newErrors.password = t("passwordRequired"); - } - - setErrors(newErrors); - return Object.keys(newErrors).length === 0; - }; - - const handleLogin = async (e) => { - e.preventDefault(); - if (!validateForm()) return; - - setIsLoading(true); - setErrors({}); - - try { - const loginFn = loginMethod === "email" ? loginWithEmail : loginWithPhone; - - const result = await loginFn(formData.credential, formData.password); - - if (result.status === 200) { - const token = typeof result.data === "string" ? result.data : result.data?.token || result.data?.accessToken; - AuthService.addToken(token); - - // Fetch user profile to get full name - const authUser = AuthService.getUser(); - if (authUser?.id) { - try { - const isOwner = AuthService.isOwner(); - const fetchFn = isOwner ? getOwnerByUserId : getCustomerByUserId; - const profile = await fetchFn(authUser.id); - if (profile) { - AuthService.cacheUser({ - name: profile.fullName || profile.name || `${profile.firstName || ""} ${profile.lastName || ""}`.trim(), - email: profile.email || authUser.email, - phone: profile.phone || profile.phoneNumber || authUser.phone, - }); - } - } catch (err) { - console.warn("[Login] Failed to fetch profile:", err); - } - } - - const userRole = AuthService.isOwner() ? "owner" : "customer"; - - setIsSuccess(true); - toast.success(t("loginSuccess"), { - style: { background: "#dcfce7", color: "#166534" }, - }); - - setTimeout(() => { - router.push("/"); - }, 1500); - } else if (result.status === 206) { - const tempToken = typeof result.data === "string" ? result.data : result.data?.token || result.data?.accessToken; - if (tempToken) { - AuthService.addToken(tempToken); - } - toast(t("otpRequired"), { - icon: "🔐", - style: { background: "#fef3c7", color: "#92400e" }, - }); - - // Send OTP - try { - if (loginMethod === "email") { - await sendEmailOTP(); - } else { - await sendPhoneOTP(); - } - } catch (otpErr) { - console.warn("[Login] OTP send failed, proceeding anyway:", otpErr); - } - - setStep("otp"); - } else if (result.status >= 500) { - console.error("[Login] Server error:", result.status, result.data); - toast.error(t("serverError"), { - style: { background: "#fee2e2", color: "#991b1b" }, - }); - } else if (result.status === 404) { - console.error("[Login] API endpoint not found:", result.status); - toast.error(t("apiNotFound"), { - style: { background: "#fee2e2", color: "#991b1b" }, - }); - } else if (result.status === 429) { - toast.error(t("tooManyRequests"), { - style: { background: "#fee2e2", color: "#991b1b" }, - }); - } else { - console.error("[Login] Unexpected status:", result.status, result.data); - toast.error(result.data?.message || result.data || t("invalidCredentials"), { - style: { background: "#fee2e2", color: "#991b1b" }, - }); - } - } catch (err) { - console.error("[Login] Error:", err); - toast.error(err.message || t("connectionError"), { - style: { background: "#fee2e2", color: "#991b1b" }, - }); - } finally { - setIsLoading(false); - } - }; - - const handleVerifyOTP = async (e) => { - e.preventDefault(); - if (!otpCode || otpCode.length < 4) { - setOtpError(t("otpRequired")); - return; - } - - setIsLoading(true); - setOtpError(""); - - try { - const verifyFn = loginMethod === "email" ? verifyEmail : verifyPhone; - - const result = await verifyFn(otpCode); - - if (result.ok) { - const finalToken = typeof result.data === "string" ? result.data : result.data?.token || result.data?.accessToken; - if (finalToken && typeof finalToken === "string") { - AuthService.addToken(finalToken); - } - - setIsSuccess(true); - toast.success(t("verifySuccess"), { - style: { background: "#dcfce7", color: "#166534" }, - }); - - setTimeout(() => { - router.push("/"); - }, 1500); - } else { - console.error("[OTP] Verification failed:", result.data); - setOtpError(result.data?.message || t("wrongOTP")); - } - } catch (err) { - console.error("[OTP] Error:", err); - setOtpError(err.message || t("verifyError")); - } finally { - setIsLoading(false); - } - }; - - const resendOTP = async () => { - try { - if (loginMethod === "email") { - await sendEmailOTP(); - } else { - await sendPhoneOTP(); - } - toast.success(t("resendSuccess"), { - style: { background: "#dcfce7", color: "#166534" }, - }); - } catch (err) { - console.error("[OTP] Resend failed:", err); - toast.error(t("resendFailed")); - } - }; - - // Auto-detect login method from input - const handleCredentialChange = (value) => { - setFormData({ ...formData, credential: value }); - if (errors.credential) setErrors({ ...errors, credential: null }); - - // Auto-switch method - // if (isEmail(value)) { - // setLoginMethod('email'); - // } else if (isPhoneNumber(value)) { - // setLoginMethod('phone'); - // } - }; - - const particles = Array.from({ length: 20 }, (_, i) => ({ - id: i, - x: Math.random() * 100, - y: Math.random() * 100, - size: Math.random() * 3 + 1, - duration: Math.random() * 15 + 10, - delay: Math.random() * 5, - })); - - const containerVariants = { - hidden: { opacity: 0 }, - visible: { - opacity: 1, - transition: { staggerChildren: 0.1, delayChildren: 0.2 }, - }, - }; - - const itemVariants = { - hidden: { y: 20, opacity: 0 }, - visible: { - y: 0, - opacity: 1, - transition: { type: "spring", stiffness: 100 }, - }, - }; - - return ( -
- - - {/* Particles */} -
- {particles.map((p) => ( - - ))} -
- - {/* Glow orbs */} - - - - - {/* Back link */} - - - - - - {t("backToHome")} - - - - - {/* Header */} -
- - - - - - {step === "otp" ? : } - -

SweetHome

-

{step === "otp" ? t("enterOTP") : t("welcomeBack")}

-
-
- -
- - {step === "login" ? ( - - {/* Login method tabs */} -
- - -
- - {/* Credential input */} -
- -
-
- {loginMethod === "email" ? ( - - ) : ( - - )} -
- handleCredentialChange(e.target.value)} - className={`w-full pr-12 pl-4 py-4 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${ - errors.credential ? "border-red-500" : "border-gray-700" - }`} - placeholder={loginMethod === "email" ? t("emailPlaceholder") : t("phonePlaceholder")} - dir="ltr" - /> -
- {errors.credential && ( - - {errors.credential} - - )} -
- - {/* Password */} -
- -
-
- -
- { - setFormData({ - ...formData, - password: e.target.value, - }); - if (errors.password) setErrors({ ...errors, password: null }); - }} - className={`w-full pr-12 pl-12 py-4 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${ - errors.password ? "border-red-500" : "border-gray-700" - }`} - placeholder={t("passwordPlaceholder")} - /> - -
- {errors.password && ( - - {errors.password} - - )} -
- - {/* Remember + Forgot */} -
- - - {t("forgotPassword")} - -
- - {/* Submit */} - - - {isLoading ? ( - <> - - {t("loggingIn")} - - ) : isSuccess ? ( - <> - - {t("success")} - - ) : ( - <> - - {t("login")} - - )} - - -
- ) : ( - /* OTP Verification Step */ - -
- -

- {t("sendOTPTo")}{" "} - - {formData.credential} - -

-
- -
- - { - setOtpCode(e.target.value); - if (otpError) setOtpError(""); - }} - className={`w-full px-4 py-4 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white text-center text-2xl tracking-[0.5em] placeholder-gray-500 transition-all ${ - otpError ? "border-red-500" : "border-gray-700" - }`} - placeholder="______" - maxLength={6} - dir="ltr" - /> - {otpError && ( - - {otpError} - - )} -
- - - - {isLoading ? ( - <> - - {t("verifying")} - - ) : isSuccess ? ( - <> - - {t("success")} - - ) : ( - <> - - {t("verify")} - - )} - - - -
- - -
-
- )} -
- - - {t("noAccount")}{" "} - - {t("createNewAccount")} - - -
-
- - - {t("agreeTerms")}{" "} - - {t("termsOfUse")} - {" "} - {t("and")}{" "} - - {t("privacyPolicy")} - - -
-
- ); + return ; } diff --git a/app/register/owner/error.js b/app/register/owner/error.js index a88abe0..b1859ff 100644 --- a/app/register/owner/error.js +++ b/app/register/owner/error.js @@ -1,17 +1,17 @@ -'use client'; +"use client"; -import { motion } from 'framer-motion'; -import { AlertTriangle, RefreshCw, Home } from 'lucide-react'; -import Link from 'next/link'; +import { motion } from "framer-motion"; +import { AlertTriangle, RefreshCw, Home } from "lucide-react"; +import Link from "next/link"; export default function Error({ error, reset }) { return ( -
+
-

حدث خطأ

+

حدث خطأ

نعتذر، حدث خطأ أثناء تحميل الصفحة

-
- ) : (<>

{t('register.owner.clickUploadLicense')}

JPEG, PNG, JPG • {t('register.upTo5MB')}

)} -
- - )} - - - - -
-
- -
- { setFormData({...formData, password: e.target.value}); setErrors({...errors, password: null}); }} - className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.password ? 'border-red-500' : 'border-gray-700'}`} - placeholder={t('register.passwordPlaceholder')} /> - -
- {errors.password &&

{errors.password}

} -
- - - -
-
- -
- { setFormData({...formData, confirmPassword: e.target.value}); setErrors({...errors, confirmPassword: null}); }} - className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.confirmPassword ? 'border-red-500' : 'border-gray-700'}`} - placeholder={t('register.confirmPasswordPlaceholder')} /> - - {formData.confirmPassword && ( -
- {formData.password === formData.confirmPassword ? : } -
- )} -
- {errors.confirmPassword &&

{errors.confirmPassword}

} -
- - )} - - {step === 2 && ( - <> - - -
fileInputFrontRef.current?.click()} - className={`relative border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all ${idImagePreviews.front ? 'border-green-500 bg-green-500/10' : errors.front ? 'border-red-500 bg-red-500/10' : 'border-gray-700 hover:border-amber-500 hover:bg-white/5'}`}> - handleImageUpload('front', e.target.files?.[0])} className="hidden" /> - {idImagePreviews.front ? ( -
- {t('register.uploadFrontAlt')} - -
- ) : (<>

{t('register.owner.clickUploadImage')}

JPEG, PNG, JPG • {t('register.upTo5MB')}

)} -
- {errors.front &&

{errors.front}

} -
- - - -
fileInputBackRef.current?.click()} - className={`relative border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all ${idImagePreviews.back ? 'border-green-500 bg-green-500/10' : errors.back ? 'border-red-500 bg-red-500/10' : 'border-gray-700 hover:border-amber-500 hover:bg-white/5'}`}> - handleImageUpload('back', e.target.files?.[0])} className="hidden" /> - {idImagePreviews.back ? ( -
- {t('register.uploadBackAlt')} - -
- ) : (<>

{t('register.owner.clickUploadImage')}

JPEG, PNG, JPG • {t('register.upTo5MB')}

)} -
- {errors.back &&

{errors.back}

} -
- - - setFormData({...formData, agreeTerms: e.target.checked})} - className="w-4 h-4 rounded border-gray-600 bg-white/5 text-amber-500 focus:ring-amber-500" required /> - - - - )} - - - {step === 1 ? ( - <> - - - - ) : ( - <> - - - - )} - - -
- - - - - {showOtpModal && ( - - -
-
- -
-

{t('register.otpTitle')}

-

{t('register.otpSentTo')}

-

{formData.email}

-
-
- -
-
- -
- setOtpCode(e.target.value)} - className="w-full pr-12 pl-4 py-3 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 text-white text-center tracking-[0.5em] text-xl" - placeholder="------" /> -
-
-
- -
- -
-
- )} -
-
+ <> + + ); } diff --git a/app/register/tenant/page.js b/app/register/tenant/page.js index 08d4bb2..d795c12 100644 --- a/app/register/tenant/page.js +++ b/app/register/tenant/page.js @@ -1,87 +1,60 @@ +"use client"; -'use client'; - -import { useState, useEffect, useMemo } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { useRouter } from 'next/navigation'; -import Link from 'next/link'; -import { useTranslation } from 'react-i18next'; -import { - User, Mail, Phone, Lock, Eye, EyeOff, - CheckCircle, XCircle, ArrowLeft, Home, Loader2, - Shield, KeyRound -} from 'lucide-react'; -import toast, { Toaster } from 'react-hot-toast'; -import { addCustomer, loginWithEmail, sendEmailOTP, verifyEmail } from '../../utils/api'; -import AuthService from '../../services/AuthService'; -import { CustomerType, CustomerTypeLabels } from '../../enums'; - +import { useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { useTranslation } from "react-i18next"; +import toast, { Toaster } from "react-hot-toast"; +import { addCustomer, loginWithEmail } from "../../utils/api"; +import AuthService from "../../services/AuthService"; +import { CustomerType } from "../../enums"; +import { fadeInUp } from "../../components/Animation/OwnerPageAnimation"; +import { staggerContainer } from "../../components/Animation/OwnerPageAnimation"; +import InteractiveBackground from "@/app/components/Animation/Background"; +import HeaderOfSteps from "@/app/components/OwnerComponents/HeaderOfSteps"; +import HeaderOfForm from "@/app/components/OwnerComponents/HeaderOfForm"; +import OwnerFormStepOne from "@/app/components/OwnerComponents/OwnerFormStepOne"; +import Policy from "@/app/components/OwnerComponents/PolicyAgree"; +import ButtomStepOne from "@/app/components/OwnerComponents/ButtomStepOne"; +import ButtomStepTwo from "@/app/components/OwnerComponents/ButtomStepTwo"; +import HavAccount from "@/app/components/HavAccount"; +import OtpDialog from "@/app/components/OwnerComponents/OtpDialog"; +import { validateStep1 } from "@/app/validations/OwnerValidatoin"; export default function TenantRegisterPage() { - const router = useRouter(); const { t } = useTranslation(); const [step, setStep] = useState(1); const [showOtpModal, setShowOtpModal] = useState(false); - const [showPassword, setShowPassword] = useState(false); - const [showConfirmPassword, setShowConfirmPassword] = useState(false); const [isLoading, setIsLoading] = useState(false); - const [formData, setFormData] = useState({ - firstName: '', - lastName: '', - email: '', - phone: '', - whatsapp: '', - phone2: '', - nationalNumber: '', - password: '', - confirmPassword: '', + firstName: "", + lastName: "", + email: "", + phone: "", + whatsapp: "", + phone2: "", + nationalNumber: "", + password: "", + confirmPassword: "", customerType: CustomerType.PERSONAL, - agreeTerms: false + agreeTerms: false, }); - - const [otpCode, setOtpCode] = useState(''); const [errors, setErrors] = useState({}); - const validateEmail = (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); - const validatePhone = (phone) => /^(09|05)[0-9]{8}$/.test(phone); - - const validateStep1 = () => { - const newErrors = {}; - if (!formData.firstName) newErrors.firstName = t('register.firstNameRequired'); - if (!formData.lastName) newErrors.lastName = t('register.lastNameRequired'); - if (!formData.email) newErrors.email = t('register.emailRequired'); - else if (!validateEmail(formData.email)) newErrors.email = t('register.emailInvalid'); - if (!formData.phone) newErrors.phone = t('register.phoneRequired'); - else if (!validatePhone(formData.phone)) newErrors.phone = t('register.phoneInvalid') + ` (${t('register.phoneMustStartWith')} 09/05)`; - if (!formData.password) newErrors.password = t('register.passwordRequired'); - else if (formData.password.length < 6) newErrors.password = t('validation.passwordMinLength'); - if (!formData.whatsapp) newErrors.whatsapp = t('register.whatsappRequired'); - if (!formData.phone2 || formData.phone2.length !== 7) newErrors.phone2 = t('validation.phone7Digits'); - if (!formData.nationalNumber) newErrors.nationalNumber = t('register.nationalNumberRequired'); - if (formData.password !== formData.confirmPassword) newErrors.confirmPassword = t('validation.passwordsMatch'); - setErrors(newErrors); - return Object.keys(newErrors).length === 0; - }; - const handleNextStep = () => { - if (validateStep1()) { + if (validateStep1(formData, setErrors, t)) { setStep(2); - window.scrollTo({ top: 0, behavior: 'smooth' }); + window.scrollTo({ top: 0, behavior: "smooth" }); } else { - toast.error(t('validation.correctErrors')); + toast.error(t("validation.correctErrors")); } }; const handleSubmit = async (e) => { e.preventDefault(); - if (!formData.agreeTerms) { - toast.error(t('validation.agreeToTerms')); + toast.error(t("validation.agreeToTerms")); return; } - setIsLoading(true); - const payload = { firstName: formData.firstName, lastName: formData.lastName, @@ -93,327 +66,61 @@ export default function TenantRegisterPage() { password: formData.password, customerType: formData.customerType, }; - try { const res = await addCustomer(payload, null, null); - if (res.status === 200 || res.ok) { - const tempToken = res.data; - if (tempToken) { - AuthService.addToken(tempToken); - } - const apiMessage = res.message || res.data?.message; - toast.success(apiMessage || t('register.accountCreated'), { duration: 4000 }); - + toast.success(apiMessage || t("register.accountCreated"), { duration: 4000 }); const loginRes = await loginWithEmail(formData.email, formData.password); if (loginRes.status === 206) { const otpToken = loginRes.data; if (otpToken) AuthService.addToken(otpToken); - toast(loginRes.message || t('register.otpSentEmail'), { icon: '📧' }); + toast(loginRes.message || t("register.otpSentEmail"), { icon: "📧" }); setShowOtpModal(true); } else if (loginRes.status === 200) { const loginToken = loginRes.data; if (loginToken) AuthService.addToken(loginToken); - toast.success(loginRes.message || t('register.loginSuccess')); - router.push('/'); + toast.success(loginRes.message || t("register.loginSuccess")); } } else { - const errMsg = res.message || res.data?.message || t('register.accountCreationFailed'); + const errMsg = res.message || res.data?.message || t("register.accountCreationFailed"); toast.error(errMsg); } } catch (err) { - toast.error(err.message || t('register.registrationError')); + toast.error(err.message || t("register.registrationError")); } finally { setIsLoading(false); } }; - - const handleVerifyOTP = async () => { - if (!otpCode || otpCode.length < 4) { - toast.error(t('register.otpRequired')); - return; - } - setIsLoading(true); - try { - const res = await verifyEmail(otpCode); - if (res.status === 200) { - AuthService.deleteToken(); - toast.success(res.message || t('register.emailVerified')); - setShowOtpModal(false); - setTimeout(() => router.push('/login'), 1500); - } else { - toast.error(res.message || res.data?.message || t('accountVerification.otpInvalid')); - } - } catch (err) { - toast.error(err.message || t('register.verificationError')); - } finally { - setIsLoading(false); - } - }; - - const handleResendOTP = async () => { - setIsLoading(true); - try { - await sendEmailOTP(); - toast.success(t('register.newOtpSent')); - } catch (err) { - toast.error(t('register.resendOtpFailed')); - } finally { - setIsLoading(false); - } - }; - - const fadeInUp = { - initial: { opacity: 0, y: 20 }, - animate: { opacity: 1, y: 0 }, - transition: { duration: 0.5 } - }; - - const staggerContainer = { - animate: { transition: { staggerChildren: 0.1 } } - }; - - const backgroundElements = useMemo(() => { - const circles = [ - { style: { top: '20%', right: '20%', width: '256px', height: '256px' }, className: 'bg-blue-500/10' }, - { style: { bottom: '20%', left: '20%', width: '320px', height: '320px' }, className: 'bg-blue-500/10' }, - { style: { top: '50%', left: '50%', width: '384px', height: '384px', transform: 'translate(-50%, -50%)' }, className: 'bg-blue-500/10' }, - ]; - const dots = [ - { left: '5%', top: '10%', size: '120px' }, - { left: '15%', top: '70%', size: '80px' }, - { left: '25%', top: '30%', size: '150px' }, - { left: '35%', top: '85%', size: '100px' }, - { left: '45%', top: '15%', size: '90px' }, - { left: '55%', top: '60%', size: '130px' }, - { left: '65%', top: '40%', size: '70px' }, - { left: '75%', top: '80%', size: '110px' }, - { left: '85%', top: '20%', size: '140px' }, - { left: '95%', top: '50%', size: '85px' }, - ]; - return ( - <> - {circles.map((circle, i) => ( -
- ))} - {dots.map((dot, i) => ( -
- ))} - - ); - }, []); - return ( -
+
-
{backgroundElements}
- +
+ {" "} +
- - - - {t('forgotPassword.backToLogin')} - - - -
- {[1, 2].map((s) => ( - = s ? 'bg-blue-500' : 'bg-gray-700'}`} animate={{ scaleX: step >= s ? 1 : 0.5 }} /> - ))} -
- +
-
- - - - - -

{step === 1 ? t('register.tenant.step1Title') : t('register.tenant.step2Title')}

-

{step === 1 ? t('register.tenant.step1Desc') : t('register.tenant.step2Desc')}

-
-
- +
- { e.preventDefault(); handleNextStep(); } : handleSubmit} className="space-y-6"> - {step === 1 && ( - <> - -
- -
-
- -
- { setFormData({...formData, firstName: e.target.value}); setErrors({...errors, firstName: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.firstName ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.firstName')} /> -
- {errors.firstName &&

{errors.firstName}

} -
-
- - { setFormData({...formData, lastName: e.target.value}); setErrors({...errors, lastName: null}); }} className={`w-full px-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.lastName ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.lastName')} /> - {errors.lastName &&

{errors.lastName}

} -
-
- - - -
-
- -
- { setFormData({...formData, email: e.target.value}); setErrors({...errors, email: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.email ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.emailPlaceholder')} /> -
- {errors.email &&

{errors.email}

} -
- - - -
-
- -
- { setFormData({...formData, phone: e.target.value}); setErrors({...errors, phone: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.phone ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.phonePlaceholder')} /> -
- {errors.phone &&

{errors.phone}

} -
- - - -
-
- -
- { setFormData({...formData, whatsapp: e.target.value}); setErrors({...errors, whatsapp: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.whatsapp ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.whatsappPlaceholder')} /> -
- {errors.whatsapp &&

{errors.whatsapp}

} -
- - - -
-
- -
- { setFormData({...formData, phone2: e.target.value}); setErrors({...errors, phone2: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.phone2 ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.phoneSecondaryPlaceholder')} maxLength={7} /> -
- {errors.phone2 &&

{errors.phone2}

} -
- - - -
-
- -
- { setFormData({...formData, nationalNumber: e.target.value}); setErrors({...errors, nationalNumber: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.nationalNumber ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.nationalNumberPlaceholder')} /> -
- {errors.nationalNumber &&

{errors.nationalNumber}

} -
- - - - - - - - -
-
- -
- { setFormData({...formData, password: e.target.value}); setErrors({...errors, password: null}); }} className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.password ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.passwordPlaceholder')} /> - -
- {errors.password &&

{errors.password}

} -
- - - -
-
- -
- { setFormData({...formData, confirmPassword: e.target.value}); setErrors({...errors, confirmPassword: null}); }} className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.confirmPassword ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.confirmPasswordPlaceholder')} /> - - {formData.confirmPassword && (
{formData.password === formData.confirmPassword ? : }
)} -
- {errors.confirmPassword &&

{errors.confirmPassword}

} -
- - )} - - {step === 2 && ( - <> - - setFormData({...formData, agreeTerms: e.target.checked})} className="w-4 h-4 rounded border-gray-600 bg-white/5 text-blue-500 focus:ring-blue-500" required /> - - - - )} - + { e.preventDefault(); handleNextStep(); }: handleSubmit } + className="space-y-6" + > + {step === 1 && } + {step === 2 && } - {step === 1 ? ( - <> - - - - ) : ( - <> - - - - )} + {step === 1 ? : } - - {step === 1 && ( - - {t('register.haveAccount')} {t('register.loginLink')} - - )} + {step === 1 && }
- - - {showOtpModal && ( - - -
-
-

{t('register.otpTitle')}

-

{t('register.otpSentTo')}

-

{formData.email}

-
-
- -
-
- setOtpCode(e.target.value)} className="w-full pr-12 pl-4 py-3 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 text-white text-center tracking-[0.5em] text-xl" placeholder="------" /> -
-
-
- -
- -
-
- )} -
+ {showOtpModal && }
); } diff --git a/app/services/AuthService.js b/app/services/AuthService.js index 35d8d6f..4de5b6e 100644 --- a/app/services/AuthService.js +++ b/app/services/AuthService.js @@ -1,4 +1,5 @@ import Cookies from "js-cookie"; +import { getCustomerByUserId, getOwnerByUserId, loginWithEmail, loginWithPhone } from "../utils/api"; /** * AuthService * Manages authentication tokens and user role detection via JWT decoding. @@ -39,19 +40,21 @@ const AuthService = Object.freeze({ if (typeof window === "undefined") return null; return Cookies.get(TOKEN_KEY); }, - deleteToken() { - localStorage.removeItem(USER_KEY); + Cookies.remove(USER_KEY); Cookies.remove(TOKEN_KEY); }, - /** * Cache full user profile (from API) * @param {object} user — { name, email, phone, ... } */ cacheUser(user) { - if (!user) return; - localStorage.setItem(USER_KEY, JSON.stringify(user)); + Cookies.set(USER_KEY, JSON.stringify(user), { + expires: 7, + secure: true, + sameSite: "lax", + path: "/", + }); }, /** @@ -59,8 +62,12 @@ const AuthService = Object.freeze({ * @returns {object|null} */ getCachedUser() { + const user = Cookies.get(USER_KEY); + + if (!user) return null; + try { - return JSON.parse(localStorage.getItem(USER_KEY)); + return JSON.parse(user); } catch { return null; } @@ -88,7 +95,6 @@ const AuthService = Object.freeze({ getUser() { const payload = this.decodeToken(); if (!payload) return null; - const cached = this.getCachedUser(); return { @@ -96,7 +102,7 @@ const AuthService = Object.freeze({ name: cached?.name || payload["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"] || null, email: cached?.email || payload["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"] || null, phone: cached?.phone || payload["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/mobilephone"] || null, - roles: this.getRoles(), + roles: cached?.roles, }; }, @@ -107,7 +113,6 @@ const AuthService = Object.freeze({ getUserId() { const user = this.getUser(); if (!user?.id) return null; - const parsedId = Number(user.id); return Number.isFinite(parsedId) ? parsedId : user.id; }, @@ -124,7 +129,21 @@ const AuthService = Object.freeze({ if (typeof roles === "string") return [roles]; return []; }, + async cacheCurrentUser(getOwnerByUserId, getCustomerByUserId) { + const authUser = this.getUser(); + if (!authUser?.id) { + return; + } + const fetchFn = this.isOwner() ? getOwnerByUserId : getCustomerByUserId; + const profile = await fetchFn(authUser.id); + this.cacheUser({ + name: profile.firstName + profile.lastName, + email: profile.email, + phone: profile.phoneNumber, + roles: this.getRoles(), + }); + }, /** * User has Owner role * @returns {boolean} @@ -164,6 +183,37 @@ const AuthService = Object.freeze({ isAuthenticated() { return !!this.getToken(); }, + + async login({ loginMethod, credential, password }) { + const loginFn = loginMethod === "email" ? loginWithEmail : loginWithPhone; + const result = await loginFn(credential, password); + switch (result.status) { + case 200: { + const token = typeof result.data === "string" ? result.data : result.data.token; + this.addToken(token); + await this.cacheCurrentUser(getOwnerByUserId, getCustomerByUserId); + return { + type: "SUCCESS", + }; + } + + case 206: { + const token = typeof result.data === "string" ? result.data : result.data.token; + this.addToken(token); + if (loginMethod === "email") await sendEmailOTP(); + else await sendPhoneOTP(); + return { + type: "OTP_REQUIRED", + }; + } + + default: + return { + type: "ERROR", + result, + }; + } + }, }); export default AuthService; diff --git a/app/utils/api.js b/app/utils/api.js index 693ffe4..aeb0e7a 100644 --- a/app/utils/api.js +++ b/app/utils/api.js @@ -1,6 +1,4 @@ -import { Toast } from "flowbite-react"; import AuthService from "../services/AuthService"; - const API_BASE = process.env.NEXT_PUBLIC_API_URL || "https://45.93.137.91.nip.io/api"; const REPORT_API_BASE = process.env.NEXT_PUBLIC_REPORT_API_URL || "http://45.93.137.91/api"; @@ -82,7 +80,6 @@ async function apiFetch(endpoint, options = {}) { } } catch (error) { if (error instanceof TypeError && error.message === "Failed to fetch") { - throw new Error(`تعذر الاتصال بالخادم. تحقق من اتصالك بالإنترنت أو حاول مرة أخرى لاحقاً. (${API_BASE}${endpoint})`); } throw error; @@ -419,7 +416,6 @@ export async function uploadPicture(file) { async function multipartAuthFetch(endpoint, formData) { const token = AuthService.getToken(); - const res = await fetch(`${API_BASE}${endpoint}`, { method: "POST", headers: { @@ -450,7 +446,7 @@ async function multipartAuthFetch(endpoint, formData) { }; } -export async function addOwner(data, frontImage = null, backImage = null, licenseImage = null) { +export async function addOwner(data, frontImage = null, backImage = null) { const formData = new FormData(); formData.append("FirstName", data.firstName || data.FirstName || ""); @@ -471,8 +467,6 @@ export async function addOwner(data, frontImage = null, backImage = null, licens if (frontImage) formData.append("FrontIdCarImagePath", frontImage); if (backImage) formData.append("RearIdCarImagePath", backImage); - if (licenseImage) formData.append("LicenseImagePath", licenseImage); - return multipartAuthFetch("/Owner/Add", formData); } diff --git a/app/validations/OwnerValidatoin.js b/app/validations/OwnerValidatoin.js new file mode 100644 index 0000000..4010043 --- /dev/null +++ b/app/validations/OwnerValidatoin.js @@ -0,0 +1,54 @@ +// utils/registerValidation.js + +const validateEmail = (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email?.trim()); + +// يدعم الأرقام مع البادئات اختياريًا +const validatePhone = (phone) => /^(09|05)[0-9]{8}$/.test(phone?.trim()); + +export const validateStep1 = (formData, setErrors, t) => { + const newErrors = {}; + + if (!formData.firstName?.trim()) newErrors.firstName = t("register.firstNameRequired"); + if (!formData.lastName?.trim()) newErrors.lastName = t("register.lastNameRequired"); + + if (!formData.email?.trim()) { + newErrors.email = t("register.emailRequired"); + } else if (!validateEmail(formData.email)) { + newErrors.email = t("register.emailInvalid"); + } + + if (!formData.phone?.trim()) { + newErrors.phone = t("register.phoneRequired"); + } else if (!validatePhone(formData.phone)) { + newErrors.phone = `${t("register.phoneInvalid")} (${t("register.phoneMustStartWith")} 09/05)`; + } + + if (!formData.whatsapp?.trim()) { + newErrors.whatsapp = t("register.whatsappRequired"); + } else if (!validatePhone(formData.whatsapp)) { + newErrors.whatsapp = `${t("register.phoneInvalid")} (${t("register.phoneMustStartWith")} 09/05)`; + } + + if (!formData.password) { + newErrors.password = t("register.passwordRequired"); + } else if (formData.password.length < 6) { + newErrors.password = t("validation.passwordMinLength"); + } + + if (formData.password !== formData.confirmPassword) { + newErrors.confirmPassword = t("validation.passwordsMatch"); + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; +}; + +export const validateStep2 = (idImages, setErrors, t) => { + const newErrors = {}; + + if (!idImages?.front) newErrors.front = t("register.frontIdRequired"); + if (!idImages?.back) newErrors.back = t("register.backIdRequired"); + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; +}; diff --git a/public/NotUsed.txt b/public/NotUsed.txt new file mode 100644 index 0000000..904ad1a --- /dev/null +++ b/public/NotUsed.txt @@ -0,0 +1,169 @@ +: ( + // /* OTP Verification Step */ + // + //
+ // + //

+ // {t("sendOTPTo")}{" "} + // + // {formData.credential} + // + //

+ //
+ + //
+ // + // { + // setOtpCode(e.target.value); + // if (otpError) setOtpError(""); + // }} + // className={`w-full px-4 py-4 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white text-center text-2xl tracking-[0.5em] placeholder-gray-500 transition-all ${ + // otpError ? "border-red-500" : "border-gray-700" + // }`} + // placeholder="______" + // maxLength={6} + // dir="ltr" + // /> + // {otpError && ( + // + // {otpError} + // + // )} + //
+ + // + // + // {isLoading ? ( + // <> + // + // {t("verifying")} + // + // ) : isSuccess ? ( + // <> + // + // {t("success")} + // + // ) : ( + // <> + // + // {t("verify")} + // + // )} + // + // + + //
+ // + // + //
+ //
+ <> + // )} + + + + + + // Send OTP + try { + if (loginMethod === "email") { + await sendEmailOTP(); + } else { + await sendPhoneOTP(); + } + } catch (otpErr) { + console.warn("[Login] OTP send failed, proceeding anyway:", otpErr); + } + + setStep("otp"); + } else if (result.status >= 500) { + console.error("[Login] Server error:", result.status, result.data); + toast.error(t("serverError"), { + style: { background: "#fee2e2", color: "#991b1b" }, + }); + } else if (result.status === 404) { + console.error("[Login] API endpoint not found:", result.status); + toast.error(t("apiNotFound"), { + style: { background: "#fee2e2", color: "#991b1b" }, + }); + } else if (result.status === 429) { + toast.error(t("tooManyRequests"), { + style: { background: "#fee2e2", color: "#991b1b" }, + }); + } else { + console.error("[Login] Unexpected status:", result.status, result.data); + toast.error(result.data?.message || result.data || t("invalidCredentials"), { + style: { background: "#fee2e2", color: "#991b1b" }, + }); + } + } catch (err) { + console.error("[Login] Error:", err); + toast.error(err.message || t("connectionError"), { + style: { background: "#fee2e2", color: "#991b1b" }, + }); + toast(t("otpRequired"), { + icon: "🔐", + style: { background: "#fef3c7", color: "#92400e" }, + }); + + // Send OTP + try { + if (loginMethod === "email") { + await sendEmailOTP(); + } else { + await sendPhoneOTP(); + } + } catch (otpErr) { + console.warn("[Login] OTP send failed, proceeding anyway:", otpErr); + } + + setStep("otp"); + } else if (result.status >= 500) { + console.error("[Login] Server error:", result.status, result.data); + toast.error(t("serverError"), { + style: { background: "#fee2e2", color: "#991b1b" }, + }); + } else if (result.status === 404) { + console.error("[Login] API endpoint not found:", result.status); + toast.error(t("apiNotFound"), { + style: { background: "#fee2e2", color: "#991b1b" }, + }); + } else if (result.status === 429) { + toast.error(t("tooManyRequests"), { + style: { background: "#fee2e2", color: "#991b1b" }, + }); + } else { + console.error("[Login] Unexpected status:", result.status, result.data); + toast.error(result.data?.message || result.data || t("invalidCredentials"), { + style: { background: "#fee2e2", color: "#991b1b" }, + }); + } + } catch (err) { + console.error("[Login] Error:", err); + toast.error(err.message || t("connectionError"), { + style: { background: "#fee2e2", color: "#991b1b" }, + }); + } finally { + setIsLoading(false); + } \ No newline at end of file