fixed owner and customer register make reuseable component and completed it
This commit is contained in:
70
app/components/Animation/HeaderLoginAnimation.js
Normal file
70
app/components/Animation/HeaderLoginAnimation.js
Normal file
@ -0,0 +1,70 @@
|
||||
import { motion } from "framer-motion";
|
||||
export 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,
|
||||
}));
|
||||
|
||||
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 (
|
||||
<>
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
{particles.map((p) => (
|
||||
<motion.div
|
||||
key={p.id}
|
||||
className="absolute rounded-full bg-amber-500/20"
|
||||
style={{
|
||||
left: `${p.x}%`,
|
||||
top: `${p.y}%`,
|
||||
width: p.size,
|
||||
height: p.size,
|
||||
}}
|
||||
animate={{
|
||||
y: [0, -20, 0],
|
||||
x: [0, 10, -10, 0],
|
||||
opacity: [0.2, 0.4, 0.2],
|
||||
}}
|
||||
transition={{
|
||||
duration: p.duration,
|
||||
repeat: Infinity,
|
||||
delay: p.delay,
|
||||
ease: "linear",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Glow orbs */}
|
||||
<motion.div
|
||||
className="absolute top-20 left-20 w-64 h-64 bg-amber-500/10 rounded-full blur-3xl"
|
||||
animate={{ scale: [1, 1.2, 1], x: [0, 30, 0], y: [0, -20, 0] }}
|
||||
transition={{ duration: 12, repeat: Infinity }}
|
||||
/>
|
||||
<motion.div
|
||||
className="absolute bottom-20 right-20 w-80 h-80 bg-blue-500/10 rounded-full blur-3xl"
|
||||
animate={{ scale: [1, 1.3, 1], x: [0, -30, 0], y: [0, 20, 0] }}
|
||||
transition={{ duration: 15, repeat: Infinity }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
26
app/components/Animation/OwnerPageAnimation.js
Normal file
26
app/components/Animation/OwnerPageAnimation.js
Normal file
@ -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) => (
|
||||
<div key={index} style={circle.style} className={`absolute rounded-full pointer-events-none ${circle.className}`} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
17
app/components/HavAccount.js
Normal file
17
app/components/HavAccount.js
Normal file
@ -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 (
|
||||
<>
|
||||
<motion.p variants={fadeInUp} className="text-center text-gray-400 mt-4">
|
||||
{t("register.haveAccount")}
|
||||
<Link href="/login" className="text-blue-400 hover:text-blue-300 font-medium transition-colors">
|
||||
{t("register.loginLink")}
|
||||
</Link>
|
||||
</motion.p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
17
app/components/LoginComponent/CreateNewAccount.js
Normal file
17
app/components/LoginComponent/CreateNewAccount.js
Normal file
@ -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 (
|
||||
<>
|
||||
<motion.p variants={itemVariants} className="text-center text-gray-400 mt-6">
|
||||
{t("noAccount")}{" "}
|
||||
<Link href="/auth/choose-role" className="text-amber-400 hover:text-amber-300 font-medium transition-colors">
|
||||
{t("createNewAccount")}
|
||||
</Link>
|
||||
</motion.p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
45
app/components/LoginComponent/HeaderLogin.js
Normal file
45
app/components/LoginComponent/HeaderLogin.js
Normal file
@ -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 (
|
||||
<>
|
||||
<motion.div variants={itemVariants} className="absolute -top-16 left-0">
|
||||
<Link href="/" className="group flex items-center gap-2 text-gray-400 hover:text-white transition-colors">
|
||||
<motion.div whileHover={{ x: -5 }} transition={{ type: "spring", stiffness: 400 }}>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</motion.div>
|
||||
<span>{t("backToHome")}</span>
|
||||
</Link>
|
||||
</motion.div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function HeaderForm({ step }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<div className="bg-gradient-to-r from-amber-700 to-amber-400 p-8 text-center relative overflow-hidden">
|
||||
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ delay: 0.2, type: "spring" }} className="absolute -top-10 -right-10 w-40 h-40 bg-white/10 rounded-full" />
|
||||
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ delay: 0.3, type: "spring" }} className="absolute -bottom-10 -left-10 w-40 h-40 bg-white/10 rounded-full" />
|
||||
|
||||
<motion.div initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} transition={{ delay: 0.2 }} className="relative z-10">
|
||||
<motion.div
|
||||
animate={{ rotate: [0, 10, -10, 0] }}
|
||||
transition={{ duration: 2, repeat: Infinity }}
|
||||
className="w-20 h-20 mx-auto mb-4 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-sm"
|
||||
>
|
||||
{step === "otp" ? <KeyRound className="w-10 h-10 text-white" /> : <Home className="w-10 h-10 text-white" />}
|
||||
</motion.div>
|
||||
<h1 className="text-3xl font-bold text-white mb-2">SweetHome</h1>
|
||||
<p className="text-amber-100">{step === "otp" ? t("enterOTP") : t("welcomeBack")}</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
127
app/components/LoginComponent/Login.jsx
Normal file
127
app/components/LoginComponent/Login.jsx
Normal file
@ -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 (
|
||||
<div className="min-h-screen flex items-center justify-center p-4 relative overflow-hidden">
|
||||
<Toaster position="top-center" reverseOrder={false} />
|
||||
<InteractiveBackground />
|
||||
{/* Particles */}
|
||||
<HeaderAnimation />
|
||||
<motion.div variants={containerVariants} initial="hidden" animate="visible" className="relative w-full max-w-md z-10">
|
||||
{/* Back link */}
|
||||
<BackLink />
|
||||
<motion.div variants={itemVariants} className="bg-white/10 backdrop-blur-2xl rounded-3xl shadow-2xl border border-white/10 overflow-hidden">
|
||||
{/* Header */}
|
||||
<HeaderForm step={step} />
|
||||
<div className="p-8">
|
||||
<AnimatePresence mode="wait">
|
||||
{step === "login" ? (
|
||||
<motion.form key="login" initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: 20 }} onSubmit={handleLogin} className="space-y-6">
|
||||
{/* Login method tabs */}
|
||||
<TabsLogin loginMethod={loginMethod} handelTabs={handelTabs} />
|
||||
{/* Form Login */}
|
||||
<LoginForm
|
||||
loginMethod={loginMethod}
|
||||
errors={errors}
|
||||
formData={formData}
|
||||
isLoading={isLoading}
|
||||
isSuccess={isSuccess}
|
||||
setFormData={setFormData}
|
||||
handleCredentialChange={handleCredentialChange}
|
||||
/>
|
||||
</motion.form>
|
||||
) : (
|
||||
// /* OTP Verification Step */
|
||||
<OtpComponent isLoading={isLoading} setIsLoading={setIsLoading} isSuccess={isSuccess} setIsSuccess={setIsSuccess} loginMethod={loginMethod} formData={formData} setStep={setStep} />
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<CreateNewAccount />
|
||||
</div>
|
||||
</motion.div>
|
||||
<PolicyAndPrivecy />
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
124
app/components/LoginComponent/LoginForm.js
Normal file
124
app/components/LoginComponent/LoginForm.js
Normal file
@ -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 (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">{loginMethod === "email" ? t("emailLabel") : t("phoneLabel")}</label>
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||
{loginMethod === "email" ? (
|
||||
<Mail className={`w-5 h-5 transition-colors ${errors.credential ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
|
||||
) : (
|
||||
<Phone className={`w-5 h-5 transition-colors ${errors.credential ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
// type={loginMethod === 'email' ? 'email' : 'tel'}
|
||||
value={formData.credential}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
{errors.credential && (
|
||||
<motion.p initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} className="text-red-500 text-sm mt-1">
|
||||
{errors.credential}
|
||||
</motion.p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Password */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">{t("password")}</label>
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||
<Lock className={`w-5 h-5 transition-colors ${errors.password ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
|
||||
</div>
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={formData.password}
|
||||
onChange={(e) => {
|
||||
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")}
|
||||
/>
|
||||
<button type="button" onClick={() => setShowPassword(!showPassword)} className="absolute inset-y-0 left-0 pl-3 flex items-center">
|
||||
{showPassword ? <EyeOff className="w-5 h-5 text-gray-400 hover:text-gray-300 transition-colors" /> : <Eye className="w-5 h-5 text-gray-400 hover:text-gray-300 transition-colors" />}
|
||||
</button>
|
||||
</div>
|
||||
{errors.password && (
|
||||
<motion.p initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} className="text-red-500 text-sm mt-1">
|
||||
{errors.password}
|
||||
</motion.p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Remember + Forgot */}
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="flex items-center gap-2 cursor-pointer group">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.rememberMe}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
rememberMe: e.target.checked,
|
||||
})
|
||||
}
|
||||
className="w-4 h-4 rounded border-gray-600 bg-white/5 text-amber-500 focus:ring-amber-500 focus:ring-offset-0"
|
||||
/>
|
||||
<span className="text-sm text-gray-400 group-hover:text-white transition-colors">{t("rememberMe")}</span>
|
||||
</label>
|
||||
<Link href="/forgot-password" className="text-sm text-amber-400 hover:text-amber-300 transition-colors">
|
||||
{t("forgotPassword")}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Submit */}
|
||||
<motion.button
|
||||
type="submit"
|
||||
disabled={isLoading || isSuccess}
|
||||
className="relative w-full bg-gradient-to-r from-amber-500 to-amber-600 text-white py-4 rounded-xl font-bold text-lg overflow-hidden group disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<span className="relative z-10 flex items-center justify-center gap-2">
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
{t("loggingIn")}{" "}
|
||||
</>
|
||||
) : isSuccess ? (
|
||||
<>
|
||||
<CheckCircle className="w-5 h-5" /> {t("success")}{" "}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{" "}
|
||||
<LogIn className="w-5 h-5" />
|
||||
{t("login")}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</motion.button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
145
app/components/LoginComponent/OtpComponent.js
Normal file
145
app/components/LoginComponent/OtpComponent.js
Normal file
@ -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 (
|
||||
<motion.form key="otp" initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} onSubmit={handleVerifyOTP} className="space-y-6">
|
||||
<div className="text-center mb-4">
|
||||
<Shield className="w-12 h-12 text-amber-500 mx-auto mb-3" />
|
||||
<p className="text-amber-300 text-sm">
|
||||
{t("sendOTPTo")}{" "}
|
||||
<span className="text-amber-50 font-medium" dir="ltr">
|
||||
{formData?.credential}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">{t("otpLabel")}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={otpCode}
|
||||
onChange={(e) => {
|
||||
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 && (
|
||||
<motion.p initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} className="text-red-500 text-sm mt-1 text-center">
|
||||
{otpError}
|
||||
</motion.p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<motion.button
|
||||
type="submit"
|
||||
disabled={isLoading || isSuccess}
|
||||
className="relative w-full bg-gradient-to-r from-amber-500 to-amber-600 text-white py-4 rounded-xl font-bold text-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
{t("verifying")}
|
||||
</>
|
||||
) : isSuccess ? (
|
||||
<>
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
{t("success")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<KeyRound className="w-5 h-5" />
|
||||
{t("verify")}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</motion.button>
|
||||
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setStep("login");
|
||||
setOtpCode("");
|
||||
setOtpError("");
|
||||
}}
|
||||
className="text-gray-400 hover:text-white transition-colors"
|
||||
>
|
||||
← {t("back")}
|
||||
</button>
|
||||
<button type="button" onClick={resendOTP} className="text-amber-400 hover:text-amber-300 transition-colors">
|
||||
{t("resendCode")}
|
||||
</button>
|
||||
</div>
|
||||
</motion.form>
|
||||
);
|
||||
}
|
||||
21
app/components/LoginComponent/PolicyAndPrivecy.js
Normal file
21
app/components/LoginComponent/PolicyAndPrivecy.js
Normal file
@ -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 (
|
||||
<>
|
||||
<motion.p variants={itemVariants} className="text-center text-gray-500 text-xs mt-4">
|
||||
{t("agreeTerms")}{" "}
|
||||
<Link href="/terms" className="text-amber-400 hover:text-amber-300 transition-colors">
|
||||
{t("termsOfUse")}
|
||||
</Link>{" "}
|
||||
{t("and")}{" "}
|
||||
<Link href="/privacy" className="text-amber-400 hover:text-amber-300 transition-colors">
|
||||
{t("privacyPolicy")}
|
||||
</Link>
|
||||
</motion.p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
36
app/components/LoginComponent/Tabs.js
Normal file
36
app/components/LoginComponent/Tabs.js
Normal file
@ -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 (
|
||||
<>
|
||||
<div className="flex gap-2 bg-white/5 p-1 rounded-xl">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
handelTabs("email");
|
||||
}}
|
||||
className={`flex-1 py-2.5 rounded-lg text-sm font-medium transition-all flex items-center justify-center gap-2 ${
|
||||
loginMethod === "email" ? "bg-amber-500 text-white shadow-lg" : "text-gray-400 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
<Mail className="w-4 h-4" />
|
||||
{t("emailMethod")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
handelTabs("phone");
|
||||
}}
|
||||
className={`flex-1 py-2.5 rounded-lg text-sm font-medium transition-all flex items-center justify-center gap-2 ${
|
||||
loginMethod === "phone" ? "bg-amber-500 text-white shadow-lg" : "text-gray-400 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
<Phone className="w-4 h-4" />
|
||||
{t("phoneMethod")}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
25
app/components/OwnerComponents/ButtomStepOne.js
Normal file
25
app/components/OwnerComponents/ButtomStepOne.js
Normal file
@ -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 (
|
||||
<>
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push("/auth/choose-role")}
|
||||
className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors"
|
||||
>
|
||||
{t("register.cancel")}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className={`flex-1 bg-gradient-to-r ${type === "owner" ? " from-amber-500 to-amber-600" : " from-blue-500 to-blue-600"} text-white py-3 px-4 rounded-xl font-medium ${type === "onwer" ? "hover:from-amber-600 hover:to-amber-700" : "hover:from-blue-600 hover:to-blue-700"} transition-all`}
|
||||
>
|
||||
{t("register.next")}
|
||||
</button>
|
||||
</>
|
||||
</>
|
||||
);
|
||||
}
|
||||
27
app/components/OwnerComponents/ButtomStepTwo.js
Normal file
27
app/components/OwnerComponents/ButtomStepTwo.js
Normal file
@ -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 (
|
||||
<>
|
||||
<button type="button" onClick={() => setStep(1)} className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors">
|
||||
{t("register.previous")}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading || !formData.agreeTerms}
|
||||
className={`flex-1 bg-gradient-to-r${type === "owner" ? " from-amber-500 to-amber-600" : " from-blue-500 to-blue-600"} text-amber-50 py-3 px-4 rounded-xl font-medium${type === "onwer" ? "hover:from-amber-600 hover:to-amber-700" : "hover:from-blue-600 hover:to-blue-700"} transition-all disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
<span>{t("register.registering")}</span>
|
||||
</div>
|
||||
) : (
|
||||
t("register.createAccount")
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
6
app/components/OwnerComponents/FormOwner.js
Normal file
6
app/components/OwnerComponents/FormOwner.js
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
export default function FormOwner(){
|
||||
return(<>
|
||||
|
||||
</>)
|
||||
}
|
||||
44
app/components/OwnerComponents/HeaderOfForm.js
Normal file
44
app/components/OwnerComponents/HeaderOfForm.js
Normal file
@ -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: <Building className="w-10 h-10 text-white" />,
|
||||
},
|
||||
{
|
||||
colors: "from-blue-500 to-blue-600",
|
||||
titleKey: "register.tenant.step1Title",
|
||||
title2Key: "register.tenant.step2Title",
|
||||
descKey: "register.tenant.step1Desc",
|
||||
desc2Key: "register.tenant.step2Desc",
|
||||
icon: <Home className="w-10 h-10 text-white" />,
|
||||
},
|
||||
];
|
||||
export default function HeaderOfForm({ step = 1, num = 0 }) {
|
||||
const { t } = useTranslation();
|
||||
const currentData = data[num] || data[0];
|
||||
return (
|
||||
<div className={`bg-gradient-to-r ${currentData.colors} p-8 text-center relative overflow-hidden`}>
|
||||
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ delay: 0.2, type: "spring" }} className="absolute -top-10 -right-10 w-40 h-40 bg-white/10 rounded-full" />
|
||||
<motion.div initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} className="relative z-10">
|
||||
<motion.div
|
||||
animate={{ rotate: [0, 10, -10, 0] }}
|
||||
transition={{ duration: 2, repeat: Infinity }}
|
||||
className="w-20 h-20 mx-auto mb-4 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-sm"
|
||||
>
|
||||
{data[num].icon}
|
||||
</motion.div>
|
||||
<h1 className="text-3xl font-bold text-white mb-2">{t(step === 1 ? t(currentData.titleKey) : t(currentData.title2Key))}</h1>
|
||||
<p className="text-amber-100">{t(step === 1 ? t(currentData.descKey) : t(currentData.desc2Key))}</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
28
app/components/OwnerComponents/HeaderOfSteps.js
Normal file
28
app/components/OwnerComponents/HeaderOfSteps.js
Normal file
@ -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 (
|
||||
<>
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Link href="/auth/choose-role" className="flex items-center gap-2 text-gray-400 hover:text-white transition-colors group">
|
||||
<motion.div whileHover={{ x: -5 }}>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</motion.div>
|
||||
<span>{t("forgotPassword.backToLogin")}</span>
|
||||
</Link>
|
||||
<span className="text-sm text-gray-200">{t("register.stepOf2", { step: step })}</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{numberStep.map((s) => (
|
||||
<motion.div key={s} className={`h-2 flex-1 rounded-full ${step >= s ? colors : "bg-gray-800"}`} animate={{ scaleX: step >= s ? 1 : 0.5 }} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
104
app/components/OwnerComponents/OtpDialog.js
Normal file
104
app/components/OwnerComponents/OtpDialog.js
Normal file
@ -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 (
|
||||
<>
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 z-50">
|
||||
<motion.div initial={{ scale: 0.9, y: 20 }} animate={{ scale: 1, y: 0 }} exit={{ scale: 0.9, y: 20 }} className="bg-gray-900 border border-white/10 rounded-2xl w-full max-w-md p-6 shadow-2xl">
|
||||
<div className="text-center mb-6">
|
||||
<div className="w-16 h-16 bg-amber-500/20 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||
<Shield className={`w-8 h-8 ${type === "owner" ? "text-amber-500" : "text-blue-500"} `} />
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-white">{t("register.otpTitle")}</h2>
|
||||
<p className="text-gray-400 text-sm mt-1">{t("register.otpSentTo")}</p>
|
||||
<p className={`${type === "owner" ? "text-amber-500" : "text-blue-500"} font-medium text-sm`}>{formData.email}</p>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">{t("register.otpLabel")}</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||
<KeyRound className="w-5 h-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={otpCode}
|
||||
maxLength={6}
|
||||
onChange={(e) => 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="------"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={handleVerifyOTP}
|
||||
disabled={isLoading || !otpCode}
|
||||
className={`flex-1 bg-gradient-to-r ${type === "owner" ? "from-amber-500 to-amber-600" : "from-blue-500 to-blue-600"} text-white py-3 rounded-xl font-medium ${type === "owner" ? "hover:from-amber-600 hover:to-amber-700" : "hover:from-blue-600 hover:to-blue-700"} disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2`}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
<span>{t("register.verifying")}</span>
|
||||
</>
|
||||
) : (
|
||||
t("register.verify")
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleResendOTP}
|
||||
disabled={isLoading}
|
||||
className={`w-full text-center ${type === "owner" ? "text-amber-400 hover:text-amber-300" : "text-blue-400 hover:text-blue-300"} text-sm mt-3 disabled:opacity-50`}
|
||||
>
|
||||
{t("register.resendOtp")}
|
||||
</button>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
170
app/components/OwnerComponents/Owner.js
Normal file
170
app/components/OwnerComponents/Owner.js
Normal file
@ -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 (
|
||||
<div className="min-h-screen flex items-center justify-center p-4 relative overflow-hidden">
|
||||
<Toaster position="top-center" reverseOrder={false} />
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
<BackgroundElements />
|
||||
<InteractiveBackground />
|
||||
</div>
|
||||
<motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.5 }} className="relative z-10 w-full max-w-2xl">
|
||||
<HeaderOfSteps step={step} numberStep={[1, 2]} colors={"bg-amber-500"} />
|
||||
<motion.div
|
||||
key={step}
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="bg-white/5 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/10 overflow-hidden"
|
||||
>
|
||||
<HeaderOfForm step={step} num={0} />
|
||||
<div className="p-8">
|
||||
<motion.form
|
||||
variants={staggerContainer}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
onSubmit={
|
||||
step === 1
|
||||
? (e) => {
|
||||
e.preventDefault();
|
||||
handleNextStep();
|
||||
}
|
||||
: handleSubmit
|
||||
}
|
||||
className="space-y-6"
|
||||
>
|
||||
{step === 1 && (
|
||||
<OwnerFormStepOne
|
||||
setErrors={setErrors}
|
||||
setFormData={setFormData}
|
||||
formData={formData}
|
||||
errors={errors}
|
||||
isCompany={isCompany}
|
||||
setIdImagePreviews={setIdImagePreviews}
|
||||
setIdImages={setIdImages}
|
||||
type={"owner"}
|
||||
/>
|
||||
)}
|
||||
{step === 2 && (
|
||||
<>
|
||||
<OwnerFormStepTwo idImagePreviews={idImagePreviews} errors={errors} setIdImagePreviews={setIdImagePreviews} setIdImages={setIdImages} setFormData={setFormData} formData={formData} />
|
||||
</>
|
||||
)}
|
||||
<motion.div variants={fadeInUp} className="flex gap-3 pt-4">
|
||||
{step === 1 ? (
|
||||
<>
|
||||
<ButtomStepOne type={"owner"} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ButtomStepTwo formData={formData} isLoading={isLoading} setStep={setStep} type={"owner"} />
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
</motion.form>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
<AnimatePresence>{showOtpModal && <OtpDialog formData={formData} setShowOtpModal={setShowOtpModal} type={"owner"} />}</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
271
app/components/OwnerComponents/OwnerFormStepOne.js
Normal file
271
app/components/OwnerComponents/OwnerFormStepOne.js
Normal file
@ -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 (
|
||||
<>
|
||||
<motion.div variants={fadeInUp} className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
{t("register.firstName")} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||
<User className={`w-5 h-5 ${errors.firstName ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.firstName}
|
||||
onChange={(e) => {
|
||||
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")}
|
||||
/>
|
||||
</div>
|
||||
{errors.firstName && <p className="text-red-500 text-sm mt-1">{errors.firstName}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
{t("register.lastName")} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.lastName}
|
||||
onChange={(e) => {
|
||||
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 && <p className="text-red-500 text-sm mt-1">{errors.lastName}</p>}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={fadeInUp}>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
{t("register.email")} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||
<Mail className={`w-5 h-5 ${errors.email ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
|
||||
</div>
|
||||
<input
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => {
|
||||
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")}
|
||||
/>
|
||||
</div>
|
||||
{errors.email && <p className="text-red-500 text-sm mt-1">{errors.email}</p>}
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={fadeInUp}>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
{t("register.phone")} <span className="text-gray-500">({t("register.optional")})</span>
|
||||
</label>
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||
<Phone className="w-5 h-5 text-gray-400 group-focus-within:text-amber-500" />
|
||||
</div>
|
||||
<input
|
||||
type="tel"
|
||||
value={formData.phone}
|
||||
onChange={(e) => {
|
||||
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")}
|
||||
/>
|
||||
</div>
|
||||
{errors.phone && <p className="text-red-500 text-sm mt-1">{errors.phone}</p>}
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={fadeInUp}>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
{t("register.whatsapp")} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||
<MessageCircle className={`w-5 h-5 ${errors.whatsapp ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
|
||||
</div>
|
||||
<input
|
||||
type="tel"
|
||||
value={formData.whatsapp}
|
||||
onChange={(e) => {
|
||||
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")}
|
||||
/>
|
||||
</div>
|
||||
{errors.whatsapp && <p className="text-red-500 text-sm mt-1">{errors.whatsapp}</p>}
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={fadeInUp}>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
{t("register.phoneSecondary")} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||
<Phone className={`w-5 h-5 ${errors.phone2 ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
|
||||
</div>
|
||||
<input
|
||||
type="tel"
|
||||
value={formData.phone2}
|
||||
onChange={(e) => {
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
{errors.phone2 && <p className="text-red-500 text-sm mt-1">{errors.phone2}</p>}
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={fadeInUp}>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
{t("register.nationalNumber")} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||
<User className={`w-5 h-5 ${errors.nationalNumber ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.nationalNumber}
|
||||
onChange={(e) => {
|
||||
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")}
|
||||
/>
|
||||
</div>
|
||||
{errors.nationalNumber && <p className="text-red-500 text-sm mt-1">{errors.nationalNumber}</p>}
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={fadeInUp}>
|
||||
{type === "owner" ? (
|
||||
<>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
{t("register.ownerType")} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
value={formData.ownerType.toString()}
|
||||
onChange={(e) => {
|
||||
const selectedType = parseInt(e.target.value, 10);
|
||||
setFormData((prev) => ({ ...prev, ownerType: selectedType }));
|
||||
}}
|
||||
className="w-full py-3 px-4 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent appearance-none text-gray-900 cursor-pointer"
|
||||
>
|
||||
{Object.entries(OwnerTypeLabels).map(([value, label]) => (
|
||||
<option key={value} value={value} className="bg-gray-900 ">
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<motion.div variants={fadeInUp}>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
{t("register.customerType")} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
value={formData.customerType}
|
||||
onChange={(e) => setFormData({ ...formData, customerType: e.target.value })}
|
||||
className="w-full py-3 px-4 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white appearance-none cursor-pointer"
|
||||
>
|
||||
{Object.entries(CustomerTypeLabels).map(([value, label]) => (
|
||||
<option key={value} value={value} className="bg-gray-900 text-white">
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
<AnimatePresence>
|
||||
{isCompany && <UploadImage idImagePreviews={idImagePreviews} type={"license"} errors={errors.license} setIdImagePreviews={setIdImagePreviews} setIdImages={setIdImages} />}
|
||||
</AnimatePresence>
|
||||
|
||||
<motion.div variants={fadeInUp}>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
{t("register.password")} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||
<Lock className={`w-5 h-5 ${errors.password ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
|
||||
</div>
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={formData.password}
|
||||
onChange={(e) => {
|
||||
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")}
|
||||
/>
|
||||
<button type="button" onClick={() => setShowPassword(!showPassword)} className="absolute inset-y-0 left-0 pl-3 flex items-center">
|
||||
{showPassword ? <EyeOff className="w-5 h-5 text-gray-400" /> : <Eye className="w-5 h-5 text-gray-400" />}
|
||||
</button>
|
||||
</div>
|
||||
{errors.password && <p className="text-red-500 text-sm mt-1">{errors.password}</p>}
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={fadeInUp}>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
{t("register.confirmPassword")} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||
<Lock className={`w-5 h-5 ${errors.confirmPassword ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
|
||||
</div>
|
||||
<input
|
||||
type={showConfirmPassword ? "text" : "password"}
|
||||
value={formData.confirmPassword}
|
||||
onChange={(e) => {
|
||||
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")}
|
||||
/>
|
||||
<button type="button" onClick={() => setShowConfirmPassword(!showConfirmPassword)} className="absolute inset-y-0 left-0 pl-3 flex items-center">
|
||||
{showConfirmPassword ? <EyeOff className="w-5 h-5 text-gray-400" /> : <Eye className="w-5 h-5 text-gray-400" />}
|
||||
</button>
|
||||
{formData.confirmPassword && (
|
||||
<div className="absolute inset-y-0 left-12 flex items-center">
|
||||
{formData.password === formData.confirmPassword ? <CheckCircle className="w-5 h-5 text-green-500" /> : <XCircle className="w-5 h-5 text-red-500" />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{errors.confirmPassword && <p className="text-red-500 text-sm mt-1">{errors.confirmPassword}</p>}
|
||||
</motion.div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
12
app/components/OwnerComponents/OwnerFormStepTwo.js
Normal file
12
app/components/OwnerComponents/OwnerFormStepTwo.js
Normal file
@ -0,0 +1,12 @@
|
||||
import Policy from "./PolicyAgree";
|
||||
import UploadImage from "./UploadImage";
|
||||
export default function OwnerFormStepTwo({idImagePreviews,errors,setIdImagePreviews,setIdImages,setFormData,formData}) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<UploadImage idImagePreviews={idImagePreviews} type={"front"} errors={errors.front} setIdImagePreviews={setIdImagePreviews} setIdImages={setIdImages} />
|
||||
<UploadImage idImagePreviews={idImagePreviews} type={"back"} errors={errors.back} setIdImagePreviews={setIdImagePreviews} setIdImages={setIdImages} />
|
||||
<Policy setFormData={setFormData} formData={formData} type={"owner"} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
31
app/components/OwnerComponents/PolicyAgree.js
Normal file
31
app/components/OwnerComponents/PolicyAgree.js
Normal file
@ -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 (
|
||||
<>
|
||||
<motion.div variants={fadeInUp} className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="terms"
|
||||
checked={formData.agreeTerms}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
<label htmlFor="terms" className="text-sm text-gray-300">
|
||||
{t("register.agreeTerms")}{" "}
|
||||
<Link href="/terms" className={`${type === "owner" ? `text-amber-500 focus:ring-amber-500` : ` text-blue-500 focus:ring-blue-500`}`}>
|
||||
{t("register.termsOfUse")}
|
||||
</Link>{" "}
|
||||
{t("register.and")}{" "}
|
||||
<Link href="/privacy" className={`${type === "owner" ? `text-amber-500 focus:ring-amber-500` : ` text-blue-500 focus:ring-blue-500`}`}>
|
||||
{t("register.privacyPolicy")}
|
||||
</Link>
|
||||
</label>
|
||||
</motion.div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
85
app/components/OwnerComponents/UploadImage.js
Normal file
85
app/components/OwnerComponents/UploadImage.js
Normal file
@ -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 (
|
||||
<motion.div variants={fadeInUp} key={`${type}-upload-step`}>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
{getLabel()} {type === "license" ? <span className="text-gray-400">({t("register.optional")})</span> : <span className="text-red-500">*</span>}
|
||||
</label>
|
||||
{type === "license" && <div className="mb-2 text-xs text-gray-400">{t("register.owner.licenseHint")}</div>}
|
||||
<div
|
||||
onClick={() => 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"
|
||||
}`}
|
||||
>
|
||||
<input ref={fileInputRef} type="file" accept="image/*" onChange={(e) => handleImageUpload(type, e.target.files?.[0], t, setIdImagePreviews, setIdImages)} className="hidden" />
|
||||
{currentPreview ? (
|
||||
<div className="relative">
|
||||
<Image src={currentPreview} alt={getAltText()} width={200} height={120} className="mx-auto rounded-lg object-cover" />
|
||||
<button type="button" onClick={handleRemoveImage} className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 rounded-full flex items-center justify-center hover:bg-red-600 transition-colors">
|
||||
<X className="w-4 h-4 text-white" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Camera className="w-12 h-12 text-gray-500 mx-auto mb-3" />
|
||||
<p className="text-gray-400">{getClickText()}</p>
|
||||
<p className="text-xs text-gray-500 mt-2">JPEG, PNG, JPG • {t("register.upTo5MB")}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{errors && <p className="text-red-500 text-sm mt-1">{errors}</p>}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user