add Middleware + fixed all pages and fixed add propertise

This commit is contained in:
hamzaobed7
2026-08-06 22:26:35 +03:00
parent 7da4bf9f2b
commit 4d1b744516
30 changed files with 3618 additions and 4345 deletions

View File

@ -1,24 +1,15 @@
'use client';
"use client";
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { motion } from 'framer-motion';
import { useTranslation } from 'react-i18next';
import {
Mail,
Phone,
Shield,
ChevronLeft,
Loader2,
CheckCircle,
XCircle,
Send,
Key
} from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
import AuthService from '../services/AuthService';
import { sendEmailOTP, sendPhoneOTP, verifyEmail, verifyPhone } from '../utils/api';
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { motion } from "framer-motion";
import { useTranslation } from "react-i18next";
import { Mail, Phone, Shield, ChevronLeft, Loader2, CheckCircle, XCircle, Send, Key } from "lucide-react";
import toast, { Toaster } from "react-hot-toast";
import AuthService from "../services/AuthService";
import { sendEmailOTP, sendPhoneOTP, verifyEmail, verifyPhone } from "../utils/api";
import InteractiveBackground from "../components/Animation/Background";
export default function AccountVerificationPage() {
const router = useRouter();
@ -26,8 +17,8 @@ export default function AccountVerificationPage() {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [emailCode, setEmailCode] = useState('');
const [phoneCode, setPhoneCode] = useState('');
const [emailCode, setEmailCode] = useState("");
const [phoneCode, setPhoneCode] = useState("");
const [sendingEmailOTP, setSendingEmailOTP] = useState(false);
const [sendingPhoneOTP, setSendingPhoneOTP] = useState(false);
const [verifyingEmail, setVerifyingEmail] = useState(false);
@ -43,7 +34,7 @@ export default function AccountVerificationPage() {
setUser(authUser);
setIsLoading(false);
} else {
router.push('/login');
router.push("/login");
}
}, [router]);
@ -51,10 +42,10 @@ export default function AccountVerificationPage() {
setSendingEmailOTP(true);
try {
await sendEmailOTP();
toast.success(t('accountVerification.emailOtpSent'));
toast.success(t("accountVerification.emailOtpSent"));
setShowEmailInput(true);
} catch (err) {
toast.error(err.message || t('accountVerification.otpSendFailed'));
toast.error(err.message || t("accountVerification.otpSendFailed"));
} finally {
setSendingEmailOTP(false);
}
@ -62,7 +53,7 @@ export default function AccountVerificationPage() {
const handleVerifyEmail = async () => {
if (!emailCode.trim()) {
toast.error(t('accountVerification.otpRequired'));
toast.error(t("accountVerification.otpRequired"));
return;
}
setVerifyingEmail(true);
@ -70,9 +61,9 @@ export default function AccountVerificationPage() {
await verifyEmail(emailCode.trim());
setEmailVerified(true);
setShowEmailInput(false);
toast.success(t('accountVerification.emailVerifiedSuccess'));
toast.success(t("accountVerification.emailVerifiedSuccess"));
} catch (err) {
toast.error(err.message || t('accountVerification.otpInvalid'));
toast.error(err.message || t("accountVerification.otpInvalid"));
} finally {
setVerifyingEmail(false);
}
@ -82,10 +73,10 @@ export default function AccountVerificationPage() {
setSendingPhoneOTP(true);
try {
await sendPhoneOTP();
toast.success(t('accountVerification.phoneOtpSent'));
toast.success(t("accountVerification.phoneOtpSent"));
setShowPhoneInput(true);
} catch (err) {
toast.error(err.message || t('accountVerification.otpSendFailed'));
toast.error(err.message || t("accountVerification.otpSendFailed"));
} finally {
setSendingPhoneOTP(false);
}
@ -93,7 +84,7 @@ export default function AccountVerificationPage() {
const handleVerifyPhone = async () => {
if (!phoneCode.trim()) {
toast.error(t('accountVerification.otpRequired'));
toast.error(t("accountVerification.otpRequired"));
return;
}
setVerifyingPhone(true);
@ -101,74 +92,56 @@ export default function AccountVerificationPage() {
await verifyPhone(phoneCode.trim());
setPhoneVerified(true);
setShowPhoneInput(false);
toast.success(t('accountVerification.phoneVerifiedSuccess'));
toast.success(t("accountVerification.phoneVerifiedSuccess"));
} catch (err) {
toast.error(err.message || t('accountVerification.otpInvalid'));
toast.error(err.message || t("accountVerification.otpInvalid"));
} finally {
setVerifyingPhone(false);
}
};
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.1 }
}
transition: { staggerChildren: 0.1 },
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 }
visible: { opacity: 1, y: 0 },
};
return (
<div className="min-h-screen bg-gray-900 py-8" dir="rtl">
<div className="min-h-screen py-8" dir="rtl">
<Toaster position="top-center" reverseOrder={false} />
<div className="container mx-auto px-4 max-w-2xl">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="flex items-center gap-3 mb-8"
>
<Link
href="/settings"
className="p-2 rounded-xl hover:bg-gray-200 transition-colors text-gray-600"
>
<InteractiveBackground />
<div className="relative z-10 container mx-auto px-4 max-w-2xl">
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="flex items-center gap-3 mb-8">
<Link href="/settings" className="p-2 rounded-xl hover:bg-gray-200 transition-colors text-gray-600">
<ChevronLeft className="w-5 h-5" />
</Link>
<h1 className="text-2xl font-bold text-gray-900">{t('accountVerification.title')}</h1>
<h1 className="text-2xl font-bold text-gray-900">{t("accountVerification.title")}</h1>
</motion.div>
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="space-y-6"
>
<motion.div variants={containerVariants} initial="hidden" animate="visible" className="space-y-6">
<motion.div variants={itemVariants} className="bg-white rounded-2xl shadow-sm overflow-hidden">
<div className="px-6 py-4 border-b border-gray-100 flex items-center gap-3">
<Mail className="w-5 h-5 text-amber-600" />
<h2 className="text-lg font-semibold text-gray-800">{t('accountVerification.emailSection')}</h2>
<h2 className="text-lg font-semibold text-gray-800">{t("accountVerification.emailSection")}</h2>
{emailVerified && (
<span className="flex items-center gap-1 text-xs text-green-600 bg-green-50 px-2 py-0.5 rounded-full">
<CheckCircle className="w-3 h-3" />
{t('accountVerification.verified')}
{t("accountVerification.verified")}
</span>
)}
</div>
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<div>
<p className="text-sm text-gray-600">{user?.email || t('accountVerification.emailNotRegistered')}</p>
<p className="text-xs text-gray-400 mt-1">
{emailVerified
? t('accountVerification.emailVerifiedDesc')
: t('accountVerification.emailVerifyDesc')}
</p>
<p className="text-sm text-gray-600">{user?.email || t("accountVerification.emailNotRegistered")}</p>
<p className="text-xs text-gray-400 mt-1">{emailVerified ? t("accountVerification.emailVerifiedDesc") : t("accountVerification.emailVerifyDesc")}</p>
</div>
{!emailVerified && !showEmailInput && (
<button
@ -176,29 +149,21 @@ export default function AccountVerificationPage() {
disabled={sendingEmailOTP}
className="flex items-center gap-2 px-4 py-2 bg-amber-500 text-white rounded-xl hover:bg-amber-600 transition-colors text-sm disabled:opacity-50"
>
{sendingEmailOTP ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Send className="w-4 h-4" />
)}
{sendingEmailOTP ? t('accountVerification.sending') : t('accountVerification.sendOtp')}
{sendingEmailOTP ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />}
{sendingEmailOTP ? t("accountVerification.sending") : t("accountVerification.sendOtp")}
</button>
)}
</div>
{showEmailInput && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
className="space-y-3"
>
<p className="text-xs text-gray-500">{t('accountVerification.enter6DigitCode')}</p>
<motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} className="space-y-3">
<p className="text-xs text-gray-500">{t("accountVerification.enter6DigitCode")}</p>
<div className="flex gap-2">
<input
type="text"
value={emailCode}
onChange={(e) => setEmailCode(e.target.value)}
placeholder={t('accountVerification.otpPlaceholder')}
placeholder={t("accountVerification.otpPlaceholder")}
maxLength={6}
className="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none text-center text-lg tracking-widest"
/>
@ -207,20 +172,12 @@ export default function AccountVerificationPage() {
disabled={verifyingEmail}
className="px-6 py-3 bg-green-500 text-white rounded-xl hover:bg-green-600 transition-colors text-sm font-medium disabled:opacity-50 flex items-center gap-2"
>
{verifyingEmail ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Key className="w-4 h-4" />
)}
{t('accountVerification.verify')}
{verifyingEmail ? <Loader2 className="w-4 h-4 animate-spin" /> : <Key className="w-4 h-4" />}
{t("accountVerification.verify")}
</button>
</div>
<button
onClick={handleSendEmailOTP}
disabled={sendingEmailOTP}
className="text-xs text-amber-600 hover:text-amber-700"
>
{t('accountVerification.resendCode')}
<button onClick={handleSendEmailOTP} disabled={sendingEmailOTP} className="text-xs text-amber-600 hover:text-amber-700">
{t("accountVerification.resendCode")}
</button>
</motion.div>
)}
@ -228,7 +185,7 @@ export default function AccountVerificationPage() {
{emailVerified && (
<div className="flex items-center gap-2 text-green-600 bg-green-50 px-4 py-3 rounded-xl">
<CheckCircle className="w-5 h-5" />
<span className="text-sm font-medium">{t('accountVerification.emailVerifiedSuccess')}</span>
<span className="text-sm font-medium">{t("accountVerification.emailVerifiedSuccess")}</span>
</div>
)}
</div>
@ -237,23 +194,19 @@ export default function AccountVerificationPage() {
<motion.div variants={itemVariants} className="bg-white rounded-2xl shadow-sm overflow-hidden">
<div className="px-6 py-4 border-b border-gray-100 flex items-center gap-3">
<Phone className="w-5 h-5 text-amber-600" />
<h2 className="text-lg font-semibold text-gray-800">{t('accountVerification.phoneSection')}</h2>
<h2 className="text-lg font-semibold text-gray-800">{t("accountVerification.phoneSection")}</h2>
{phoneVerified && (
<span className="flex items-center gap-1 text-xs text-green-600 bg-green-50 px-2 py-0.5 rounded-full">
<CheckCircle className="w-3 h-3" />
{t('accountVerification.verified')}
{t("accountVerification.verified")}
</span>
)}
</div>
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<div>
<p className="text-sm text-gray-600">{user?.phone || t('accountVerification.phoneNotRegistered')}</p>
<p className="text-xs text-gray-400 mt-1">
{phoneVerified
? t('accountVerification.phoneVerifiedDesc')
: t('accountVerification.phoneVerifyDesc')}
</p>
<p className="text-sm text-gray-600">{user?.phone || t("accountVerification.phoneNotRegistered")}</p>
<p className="text-xs text-gray-400 mt-1">{phoneVerified ? t("accountVerification.phoneVerifiedDesc") : t("accountVerification.phoneVerifyDesc")}</p>
</div>
{!phoneVerified && !showPhoneInput && (
<button
@ -261,29 +214,21 @@ export default function AccountVerificationPage() {
disabled={sendingPhoneOTP}
className="flex items-center gap-2 px-4 py-2 bg-amber-500 text-white rounded-xl hover:bg-amber-600 transition-colors text-sm disabled:opacity-50"
>
{sendingPhoneOTP ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Send className="w-4 h-4" />
)}
{sendingPhoneOTP ? t('accountVerification.sending') : t('accountVerification.sendOtp')}
{sendingPhoneOTP ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />}
{sendingPhoneOTP ? t("accountVerification.sending") : t("accountVerification.sendOtp")}
</button>
)}
</div>
{showPhoneInput && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
className="space-y-3"
>
<p className="text-xs text-gray-500">{t('accountVerification.enter6DigitCodePhone')}</p>
<motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} className="space-y-3">
<p className="text-xs text-gray-500">{t("accountVerification.enter6DigitCodePhone")}</p>
<div className="flex gap-2">
<input
type="text"
value={phoneCode}
onChange={(e) => setPhoneCode(e.target.value)}
placeholder={t('accountVerification.otpPlaceholder')}
placeholder={t("accountVerification.otpPlaceholder")}
maxLength={6}
className="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none text-center text-lg tracking-widest"
/>
@ -292,20 +237,12 @@ export default function AccountVerificationPage() {
disabled={verifyingPhone}
className="px-6 py-3 bg-green-500 text-white rounded-xl hover:bg-green-600 transition-colors text-sm font-medium disabled:opacity-50 flex items-center gap-2"
>
{verifyingPhone ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Key className="w-4 h-4" />
)}
{t('accountVerification.verify')}
{verifyingPhone ? <Loader2 className="w-4 h-4 animate-spin" /> : <Key className="w-4 h-4" />}
{t("accountVerification.verify")}
</button>
</div>
<button
onClick={handleSendPhoneOTP}
disabled={sendingPhoneOTP}
className="text-xs text-amber-600 hover:text-amber-700"
>
{t('accountVerification.resendCode')}
<button onClick={handleSendPhoneOTP} disabled={sendingPhoneOTP} className="text-xs text-amber-600 hover:text-amber-700">
{t("accountVerification.resendCode")}
</button>
</motion.div>
)}
@ -313,7 +250,7 @@ export default function AccountVerificationPage() {
{phoneVerified && (
<div className="flex items-center gap-2 text-green-600 bg-green-50 px-4 py-3 rounded-xl">
<CheckCircle className="w-5 h-5" />
<span className="text-sm font-medium">{t('accountVerification.phoneVerifiedSuccess')}</span>
<span className="text-sm font-medium">{t("accountVerification.phoneVerifiedSuccess")}</span>
</div>
)}
</div>
@ -322,20 +259,20 @@ export default function AccountVerificationPage() {
<motion.div variants={itemVariants} className="bg-gradient-to-br from-amber-500 to-amber-600 rounded-2xl p-6 text-white">
<div className="flex items-center gap-3 mb-3">
<Shield className="w-6 h-6" />
<h3 className="text-lg font-semibold">{t('accountVerification.whyVerify')}</h3>
<h3 className="text-lg font-semibold">{t("accountVerification.whyVerify")}</h3>
</div>
<ul className="space-y-2 text-sm text-amber-50">
<ul className="space-y-2 text-sm " style={{ color: "#0f172a", fontWeight: "400" }}>
<li className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 flex-shrink-0" />
{t('accountVerification.reason1')}
{t("accountVerification.reason1")}
</li>
<li className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 flex-shrink-0" />
{t('accountVerification.reason2')}
{t("accountVerification.reason2")}
</li>
<li className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 flex-shrink-0" />
{t('accountVerification.reason3')}
{t("accountVerification.reason3")}
</li>
</ul>
</motion.div>

View File

@ -8,6 +8,7 @@ import { ShieldAlert, LogOut, MessageSquare, Send, Loader2 } from "lucide-react"
import toast, { Toaster } from "react-hot-toast";
import AuthService from "../services/AuthService";
import { sendGeneralReport } from "../utils/api";
import InteractiveBackground from "../components/Animation/Background";
export default function BlockedPage() {
const { t } = useTranslation();
@ -49,10 +50,10 @@ export default function BlockedPage() {
};
return (
<div className="min-h-screen bg-gray-900 flex items-center justify-center p-4" dir="rtl">
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4" dir="rtl">
<Toaster position="top-center" reverseOrder={false} />
<motion.div initial={{ opacity: 0, y: 24 }} animate={{ opacity: 1, y: 0 }} className="w-full max-w-5xl">
<InteractiveBackground />
<motion.div initial={{ opacity: 0, y: 24 }} animate={{ opacity: 1, y: 0 }} className="w-full max-w-5xl relative z-10">
<div className="text-center mb-10">
<motion.div initial={{ scale: 0.9 }} animate={{ scale: 1 }} className="w-24 h-24 bg-red-100 rounded-3xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-red-100">
<ShieldAlert className="w-12 h-12 text-red-600" />
@ -61,7 +62,7 @@ export default function BlockedPage() {
<p className="text-gray-600 text-lg max-w-2xl mx-auto">{t("blocked.description")}</p>
</div>
<div className="grid md:grid-cols-2 gap-6">
<div className="grid md:grid-cols-2 gap-6 relative z-10 ">
<motion.div
initial={{ opacity: 0, x: -24 }}
animate={{ opacity: 1, x: 0 }}

View File

@ -144,13 +144,7 @@ export default function BookedPropertiesPage() {
}
}, []);
if (loading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center" dir="rtl">
<Loader2 className="w-12 h-12 text-amber-500 animate-spin" />
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">

View File

@ -1,18 +1,19 @@
'use client';
"use client";
import { useState } from 'react';
import { motion } from 'framer-motion';
import { useRouter } from 'next/navigation';
import toast, { Toaster } from 'react-hot-toast';
import { Lock, Eye, EyeOff, ArrowLeft, Shield } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { changePassword } from '../utils/api';
import AuthService from '../services/AuthService';
import { useState } from "react";
import { motion } from "framer-motion";
import { useRouter } from "next/navigation";
import toast, { Toaster } from "react-hot-toast";
import { Lock, Eye, EyeOff, ArrowLeft, Shield } from "lucide-react";
import { useTranslation } from "react-i18next";
import { changePassword } from "../utils/api";
import AuthService from "../services/AuthService";
import InteractiveBackground from "../components/Animation/Background";
export default function ChangePasswordPage() {
const router = useRouter();
const { t } = useTranslation();
const [form, setForm] = useState({ oldPassword: '', newPassword: '', confirmPassword: '' });
const [form, setForm] = useState({ oldPassword: "", newPassword: "", confirmPassword: "" });
const [show, setShow] = useState({ old: false, new: false, confirm: false });
const [isLoading, setIsLoading] = useState(false);
@ -24,162 +25,124 @@ export default function ChangePasswordPage() {
e.preventDefault();
if (!AuthService.isAuthenticated()) {
toast.error(t('changePassword.loginRequired'));
router.push('/login');
toast.error(t("changePassword.loginRequired"));
router.push("/login");
return;
}
if (form.newPassword !== form.confirmPassword) {
toast.error(t('changePassword.passwordsDoNotMatch'));
toast.error(t("changePassword.passwordsDoNotMatch"));
return;
}
if (form.newPassword.length < 6) {
toast.error(t('validation.passwordMinLength'));
toast.error(t("validation.passwordMinLength"));
return;
}
setIsLoading(true);
try {
await changePassword(form.oldPassword, form.newPassword);
toast.success(t('changePassword.changeSuccess'));
setTimeout(() => router.push('/profile'), 1200);
toast.success(t("changePassword.changeSuccess"));
setTimeout(() => router.push("/profile"), 1200);
} catch (err) {
toast.error(err?.message || t('changePassword.changeFailed'));
toast.error(err?.message || t("changePassword.changeFailed"));
} finally {
setIsLoading(false);
}
};
const inputClass = "w-full pr-12 pl-4 py-3 bg-gray-50 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400 transition-all";
const inputClass =
"w-full pr-12 pl-4 py-3 bg-gray-50 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400 transition-all";
return (
<div className="min-h-screen bg-gray-900 flex items-center justify-center p-4">
<div className="min-h-screen relative z-10 flex items-center justify-center p-4" style={{ backgroundColor: "#0f172a" }}>
<Toaster position="top-center" reverseOrder={false} />
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="w-full max-w-md"
>
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
className="mb-6"
>
<button
onClick={() => router.push('/profile')}
className="flex items-center gap-2 text-gray-600 hover:text-amber-600 transition-colors"
>
<InteractiveBackground />
<motion.div initial={{ opacity: 0, y: 30 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }} className="w-full max-w-md relative z-10 ">
<motion.div initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} className="mb-6 relative z-10">
<button onClick={() => router.push("/profile")} className="flex items-center relative z-10 gap-2 text-gray-600 hover:text-amber-600 transition-colors">
<ArrowLeft className="w-5 h-5" />
<span>{t('changePassword.backToProfile')}</span>
<span>{t("changePassword.backToProfile")}</span>
</button>
</motion.div>
<div className="bg-white rounded-3xl shadow-xl overflow-hidden">
<div className=" rounded-3xl shadow-xl overflow-hidden" style={{ backgroundColor: "#1e293b" }}>
<div className="bg-gradient-to-l from-amber-500 to-amber-600 p-8 text-center">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', stiffness: 200 }}
transition={{ type: "spring", stiffness: 200 }}
className="w-16 h-16 bg-white/20 rounded-full flex items-center justify-center mx-auto mb-4"
>
<Shield className="w-8 h-8 text-white" />
</motion.div>
<motion.h1
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
className="text-3xl font-bold text-white mb-2"
>
{t('changePassword.title')}
<motion.h1 initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} className="text-3xl font-bold text-white mb-2">
{t("changePassword.title")}
</motion.h1>
<motion.p
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.1 }}
className="text-amber-100"
>
{t('changePassword.subtitle')}
<motion.p initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} transition={{ delay: 0.1 }} className="text-amber-100">
{t("changePassword.subtitle")}
</motion.p>
</div>
<form onSubmit={handleSubmit} className="p-8 space-y-6">
<form onSubmit={handleSubmit} className="p-8 space-y-6 " style={{ backgroundColor: "#0f172a" }}>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
{t('changePassword.currentPasswordLabel')}
</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{t("changePassword.currentPasswordLabel")}</label>
<div className="relative">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className="w-5 h-5 text-gray-400" />
</div>
<input
type={show.old ? 'text' : 'password'}
type={show.old ? "text" : "password"}
value={form.oldPassword}
onChange={handleChange('oldPassword')}
onChange={handleChange("oldPassword")}
className={inputClass}
placeholder={t('changePassword.currentPasswordPlaceholder')}
placeholder={t("changePassword.currentPasswordPlaceholder")}
required
/>
<button
type="button"
onClick={() => toggleShow('old')}
className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600"
>
<button type="button" onClick={() => toggleShow("old")} className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600">
{show.old ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
{t('changePassword.newPasswordLabel')}
</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{t("changePassword.newPasswordLabel")}</label>
<div className="relative">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className="w-5 h-5 text-gray-400" />
</div>
<input
type={show.new ? 'text' : 'password'}
type={show.new ? "text" : "password"}
value={form.newPassword}
onChange={handleChange('newPassword')}
onChange={handleChange("newPassword")}
className={inputClass}
placeholder={t('changePassword.newPasswordPlaceholder')}
placeholder={t("changePassword.newPasswordPlaceholder")}
required
minLength={6}
/>
<button
type="button"
onClick={() => toggleShow('new')}
className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600"
>
<button type="button" onClick={() => toggleShow("new")} className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600">
{show.new ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
{t('changePassword.confirmNewPasswordLabel')}
</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{t("changePassword.confirmNewPasswordLabel")}</label>
<div className="relative">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className="w-5 h-5 text-gray-400" />
</div>
<input
type={show.confirm ? 'text' : 'password'}
type={show.confirm ? "text" : "password"}
value={form.confirmPassword}
onChange={handleChange('confirmPassword')}
onChange={handleChange("confirmPassword")}
className={inputClass}
placeholder={t('changePassword.confirmPasswordPlaceholder')}
placeholder={t("changePassword.confirmPasswordPlaceholder")}
required
minLength={6}
/>
<button
type="button"
onClick={() => toggleShow('confirm')}
className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600"
>
<button type="button" onClick={() => toggleShow("confirm")} className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600">
{show.confirm ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
@ -195,10 +158,10 @@ export default function ChangePasswordPage() {
{isLoading ? (
<div className="flex items-center justify-center gap-2">
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
<span>{t('changePassword.saving')}</span>
<span>{t("changePassword.saving")}</span>
</div>
) : (
t('changePassword.changePassword')
t("changePassword.changePassword")
)}
</motion.button>
</form>

View File

@ -5,6 +5,7 @@ import { usePathname } from "next/navigation";
import { Home, Building, Calendar, CreditCard, Briefcase, BookOpen } from "lucide-react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { label } from "framer-motion/client";
export default function BottomNav({ isOwner, isOwnerOrAgent }) {
const { t } = useTranslation();
@ -19,8 +20,15 @@ export default function BottomNav({ isOwner, isOwnerOrAgent }) {
const items = [
{ href: "/", label: t("home"), icon: Home },
{ href: "/properties", label: t("ourProperties"), icon: Building },
...(isOwnerOrAgent ? [{ href: "/owner/properties", label: t("myProperties"), icon: Briefcase }] : []),
{ href: bookingsHref, label: t("reservations"), icon: Calendar },
...(isOwnerOrAgent
? [
{ href: "/owner/properties", label: t("myProperties"), icon: Briefcase },
{ href: "/owner/bookings", label: "عقاراتي المحجوزة", icon: Briefcase },
{ href: "/reservations", label: "حجوزاتي", icon: Calendar },
]
: [{ href: "/booked-properties", label: "حجوزاتي", icon: Briefcase }]),
{ href: bookingsHref, label: "طلبات الحجز", icon: Calendar },
{ href: "/payments", label: t("payments"), icon: CreditCard },
...(isOwnerOrAgent ? [{ href: "/owner/account-book", label: t("accountBook"), icon: BookOpen }] : []),
];
@ -46,9 +54,7 @@ export default function BottomNav({ isOwner, isOwnerOrAgent }) {
<div className="relative">
<Icon className="w-6 h-6" />
{it.badge > 0 && (
<div className="absolute -top-2 -right-2 bg-red-500 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center">
{it.badge > 9 ? "9+" : it.badge}
</div>
<div className="absolute -top-2 -right-2 bg-red-500 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center">{it.badge > 9 ? "9+" : it.badge}</div>
)}
</div>
<div className="mt-1 text-xs text-center" aria-hidden>

View File

@ -76,9 +76,9 @@ export default function Owner() {
try {
const licenseImage = isCompany ? idImages.license : null;
const res = await addOwner(payload, idImages.front, idImages.back);
const res = await addOwner(payload, idImages.front, idImages.back,licenseImage);
if (res.status === 200 || res.ok) {
toast.success(res.message || t("register.accountCreated"), { duration: 4000 });
toast.success(res.message || t("register.accountCreated")||res.data[0].message , { duration: 4000 });
const loginRes = await loginWithEmail(formData.email, formData.password);
if (loginRes.status === 206) {
const otpToken = loginRes.data;
@ -150,15 +150,7 @@ export default function Owner() {
</>
)}
<motion.div variants={fadeInUp} className="flex gap-3 pt-4">
{step === 1 ? (
<>
<ButtomStepOne type={"owner"} />
</>
) : (
<>
<ButtomStepTwo formData={formData} isLoading={isLoading} setStep={setStep} type={"owner"} />
</>
)}
{step === 1 ? ( <> <ButtomStepOne type={"owner"} /></>) : ( <> <ButtomStepTwo formData={formData} isLoading={isLoading} setStep={setStep} type={"owner"} /> </>)}
</motion.div>
</motion.form>
</div>

View File

@ -18,7 +18,21 @@ const City = Object.freeze({
QAMISHLI: 'القامشلي',
RURAL_DAMASCUS: 'ريف دمشق',
});
export const CityEnum = Object.freeze({
DAMASCUS: 0,
ALEPPO: 1,
HOMS: 2,
LATAKIA: 3,
DARAA: 4,
TARTOUS: 5,
SUWEIDA: 6,
DEIR_EZZOR: 7,
RAQQA: 8,
IDLIB: 9,
HASAKAH: 10,
Qamishli: 11,
RURAL_DAMASCUS: 12,
});
// All cities as a flat array
const CitiesList = Object.freeze(Object.values(City));
@ -51,4 +65,6 @@ const CityTranslationKeys = Object.freeze({
[City.RURAL_DAMASCUS]: 'city.ruralDamascus',
});
export { City, CitiesList, extractCity, CityTranslationKeys };
export { City, CitiesList, extractCity, CityTranslationKeys, };

View File

@ -32,7 +32,7 @@ export default function FAQPage() {
};
return (
<div className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12" dir="rtl">
<div className="min-h-screen py-12" dir="rtl">
<div className="container mx-auto px-4 max-w-4xl">
<motion.div
initial={{ opacity: 0, y: -20 }}

View File

@ -1,14 +1,15 @@
'use client';
"use client";
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useRouter } from 'next/navigation';
import { motion } from 'framer-motion';
import Link from 'next/link';
import Image from 'next/image';
import { Heart, MapPin, Bed, Bath, Square, X, ImageIcon } from 'lucide-react';
import { useFavorites } from '@/app/contexts/FavoritesContext';
import AuthService from '@/app/services/AuthService';
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/navigation";
import { motion } from "framer-motion";
import Link from "next/link";
import Image from "next/image";
import { Heart, MapPin, Bed, Bath, Square, X, ImageIcon } from "lucide-react";
import { useFavorites } from "@/app/contexts/FavoritesContext";
import AuthService from "@/app/services/AuthService";
import Loading from "../loading";
export default function FavoritesPage() {
const { t } = useTranslation();
@ -25,38 +26,28 @@ export default function FavoritesPage() {
}, [router]);
const formatCurrency = (amount) => {
return amount?.toLocaleString() + ' ' + t('currency-syp-suffix');
return amount?.toLocaleString() + " " + t("currency-syp-suffix");
};
if (favoritesLoading && favorites.length === 0) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<p className="text-gray-600">{t('loading')}</p>
</div>
</div>
);
return <Loading />;
}
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="container mx-auto px-4 max-w-6xl">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t('favorites')}</h1>
<p className="text-gray-600">{t('saved-properties')}</p>
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t("favorites")}</h1>
<p className="text-gray-600">{t("saved-properties")}</p>
</div>
{favorites.length === 0 ? (
<div className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
<Heart className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-700 mb-2">{t('no-favorites')}</h3>
<p className="text-gray-500 mb-6">{t('add-favorites-hint')}</p>
<Link
href="/properties"
className="inline-flex items-center gap-2 bg-amber-500 text-white px-6 py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors"
>
{t('browse-properties')}
<h3 className="text-xl font-bold text-gray-700 mb-2">{t("no-favorites")}</h3>
<p className="text-gray-500 mb-6">{t("add-favorites-hint")}</p>
<Link href="/properties" className="inline-flex items-center gap-2 bg-amber-500 text-white px-6 py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors">
{t("browse-properties")}
</Link>
</div>
) : (
@ -70,13 +61,7 @@ export default function FavoritesPage() {
>
<div className="relative h-48 bg-gray-100">
{property.images && property.images[0] ? (
<Image
src={property.images[0]}
alt={property.title}
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
<Image src={property.images[0]} alt={property.title} fill className="object-cover" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" />
) : (
<div className="w-full h-full flex items-center justify-center">
<ImageIcon className="w-12 h-12 text-gray-400" />
@ -95,7 +80,7 @@ export default function FavoritesPage() {
<div>
<div className="flex items-center gap-2 mb-2">
<span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-lg text-xs font-medium">
{property.type === 'apartment' ? t('buildingType.apartment') : property.type === 'villa' ? t('buildingType.villa') : t('buildingType.house')}
{property.type === "apartment" ? t("buildingType.apartment") : property.type === "villa" ? t("buildingType.villa") : t("buildingType.house")}
</span>
</div>
<h3 className="font-bold text-gray-900 mb-1 line-clamp-1">{property.title}</h3>
@ -108,7 +93,7 @@ export default function FavoritesPage() {
</div>
<div className="text-left">
<div className="text-xl font-bold text-gray-900">{formatCurrency(property.price)}</div>
<div className="text-xs text-gray-500">/{property.priceUnit === 'daily' ? t('day') : t('month')}</div>
<div className="text-xs text-gray-500">/{property.priceUnit === "daily" ? t("day") : t("month")}</div>
</div>
</div>
@ -129,11 +114,8 @@ export default function FavoritesPage() {
</div>
</div>
<Link
href={`/property/${property.id}`}
className="block w-full bg-amber-500 text-white py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors text-center"
>
{t('viewDetails')}
<Link href={`/property/${property.id}`} className="block w-full bg-amber-500 text-white py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors text-center">
{t("viewDetails")}
</Link>
</div>
</motion.div>
@ -143,4 +125,4 @@ export default function FavoritesPage() {
</div>
</div>
);
}
}

View File

@ -8,6 +8,7 @@ import toast, { Toaster } from 'react-hot-toast';
import { Mail, Key, Lock, CheckCircle, ArrowLeft, RefreshCw } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { requestForgetPasswordOtp, verifyForgetPasswordOtp } from '../utils/api';
import InteractiveBackground from '../components/Animation/Background';
export default function ForgotPasswordPage() {
const router = useRouter();
@ -68,6 +69,7 @@ export default function ForgotPasswordPage() {
return (
<div className="min-h-screen flex items-center justify-center p-4 relative overflow-hidden">
<Toaster position="top-center" reverseOrder={false} />
<InteractiveBackground/>
<div className="absolute inset-0 overflow-hidden">
<div className="absolute -top-40 -right-40 w-80 h-80 bg-amber-400 rounded-full opacity-20 blur-3xl animate-pulse"></div>
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-orange-400 rounded-full opacity-20 blur-3xl animate-pulse delay-1000"></div>

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,7 @@ import toast, { Toaster } from 'react-hot-toast';
import { getCustomerRatings } from '../utils/ratings';
import AuthService from '../services/AuthService';
import StarRating from '../components/ratings/StarRating';
import { useEffect, useState } from 'react';
const RATING_FIELDS = [
{ key: 'cleanRating', label: 'ratings.cleanliness' },
@ -99,13 +100,7 @@ export default function MyRatesPage() {
}
}
if (loading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center" dir="rtl">
<Loader2 className="w-12 h-12 text-amber-500 animate-spin" />
</div>
);
}
const overall = calcOverall(ratings);

View File

@ -1,12 +1,13 @@
'use client';
"use client";
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useRouter } from 'next/navigation';
import { motion, AnimatePresence } from 'framer-motion';
import { Bell, CheckCircle, XCircle, Calendar, MessageCircle, CheckCheck, Loader2 } from 'lucide-react';
import AuthService from '@/app/services/AuthService';
import { getUserNotifications } from '@/app/utils/api';
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion";
import { Bell, CheckCircle, XCircle, Calendar, MessageCircle, CheckCheck, Loader2 } from "lucide-react";
import AuthService from "@/app/services/AuthService";
import { getUserNotifications } from "@/app/utils/api";
import Loading from "../loading";
export default function NotificationsPage() {
const { t } = useTranslation();
@ -18,7 +19,7 @@ export default function NotificationsPage() {
useEffect(() => {
if (!AuthService.isAuthenticated()) {
router.push('/login');
router.push("/login");
return;
}
fetchNotifications();
@ -33,36 +34,27 @@ export default function NotificationsPage() {
setNotifications(items);
setUnreadCount(items.length);
} catch (err) {
console.error('Error fetching notifications:', err);
setError(err.message || t('fetch-notifications-failed'));
console.error("Error fetching notifications:", err);
setError(err.message || t("fetch-notifications-failed"));
} finally {
setIsLoading(false);
}
};
const markAsRead = (id) => {
setNotifications(prev =>
prev.map(n => (n.id === id ? { ...n, read: true } : n))
);
setUnreadCount(prev => Math.max(0, prev - 1));
setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, read: true } : n)));
setUnreadCount((prev) => Math.max(0, prev - 1));
};
const markAllAsRead = () => {
setNotifications(prev => prev.map(n => ({ ...n, read: true })));
setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
setUnreadCount(0);
};
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="text-center"
>
<Loader2 className="w-12 h-12 text-amber-500 mx-auto mb-4 animate-spin" />
<p className="text-gray-600">{t('loading')}</p>
</motion.div>
<div>
<Loading />
</div>
);
}
@ -70,13 +62,9 @@ export default function NotificationsPage() {
if (error) {
return (
<div className="min-h-screen flex items-center justify-center">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center"
>
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="text-center">
<XCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-700 mb-2">{t('loading-error')}</h3>
<h3 className="text-xl font-bold text-gray-700 mb-2">{t("loading-error")}</h3>
<p className="text-gray-500 mb-4">{error}</p>
<motion.button
whileHover={{ scale: 1.05 }}
@ -84,7 +72,7 @@ export default function NotificationsPage() {
onClick={fetchNotifications}
className="px-6 py-2 bg-amber-500 text-white rounded-xl font-medium hover:bg-amber-600 transition-colors"
>
{t('retry')}
{t("retry")}
</motion.button>
</motion.div>
</div>
@ -94,16 +82,10 @@ export default function NotificationsPage() {
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="container mx-auto px-4 max-w-4xl">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="flex justify-between items-center mb-8"
>
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="flex justify-between items-center mb-8">
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t('notifications')}</h1>
<p className="text-gray-600">
{unreadCount > 0 ? t('unread-notifications', { count: unreadCount }) : t('all-notifications-read')}
</p>
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t("notifications")}</h1>
<p className="text-gray-600">{unreadCount > 0 ? t("unread-notifications", { count: unreadCount }) : t("all-notifications-read")}</p>
</div>
{unreadCount > 0 && (
<motion.button
@ -113,20 +95,16 @@ export default function NotificationsPage() {
className="flex items-center gap-2 px-4 py-2 bg-white border border-gray-200 rounded-xl text-sm font-medium text-gray-700 hover:bg-gray-50 transition-all shadow-sm"
>
<CheckCheck className="w-4 h-4 text-amber-500" />
{t('mark-all-read')}
{t("mark-all-read")}
</motion.button>
)}
</motion.div>
{notifications.length === 0 ? (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300"
>
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
<Bell className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-700 mb-2">{t('no-notifications')}</h3>
<p className="text-gray-500">{t('notifications-empty-hint')}</p>
<h3 className="text-xl font-bold text-gray-700 mb-2">{t("no-notifications")}</h3>
<p className="text-gray-500">{t("notifications-empty-hint")}</p>
</motion.div>
) : (
<div className="space-y-4">
@ -137,32 +115,20 @@ export default function NotificationsPage() {
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, x: 100 }}
transition={{ delay: index * 0.05, type: 'spring', stiffness: 100 }}
transition={{ delay: index * 0.05, type: "spring", stiffness: 100 }}
whileHover={{ scale: 1.01 }}
onClick={() => markAsRead(notification.id)}
className={`bg-white rounded-2xl shadow-sm border transition-all hover:shadow-md cursor-pointer ${
!notification.read ? 'border-amber-200 bg-amber-50/50' : 'border-gray-200'
}`}
className={`bg-white rounded-2xl shadow-sm border transition-all hover:shadow-md cursor-pointer ${!notification.read ? "border-amber-200 bg-amber-50/50" : "border-gray-200"}`}
>
<div className="p-5 flex gap-4">
<div className={`w-12 h-12 rounded-full flex items-center justify-center shrink-0 ${
!notification.read ? 'bg-amber-100' : 'bg-gray-100'
}`}>
{!notification.read ? (
<Bell className="w-6 h-6 text-amber-600" />
) : (
<CheckCircle className="w-6 h-6 text-gray-400" />
)}
<div className={`w-12 h-12 rounded-full flex items-center justify-center shrink-0 ${!notification.read ? "bg-amber-100" : "bg-gray-100"}`}>
{!notification.read ? <Bell className="w-6 h-6 text-amber-600" /> : <CheckCircle className="w-6 h-6 text-gray-400" />}
</div>
<div className="flex-1 min-w-0">
<div className="flex justify-between items-start gap-4">
<div className="min-w-0">
<h3 className={`text-base ${!notification.read ? 'font-bold text-gray-900' : 'font-medium text-gray-700'}`}>
{notification.title}
</h3>
{notification.message && (
<p className="text-gray-500 text-sm mt-1 line-clamp-2">{notification.message}</p>
)}
<h3 className={`text-base ${!notification.read ? "font-bold text-gray-900" : "font-medium text-gray-700"}`}>{notification.title}</h3>
{notification.message && <p className="text-gray-500 text-sm mt-1 line-clamp-2">{notification.message}</p>}
<div className="flex items-center gap-2 mt-2">
{notification.date && (
<span className="text-xs text-gray-400 flex items-center gap-1">
@ -178,9 +144,7 @@ export default function NotificationsPage() {
)}
</div>
</div>
{!notification.read && (
<span className="w-2.5 h-2.5 bg-amber-500 rounded-full shrink-0 mt-2" />
)}
{!notification.read && <span className="w-2.5 h-2.5 bg-amber-500 rounded-full shrink-0 mt-2" />}
</div>
</div>
</div>

View File

@ -76,7 +76,7 @@ export default function OnboardingPage() {
};
return (
<div dir={i18n.language === 'ar' ? 'rtl' : 'ltr'} className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center p-4 relative overflow-hidden">
<div dir={i18n.language === 'ar' ? 'rtl' : 'ltr'} className="min-h-screen flex items-center justify-center p-4 relative overflow-hidden">
<div className="absolute inset-0 overflow-hidden">
{[...Array(30)].map((_, i) => (
<motion.div
@ -144,7 +144,7 @@ export default function OnboardingPage() {
transition={{ type: 'spring', stiffness: 200, delay: 0.1 }}
className="w-24 h-24 mx-auto bg-white/20 rounded-3xl flex items-center justify-center backdrop-blur-sm"
>
<IconComponent className="w-12 h-12 text-white" />
<IconComponent className="w-12 h-12 text-amber-50" />
</motion.div>
</div>
</div>
@ -154,7 +154,7 @@ export default function OnboardingPage() {
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.15 }}
className="text-3xl font-bold text-white mb-4"
className="text-3xl font-bold text-amber-50 mb-4"
>
{step.title}
</motion.h1>

View File

@ -7,6 +7,7 @@ import { DollarSign, TrendingUp, Calendar, Loader2, Star } from "lucide-react";
import toast, { Toaster } from "react-hot-toast";
import AuthService from "@/app/services/AuthService";
import { getOwnerStatistics } from "@/app/utils/api";
import Loading from "@/app/loading";
const StatCard = ({ title, value, icon: Icon, color, subtitle, isNA }) => (
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-all">
@ -64,9 +65,7 @@ export default function OwnerAccountBookPage() {
totalRevenue: data.totalRevenue ?? null,
totalReservations: data.totalReservations ?? null,
activeProperties: data.activeProperties ?? null,
financialRevenue: data.financialRevenue ?? data.totalRevenue ?? null,
financialCommission: data.financialCommission ?? null,
financialBalance: data.financialBalance ?? null,
directRevenue: data.directRevenue ?? null,
@ -106,14 +105,7 @@ export default function OwnerAccountBookPage() {
const isNA = (val) => val === null || val === undefined || val === "";
if (isLoading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<Loader2 className="w-12 h-12 text-amber-500 animate-spin mx-auto mb-4" />
<p className="text-gray-600">{t("accountBook.loading")}</p>
</div>
</div>
);
return <Loading />;
}
return (

View File

@ -1,8 +1,8 @@
'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 (

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,93 +1,69 @@
"use client";
import { useEffect, useState, useCallback } from 'react';
import Link from 'next/link';
import { motion } from 'framer-motion';
import { useTranslation } from 'react-i18next';
import {
Home,
MapPin,
Phone,
ShieldCheck,
CreditCard,
Banknote,
Building,
ArrowLeftRight,
Loader2,
Check,
X,
Calendar,
Clock,
LogIn,
Lock,
Info,
Landmark,
Wallet,
} from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
import AuthService from '@/app/services/AuthService';
import { payDeposit, getMyTransaction, getPaymentTypes } from '@/app/utils/api';
import { useEffect, useState, useCallback } from "react";
import Link from "next/link";
import { motion } from "framer-motion";
import { useTranslation } from "react-i18next";
import { Home, MapPin, Phone, ShieldCheck, CreditCard, Banknote, Building, ArrowLeftRight, Loader2, Check, X, Calendar, Clock, LogIn, Lock, Info, Landmark, Wallet } from "lucide-react";
import toast, { Toaster } from "react-hot-toast";
import AuthService from "@/app/services/AuthService";
import { payDeposit, getMyTransaction, getPaymentTypes } from "@/app/utils/api";
import Loading from "../loading";
const STATUS_MAP = ['pending', 'ownerConfirmed', 'depositPaid', 'depositConfirmed', 'completed', 'cancelled'];
const STATUS_MAP = ["pending", "ownerConfirmed", "depositPaid", "depositConfirmed", "completed", "cancelled"];
function getStatusConfig(t) {
return {
pending: { label: t('bookingStatus.pending'), color: 'bg-yellow-100 text-yellow-800 border-yellow-300', icon: Clock },
ownerConfirmed: { label: t('bookingStatus.ownerConfirmed'), color: 'bg-blue-100 text-blue-800 border-blue-300', icon: ShieldCheck },
depositPaid: { label: t('bookingStatus.depositPaid'), color: 'bg-orange-100 text-orange-800 border-orange-300', icon: Wallet },
depositConfirmed: { label: t('bookingStatus.depositConfirmed'), color: 'bg-green-100 text-green-800 border-green-300', icon: Check },
completed: { label: t('bookingStatus.completed'), color: 'bg-teal-100 text-teal-800 border-teal-300', icon: Check },
cancelled: { label: t('bookingStatus.cancelled'), color: 'bg-red-100 text-red-800 border-red-300', icon: X },
pending: { label: t("bookingStatus.pending"), color: "bg-yellow-100 text-yellow-800 border-yellow-300", icon: Clock },
ownerConfirmed: { label: t("bookingStatus.ownerConfirmed"), color: "bg-blue-100 text-blue-800 border-blue-300", icon: ShieldCheck },
depositPaid: { label: t("bookingStatus.depositPaid"), color: "bg-orange-100 text-orange-800 border-orange-300", icon: Wallet },
depositConfirmed: { label: t("bookingStatus.depositConfirmed"), color: "bg-green-100 text-green-800 border-green-300", icon: Check },
completed: { label: t("bookingStatus.completed"), color: "bg-teal-100 text-teal-800 border-teal-300", icon: Check },
cancelled: { label: t("bookingStatus.cancelled"), color: "bg-red-100 text-red-800 border-red-300", icon: X },
};
}
function getPaymentMethods(t) {
return [
{ id: 'cash', label: t('payments.method.cash'), desc: t('payments.method.cashDesc'), icon: Banknote, active: true },
{ id: 'office', label: t('payments.method.office'), desc: t('payments.method.officeDesc'), icon: Building, active: true },
{ id: 'transfer', label: t('payments.method.transfer'), desc: t('payments.method.transferDesc'), icon: ArrowLeftRight, active: false },
{ id: 'electronic', label: t('payments.method.electronic'), desc: t('payments.method.electronicDesc'), icon: CreditCard, active: false },
{ id: "cash", label: t("payments.method.cash"), desc: t("payments.method.cashDesc"), icon: Banknote, active: true },
{ id: "office", label: t("payments.method.office"), desc: t("payments.method.officeDesc"), icon: Building, active: true },
{ id: "transfer", label: t("payments.method.transfer"), desc: t("payments.method.transferDesc"), icon: ArrowLeftRight, active: false },
{ id: "electronic", label: t("payments.method.electronic"), desc: t("payments.method.electronicDesc"), icon: CreditCard, active: false },
];
}
function formatCurrency(v, sign = '') {
function formatCurrency(v, sign = "") {
return `${sign} ${Number(v ?? 0).toLocaleString()}`;
}
function formatDate(date) {
if (!date) return '';
if (!date) return "";
const d = new Date(date);
if (Number.isNaN(d.getTime())) return '';
return d.toLocaleDateString('en-GB');
if (Number.isNaN(d.getTime())) return "";
return d.toLocaleDateString("en-GB");
}
function normalizeText(value) {
return String(value ?? '').trim().toLowerCase();
return String(value ?? "")
.trim()
.toLowerCase();
}
function isHaramText(value) {
const text = normalizeText(value);
return text.includes('haram') || text.includes('هرم') || text.includes('هرام');
return text.includes("haram") || text.includes("هرم") || text.includes("هرام");
}
function isReceiptRequiredPayment(value, method = null) {
const haystack = [
value,
method?.name,
method?.label,
method?.id,
method?.description,
]
.filter(Boolean)
.join(' ');
const haystack = [value, method?.name, method?.label, method?.id, method?.description].filter(Boolean).join(" ");
const text = normalizeText(haystack);
return (
text.includes('transfer') ||
text.includes('تحويل') ||
text.includes('حوالة') ||
text.includes('bank') ||
text.includes('بنك') ||
text.includes("transfer") ||
text.includes("تحويل") ||
text.includes("حوالة") ||
text.includes("bank") ||
text.includes("بنك") ||
isHaramText(value) ||
isHaramText(method?.name) ||
isHaramText(method?.label) ||
@ -126,12 +102,12 @@ export default function PaymentsPage() {
endDate: reservation.endDate,
totalPrice: reservation.totalPrice ?? transaction.amount ?? 0,
depositAmount: transaction.amount ?? reservation.totalPrice ?? 0,
currencySign: currency.sign || t('currency.syp'),
currencyName: currency.name || '',
currencySign: currency.sign || t("currency.syp"),
currencyName: currency.name || "",
currencyRate: currency.rate,
propertyName: propertyInfo.name || propertyInfo.address || reservation.propertyName || `${t('payments.propertyLabel')} #${reservation.id || ''}`,
propertyAddress: propertyInfo.address || reservation.propertyAddress || '',
propertyCity: propertyInfo.city || reservation.city || '',
propertyName: propertyInfo.name || propertyInfo.address || reservation.propertyName || `${t("payments.propertyLabel")} #${reservation.id || ""}`,
propertyAddress: propertyInfo.address || reservation.propertyAddress || "",
propertyCity: propertyInfo.city || reservation.city || "",
_deposit: deposit,
_reservation: reservation,
};
@ -140,7 +116,7 @@ export default function PaymentsPage() {
setReservations(mapped);
} catch (err) {
console.error(err);
toast.error(t('payments.loadingTransactions'));
toast.error(t("payments.loadingTransactions"));
} finally {
setLoading(false);
}
@ -158,7 +134,7 @@ export default function PaymentsPage() {
setSelectedPayment(firstActive.name ?? firstActive.id ?? null);
}
} catch (err) {
console.error('[Payments] failed to load payment methods', err);
console.error("[Payments] failed to load payment methods", err);
setPaymentMethods([]);
} finally {
setLoadingPaymentMethods(false);
@ -180,21 +156,17 @@ export default function PaymentsPage() {
}, [loadPaymentMethods]);
const resolvePaymentTypeId = (paymentKey) => {
const method = paymentMethods.find(
(m) => String(m.id) === String(paymentKey) || String(m.name) === String(paymentKey),
);
const method = paymentMethods.find((m) => String(m.id) === String(paymentKey) || String(m.name) === String(paymentKey));
return method?.id ?? paymentKey;
};
const handlePayDeposit = async (reservation, paymentKey, paymentImageFile) => {
const paymentTypeId = resolvePaymentTypeId(paymentKey);
const selectedMethod = paymentMethods.find(
(m) => String(m.id) === String(paymentTypeId) || String(m.name) === String(paymentTypeId),
);
const selectedMethod = paymentMethods.find((m) => String(m.id) === String(paymentTypeId) || String(m.name) === String(paymentTypeId));
const requiresReceiptUpload = isReceiptRequiredPayment(paymentKey, selectedMethod);
if (requiresReceiptUpload && !paymentImageFile) {
toast.error(t('payments.receiptRequired'));
toast.error(t("payments.receiptRequired"));
return;
}
@ -205,46 +177,33 @@ export default function PaymentsPage() {
paymentTypeId,
paymentImage: paymentImageFile,
});
toast.success(t('payments.depositPaidSuccess'));
toast.success(t("payments.depositPaidSuccess"));
loadReservations();
} catch (err) {
toast.error(err?.message || t('payments.paymentFailed'));
toast.error(err?.message || t("payments.paymentFailed"));
} finally {
setPayingId(null);
}
};
const canPay = (status) => STATUS_MAP[status] === 'ownerConfirmed';
const canPay = (status) => STATUS_MAP[status] === "ownerConfirmed";
if (loading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center" dir={i18n.dir()}>
<Loader2 className="w-12 h-12 text-amber-500 animate-spin" />
</div>
);
return <Loading />;
}
if (isGuest) {
return (
<div className="min-h-screen bg-linear-to-b from-amber-50/50 to-white flex items-center justify-center p-4" dir={i18n.dir()}>
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="bg-white rounded-3xl shadow-xl border border-gray-200 p-10 max-w-md w-full text-center"
>
<motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} className="bg-white rounded-3xl shadow-xl border border-gray-200 p-10 max-w-md w-full text-center">
<div className="w-20 h-20 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-6">
<Lock className="w-10 h-10 text-amber-600" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-3">{t('payments.title')}</h2>
<p className="text-gray-600 leading-relaxed mb-8">
{t('payments.loginRequiredDesc')}
</p>
<Link
href="/login"
className="inline-flex items-center gap-2 bg-amber-500 hover:bg-amber-600 text-white px-8 py-3 rounded-2xl text-lg font-semibold transition shadow-lg shadow-amber-200"
>
<h2 className="text-2xl font-bold text-gray-900 mb-3">{t("payments.title")}</h2>
<p className="text-gray-600 leading-relaxed mb-8">{t("payments.loginRequiredDesc")}</p>
<Link href="/login" className="inline-flex items-center gap-2 bg-amber-500 hover:bg-amber-600 text-white px-8 py-3 rounded-2xl text-lg font-semibold transition shadow-lg shadow-amber-200">
<LogIn className="w-5 h-5" />
{t('login')}
{t("login")}
</Link>
</motion.div>
</div>
@ -258,20 +217,16 @@ export default function PaymentsPage() {
<div className="min-h-screen bg-gray-50 py-8" dir={i18n.dir()}>
<Toaster position="top-center" reverseOrder={false} />
<div className="container mx-auto px-4 max-w-4xl">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-8"
>
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t('payments.title')}</h1>
<p className="text-gray-600">{t('payments.description')}</p>
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t("payments.title")}</h1>
<p className="text-gray-600">{t("payments.description")}</p>
</motion.div>
{payables.length > 0 && (
<div className="space-y-6 mb-10">
<h2 className="text-xl font-bold text-gray-800 flex items-center gap-2">
<Wallet className="w-5 h-5 text-amber-500" />
{t('payments.payablesTitle')}
{t("payments.payablesTitle")}
</h2>
{payables.map((r, i) => (
<PaymentCard
@ -292,21 +247,16 @@ export default function PaymentsPage() {
<div className="space-y-4">
<h2 className="text-xl font-bold text-gray-800 flex items-center gap-2">
<Clock className="w-5 h-5 text-gray-500" />
{t('payments.previousReservationsTitle')}
{t("payments.previousReservationsTitle")}
</h2>
{others.map((r, i) => {
const statusKey = STATUS_MAP[r.status] || 'pending';
const statusKey = STATUS_MAP[r.status] || "pending";
const cfg = getStatusConfig(t)[statusKey];
const Icon = cfg.icon;
const amount = r.depositAmount || r.totalPrice || 0;
return (
<motion.div
key={r.id || i}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-2xl shadow-sm border border-gray-200 p-5"
>
<motion.div key={r.id || i} initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="bg-white rounded-2xl shadow-sm border border-gray-200 p-5">
<div className="flex items-center justify-between gap-3 mb-3">
<span className="text-sm font-medium text-gray-400">#{r.reservationId || r.id}</span>
<span className={`inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium border ${cfg.color}`}>
@ -319,9 +269,7 @@ export default function PaymentsPage() {
<Calendar className="w-4 h-4" />
{formatDate(r.startDate)} - {formatDate(r.endDate)}
</div>
<div className="text-lg font-bold text-gray-900">
{formatCurrency(amount, r.currencySign)}
</div>
<div className="text-lg font-bold text-gray-900">{formatCurrency(amount, r.currencySign)}</div>
</div>
</motion.div>
);
@ -330,14 +278,10 @@ export default function PaymentsPage() {
)}
{reservations.length === 0 && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300"
>
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
<CreditCard className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-700 mb-2">{t('payments.noTransactions')}</h3>
<p className="text-gray-500">{t('payments.noTransactionsDesc')}</p>
<h3 className="text-xl font-bold text-gray-700 mb-2">{t("payments.noTransactions")}</h3>
<p className="text-gray-500">{t("payments.noTransactionsDesc")}</p>
</motion.div>
)}
</div>
@ -353,8 +297,8 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
const [showCashDialog, setShowCashDialog] = useState(false);
const [showReceiptModal, setShowReceiptModal] = useState(false);
const [localPaymentImage, setLocalPaymentImage] = useState(null);
const [receiptPreviewUrl, setReceiptPreviewUrl] = useState('');
const [receiptFileName, setReceiptFileName] = useState('');
const [receiptPreviewUrl, setReceiptPreviewUrl] = useState("");
const [receiptFileName, setReceiptFileName] = useState("");
const methods = paymentMethods.length > 0 ? paymentMethods : getPaymentMethods(t);
@ -371,7 +315,7 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
useEffect(() => {
if (!localPaymentImage) {
setReceiptPreviewUrl('');
setReceiptPreviewUrl("");
return undefined;
}
@ -391,19 +335,19 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
setShowReceiptModal(false);
setLocalPaymentImage(null);
setReceiptPreviewUrl('');
setReceiptFileName('');
setReceiptPreviewUrl("");
setReceiptFileName("");
};
const handleReceiptSelection = (event) => {
const file = event.target.files?.[0] || null;
setLocalPaymentImage(file);
setReceiptFileName(file?.name || '');
setReceiptFileName(file?.name || "");
};
const handleReceiptConfirm = () => {
if (!localPaymentImage) {
toast.error(t('payments.receiptRequired'));
toast.error(t("payments.receiptRequired"));
return;
}
@ -412,11 +356,7 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
return (
<>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-3xl shadow-md border border-gray-200 overflow-hidden"
>
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="bg-white rounded-3xl shadow-md border border-gray-200 overflow-hidden">
{/* Property details */}
<div className="p-6 border-b border-gray-100">
<div className="flex items-start gap-4 mb-4">
@ -424,13 +364,11 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
<Home className="w-7 h-7 text-amber-600" />
</div>
<div className="min-w-0 flex-1">
<h3 className="text-xl font-bold text-gray-900 mb-1">
{r.propertyName}
</h3>
<h3 className="text-xl font-bold text-gray-900 mb-1">{r.propertyName}</h3>
{(r.propertyAddress || r.propertyCity) && (
<p className="text-sm text-gray-500 flex items-center gap-1">
<MapPin className="w-3.5 h-3.5" />
{[r.propertyCity, r.propertyAddress].filter(Boolean).join(' - ')}
{[r.propertyCity, r.propertyAddress].filter(Boolean).join(" - ")}
</p>
)}
</div>
@ -442,8 +380,7 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
{formatDate(r.startDate)} - {formatDate(r.endDate)}
</span>
<span className="flex items-center gap-1.5">
<Clock className="w-4 h-4 text-gray-400" />
#{r.reservationId || r.id}
<Clock className="w-4 h-4 text-gray-400" />#{r.reservationId || r.id}
</span>
</div>
</div>
@ -451,13 +388,9 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
{/* Deposit amount */}
<div className="px-6 py-5 border-b border-gray-100">
<div className="text-center">
<p className="text-sm text-gray-500 mb-1">{t('payments.depositAmount')}</p>
<p className="text-4xl font-bold text-amber-600">
{formatCurrency(amount, r.currencySign)}
</p>
<p className="text-xs text-gray-400 mt-2">
{t('payments.depositPaidToPlatform')}
</p>
<p className="text-sm text-gray-500 mb-1">{t("payments.depositAmount")}</p>
<p className="text-4xl font-bold text-amber-600">{formatCurrency(amount, r.currencySign)}</p>
<p className="text-xs text-gray-400 mt-2">{t("payments.depositPaidToPlatform")}</p>
</div>
</div>
@ -465,24 +398,18 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
<div className="px-6 py-4 bg-amber-50 border-b border-amber-100">
<div className="flex items-start gap-3">
<Info className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" />
<p className="text-sm text-amber-800 leading-relaxed">
{t('payments.cashOnlyNotice')}
</p>
<p className="text-sm text-amber-800 leading-relaxed">{t("payments.cashOnlyNotice")}</p>
</div>
</div>
{/* Payment method options */}
<div className="px-6 py-5 border-b border-gray-100">
<p className="text-sm font-bold text-gray-700 mb-3">{t('payments.paymentMethodLabel')}</p>
<p className="text-sm font-bold text-gray-700 mb-3">{t("payments.paymentMethodLabel")}</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{loadingPaymentMethods ? (
<div className="col-span-full rounded-2xl border border-dashed border-gray-200 bg-gray-50 p-4 text-sm text-gray-600">
{t('payments.loadingPaymentMethods')}
</div>
<div className="col-span-full rounded-2xl border border-dashed border-gray-200 bg-gray-50 p-4 text-sm text-gray-600">{t("payments.loadingPaymentMethods")}</div>
) : methods.length === 0 ? (
<div className="col-span-full rounded-2xl border border-dashed border-gray-200 bg-gray-50 p-4 text-sm text-gray-600">
{t('payments.noPaymentMethodsAvailable')}
</div>
<div className="col-span-full rounded-2xl border border-dashed border-gray-200 bg-gray-50 p-4 text-sm text-gray-600">{t("payments.noPaymentMethodsAvailable")}</div>
) : (
methods.map((method, index) => {
const optionId = method.name ?? method.id ?? `payment-method-${index}`;
@ -498,34 +425,20 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
onClick={() => isActive && handleSelectMethod(optionId, method)}
className={`relative flex items-start gap-3 p-4 rounded-2xl border-2 text-right transition-all ${
!isActive
? 'border-gray-100 bg-gray-50 opacity-50 cursor-not-allowed'
? "border-gray-100 bg-gray-50 opacity-50 cursor-not-allowed"
: isSelected
? 'border-amber-500 bg-amber-50 shadow-sm'
: 'border-gray-200 bg-white hover:border-amber-300 cursor-pointer'
? "border-amber-500 bg-amber-50 shadow-sm"
: "border-gray-200 bg-white hover:border-amber-300 cursor-pointer"
}`}
>
<div
className={`w-10 h-10 rounded-xl flex items-center justify-center shrink-0 ${
isSelected ? 'bg-amber-500 text-white' : 'bg-gray-100 text-gray-500'
}`}
>
<div className={`w-10 h-10 rounded-xl flex items-center justify-center shrink-0 ${isSelected ? "bg-amber-500 text-white" : "bg-gray-100 text-gray-500"}`}>
<Icon className="w-5 h-5" />
</div>
<div className="min-w-0 flex-1">
<p className={`text-sm font-bold ${isSelected ? 'text-amber-900' : 'text-gray-800'}`}>
{method.name || method.label || t('payments.paymentMethodName')}
</p>
{method.description && (
<p className="text-xs text-gray-500 mt-0.5 leading-relaxed">
{method.description}
</p>
)}
<span
className={`inline-block mt-1 text-[10px] font-medium px-2 py-0.5 rounded-full ${
isActive ? 'bg-amber-100 text-amber-700' : 'bg-gray-200 text-gray-500'
}`}
>
{isActive ? t('payments.methodActive') : t('payments.methodInactive')}
<p className={`text-sm font-bold ${isSelected ? "text-amber-900" : "text-gray-800"}`}>{method.name || method.label || t("payments.paymentMethodName")}</p>
{method.description && <p className="text-xs text-gray-500 mt-0.5 leading-relaxed">{method.description}</p>}
<span className={`inline-block mt-1 text-[10px] font-medium px-2 py-0.5 rounded-full ${isActive ? "bg-amber-100 text-amber-700" : "bg-gray-200 text-gray-500"}`}>
{isActive ? t("payments.methodActive") : t("payments.methodInactive")}
</span>
</div>
{isSelected && (
@ -544,7 +457,7 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
<div className="px-6 py-4 bg-amber-50 border-b border-amber-100">
<div className="flex items-start gap-2 text-sm text-amber-800">
<Info className="w-4 h-4 shrink-0 mt-0.5" />
<p>{t('payments.receiptUploadRequiredHint')}</p>
<p>{t("payments.receiptUploadRequiredHint")}</p>
</div>
</div>
)}
@ -562,12 +475,12 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
{payingId === r.id ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
{t('payments.processingPayment')}
{t("payments.processingPayment")}
</>
) : (
<>
<Banknote className="w-5 h-5" />
{t('payments.payButton', { amount: formatCurrency(amount, r.currencySign) })}
{t("payments.payButton", { amount: formatCurrency(amount, r.currencySign) })}
</>
)}
</motion.button>
@ -575,24 +488,18 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
{showReceiptModal && (
<div className="fixed inset-0 z-60 flex items-center justify-center bg-black/60 px-4 py-8">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="w-full max-w-md rounded-3xl bg-white p-6 shadow-2xl border border-gray-200"
>
<motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} className="w-full max-w-md rounded-3xl bg-white p-6 shadow-2xl border border-gray-200">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-xl font-bold text-gray-900">{t('payments.receiptDialogTitle')}</h3>
<p className="text-sm text-gray-600 mt-1">{t('payments.receiptDialogDescription')}</p>
<h3 className="text-xl font-bold text-gray-900">{t("payments.receiptDialogTitle")}</h3>
<p className="text-sm text-gray-600 mt-1">{t("payments.receiptDialogDescription")}</p>
</div>
<div className="w-12 h-12 bg-amber-100 rounded-2xl flex items-center justify-center">
<CreditCard className="w-6 h-6 text-amber-600" />
</div>
</div>
<label className="block text-sm font-bold text-gray-700 mb-2">
{t('payments.uploadReceipt')}
</label>
<label className="block text-sm font-bold text-gray-700 mb-2">{t("payments.uploadReceipt")}</label>
<input
type="file"
accept="image/*"
@ -602,34 +509,22 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
{receiptFileName && (
<div className="mt-3 rounded-2xl border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800">
{t('payments.receiptSelected')} {receiptFileName}
{t("payments.receiptSelected")} {receiptFileName}
</div>
)}
{localPaymentImage && receiptPreviewUrl && (
<div className="mt-4 overflow-hidden rounded-2xl border border-gray-200 bg-gray-50 p-2">
<img
src={receiptPreviewUrl}
alt={t('payments.uploadReceipt')}
className="h-48 w-full rounded-xl object-cover"
/>
<img src={receiptPreviewUrl} alt={t("payments.uploadReceipt")} className="h-48 w-full rounded-xl object-cover" />
</div>
)}
<div className="mt-6 flex flex-col gap-3 sm:flex-row">
<button
type="button"
onClick={() => setShowReceiptModal(false)}
className="w-full rounded-xl border border-gray-300 px-4 py-3 text-sm font-semibold text-gray-700 hover:bg-gray-100"
>
{t('payments.closeButton')}
<button type="button" onClick={() => setShowReceiptModal(false)} className="w-full rounded-xl border border-gray-300 px-4 py-3 text-sm font-semibold text-gray-700 hover:bg-gray-100">
{t("payments.closeButton")}
</button>
<button
type="button"
onClick={handleReceiptConfirm}
className="w-full rounded-xl bg-amber-500 px-4 py-3 text-sm font-semibold text-white hover:bg-amber-600"
>
{t('payments.continueToPay')}
<button type="button" onClick={handleReceiptConfirm} className="w-full rounded-xl bg-amber-500 px-4 py-3 text-sm font-semibold text-white hover:bg-amber-600">
{t("payments.continueToPay")}
</button>
</div>
</motion.div>
@ -644,30 +539,26 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
className="w-full bg-white border-2 border-amber-200 hover:border-amber-400 text-amber-700 font-bold py-3 px-6 rounded-2xl text-base transition-all flex items-center justify-center gap-2"
>
<Building className="w-5 h-5" />
{t('payments.payCashButton')}
{t("payments.payCashButton")}
</button>
</div>
{/* Cash payment dialog */}
{showCashDialog && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 px-4 py-8">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="w-full max-w-md rounded-3xl bg-white p-8 shadow-2xl border border-gray-200 text-center"
>
<motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} className="w-full max-w-md rounded-3xl bg-white p-8 shadow-2xl border border-gray-200 text-center">
<div className="w-16 h-16 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-5">
<Clock className="w-8 h-8 text-amber-600" />
</div>
<h3 className="text-xl font-bold text-gray-900 mb-3">{t('payments.cashPendingDialogTitle')}</h3>
<h3 className="text-xl font-bold text-gray-900 mb-3">{t("payments.cashPendingDialogTitle")}</h3>
<p className="text-gray-600 leading-relaxed mb-6">
{t('payments.cashPendingDialogDesc1')}
{t("payments.cashPendingDialogDesc1")}
<br />
{t('payments.cashPendingDialogDesc2')}
{t("payments.cashPendingDialogDesc2")}
</p>
<div className="bg-gray-50 rounded-2xl p-4 mb-6 text-right">
<p className="text-sm font-bold text-gray-800 mb-1">{t('payments.platformOfficeLabel')}</p>
<p className="text-sm text-gray-600">{t('payments.platformOfficeFullAddress')}</p>
<p className="text-sm font-bold text-gray-800 mb-1">{t("payments.platformOfficeLabel")}</p>
<p className="text-sm text-gray-600">{t("payments.platformOfficeFullAddress")}</p>
<p className="text-sm text-gray-600 flex items-center gap-1 mt-1" dir="ltr">
<Phone className="w-3.5 h-3.5" />
+963567823411
@ -679,13 +570,10 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
onClick={() => setShowCashDialog(false)}
className="w-full px-5 py-3 rounded-xl border border-gray-300 text-gray-700 hover:bg-gray-100 transition-colors font-medium"
>
{t('payments.closeButton')}
{t("payments.closeButton")}
</button>
<Link
href="/reservations"
className="w-full px-5 py-3 rounded-xl bg-amber-500 text-white font-semibold text-center hover:bg-amber-600 transition-colors"
>
{t('payments.myReservationsLink')}
<Link href="/reservations" className="w-full px-5 py-3 rounded-xl bg-amber-500 text-white font-semibold text-center hover:bg-amber-600 transition-colors">
{t("payments.myReservationsLink")}
</Link>
</div>
</motion.div>
@ -697,10 +585,8 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
<div className="flex items-start gap-3">
<Landmark className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" />
<div>
<p className="text-sm font-bold text-gray-800 mb-1">{t('payments.cashPaymentLocationLabel')}</p>
<p className="text-sm text-gray-600 leading-relaxed">
{t('payments.platformOfficeFullAddress')}
</p>
<p className="text-sm font-bold text-gray-800 mb-1">{t("payments.cashPaymentLocationLabel")}</p>
<p className="text-sm text-gray-600 leading-relaxed">{t("payments.platformOfficeFullAddress")}</p>
<p className="text-sm text-gray-600 flex items-center gap-1 mt-1">
<Phone className="w-3.5 h-3.5" />
<span dir="ltr">+963567823411</span>
@ -711,4 +597,4 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
</motion.div>
</>
);
}
}

View File

@ -5,24 +5,11 @@ import { motion } from "framer-motion";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { useTranslation } from "react-i18next";
import {
User,
Mail,
Phone,
MessageCircle,
Check,
X,
ArrowLeft,
Building,
Home,
Calendar,
MapPin,
Loader2,
Pencil,
} from "lucide-react";
import { User, Mail, Phone, MessageCircle, Check, X, ArrowLeft, Building, Home, Calendar, MapPin, Loader2, Pencil } from "lucide-react";
import toast, { Toaster } from "react-hot-toast";
import AuthService from "../services/AuthService";
import { getCustomerByUserId, getOwnerByUserId } from "../utils/api";
import InteractiveBackground from "../components/Animation/Background";
export default function ProfilePage() {
const { t, i18n } = useTranslation();
@ -59,30 +46,23 @@ export default function ProfilePage() {
async function fetchProfile() {
try {
const fetchFn =
userData.role === "owner" ? getOwnerByUserId : getCustomerByUserId;
const fetchFn = userData.role === "owner" ? getOwnerByUserId : getCustomerByUserId;
const profile = await fetchFn(userData.id);
if (profile) {
const profileData = {
name:
profile.fullName ||
profile.name ||
`${profile.firstName || ""} ${profile.lastName || ""}`.trim() ||
userData.name ||
"",
name: profile.fullName || profile.name || `${profile.firstName || ""} ${profile.lastName || ""}`.trim() || userData.name || "",
email: profile.email || userData.email || "",
phone:
profile.phone || profile.phoneNumber || userData.phone || "",
phone: profile.phone || profile.phoneNumber || userData.phone || "",
whatsapp: profile.whatsAppNumber || profile.whatsapp || "",
bio: profile.bio || "",
location: profile.address || profile.location || "",
joinedDate: profile.createdAt
? new Date(profile.createdAt).toLocaleDateString(i18n.language === 'ar' ? 'ar-SA' : 'en-US', {
? new Date(profile.createdAt).toLocaleDateString(i18n.language === "ar" ? "ar-SA" : "en-US", {
month: "long",
year: "numeric",
})
: new Date().toLocaleDateString(i18n.language === 'ar' ? 'ar-SA' : 'en-US', {
: new Date().toLocaleDateString(i18n.language === "ar" ? "ar-SA" : "en-US", {
month: "long",
year: "numeric",
}),
@ -109,7 +89,7 @@ export default function ProfilePage() {
whatsapp: "",
bio: "",
location: "",
joinedDate: new Date().toLocaleDateString(i18n.language === 'ar' ? 'ar-SA' : 'en-US', {
joinedDate: new Date().toLocaleDateString(i18n.language === "ar" ? "ar-SA" : "en-US", {
month: "long",
year: "numeric",
}),
@ -146,7 +126,7 @@ export default function ProfilePage() {
setFormData(updatedData);
localStorage.setItem("userProfile", JSON.stringify(updatedData));
setEditingBio(false);
toast.success(t('bio-updated-success'));
toast.success(t("bio-updated-success"));
};
const fadeInUp = {
@ -160,37 +140,25 @@ export default function ProfilePage() {
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<Loader2 className="w-12 h-12 text-amber-500 animate-spin mx-auto mb-4" />
<p className="text-gray-600">{t('loading-profile')}</p>
<p className="text-gray-600">{t("loading-profile")}</p>
</div>
</div>
);
}
return (
<div dir={i18n.language === 'ar' ? 'rtl' : 'ltr'} className="min-h-screen bg-gray-50 py-8">
<div dir={i18n.language === "ar" ? "rtl" : "ltr"} className="min-h-screen bg-gray-50 py-8">
<Toaster position="top-center" reverseOrder={false} />
<div className="container mx-auto px-4 max-w-4xl">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="flex justify-between items-center mb-6"
>
<Link
href="/"
className="flex items-center gap-2 text-gray-600 hover:text-amber-600 transition-colors"
>
<InteractiveBackground />
<div className="relative z-10 container mx-auto px-4 max-w-2xl">
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="flex justify-between items-center mb-6">
<Link href="/" className="flex items-center gap-2 text-gray-600 hover:text-amber-600 transition-colors">
<ArrowLeft className="w-5 h-5" />
<span>{t('backToHome')}</span>
<span>{t("backToHome")}</span>
</Link>
</motion.div>
<motion.div
variants={fadeInUp}
initial="initial"
animate="animate"
className="bg-white rounded-3xl shadow-xl overflow-hidden"
>
<motion.div variants={fadeInUp} initial="initial" animate="animate" className="bg-white rounded-3xl shadow-xl overflow-hidden">
<div className="h-48 bg-gradient-to-r from-amber-500 to-amber-600 relative overflow-hidden">
<motion.div
className="absolute inset-0 bg-white/10"
@ -209,43 +177,34 @@ export default function ProfilePage() {
<div className="flex justify-center -mt-16 mb-6">
<div className="relative group">
<div className="w-32 h-32 rounded-full border-4 border-white bg-gradient-to-br from-amber-500 to-amber-600 flex items-center justify-center text-white text-4xl font-bold shadow-xl overflow-hidden">
{avatarPreview ? (
<img
src={avatarPreview}
alt={formData.name}
className="w-full h-full object-cover"
/>
) : (
formData.name?.charAt(0).toUpperCase() || "U"
)}
{avatarPreview ? <img src={avatarPreview} alt={formData.name} className="w-full h-full object-cover" /> : formData.name?.charAt(0).toUpperCase() || "U"}
</div>
</div>
</div>
<div className="text-center mb-6">
<div className="flex items-center justify-center gap-2">
<h1 className="text-3xl font-bold text-gray-900">
{formData.name}
</h1>
<h1 className="text-3xl font-bold text-gray-900">{formData.name}</h1>
</div>
<div className="flex items-center justify-center gap-2 text-gray-500 mt-2">
<MapPin className="w-4 h-4" />
<span>{formData.location || t('addressNotSpecified')}</span>
<span>{formData.location || t("addressNotSpecified")}</span>
</div>
<div className="flex items-center justify-center gap-2 text-gray-500 mt-1">
<Calendar className="w-4 h-4" />
<span> {t('member-since')} {formData.joinedDate}</span>
<span>
{" "}
{t("member-since")} {formData.joinedDate}
</span>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-gray-50 p-4 rounded-xl group">
<div className="flex justify-between items-start mb-2">
<label className="text-sm font-medium text-gray-600">
{t('emailLabel')}
</label>
<label className="text-sm font-medium text-gray-600">{t("emailLabel")}</label>
</div>
<div className="flex items-center gap-2 text-gray-900">
<Mail className="w-5 h-5 text-gray-400" />
@ -255,42 +214,36 @@ export default function ProfilePage() {
<div className="bg-gray-50 p-4 rounded-xl group">
<div className="flex justify-between items-start mb-2">
<label className="text-sm font-medium text-gray-600">
{t('phoneLabel')}
</label>
<label className="text-sm font-medium text-gray-600">{t("phoneLabel")}</label>
</div>
<div className="flex items-center gap-2 text-gray-900">
<Phone className="w-5 h-5 text-gray-400" />
<span>{formData.phone || t('not-specified')}</span>
<span>{formData.phone || t("not-specified")}</span>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-xl group">
<div className="flex justify-between items-start mb-2">
<label className="text-sm font-medium text-gray-600">
{t('whatsapp-label')}
</label>
<label className="text-sm font-medium text-gray-600">{t("whatsapp-label")}</label>
</div>
<div className="flex items-center gap-2 text-gray-900">
<MessageCircle className="w-5 h-5 text-gray-400" />
<span>{formData.whatsapp || t('not-specified')}</span>
<span>{formData.whatsapp || t("not-specified")}</span>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-xl">
<label className="block text-sm font-medium text-gray-600 mb-2">
{t('account-type')}
</label>
<label className="block text-sm font-medium text-gray-600 mb-2">{t("account-type")}</label>
<div className="flex items-center gap-2">
{user?.role === "owner" ? (
<>
<Building className="w-5 h-5 text-amber-500" />
<span className="text-gray-900">{t('property-owner')}</span>
<span className="text-gray-900">{t("property-owner")}</span>
</>
) : (
<>
<Home className="w-5 h-5 text-blue-500" />
<span className="text-gray-900">{t('customer')}</span>
<span className="text-gray-900">{t("customer")}</span>
</>
)}
</div>
@ -348,17 +301,17 @@ export default function ProfilePage() {
{user?.role === "owner" && (
<div className="grid grid-cols-3 gap-4 mt-6">
<div className="bg-amber-50 p-4 rounded-xl text-center">
<div className=" p-4 rounded-xl text-center" style={{backgroundColor:"#0f172a"}} >
<div className="text-2xl font-bold text-amber-600">12</div>
<div className="text-xs text-gray-600">{t('propertiesListed')}</div>
<div className="text-xs text-gray-600">{t("propertiesListed")}</div>
</div>
<div className="bg-blue-50 p-4 rounded-xl text-center">
<div className="bg-blue-50 p-4 rounded-xl text-center"style={{backgroundColor:"#0f172a"}} >
<div className="text-2xl font-bold text-blue-600">8</div>
<div className="text-xs text-gray-600">{t('citiesCovered')}</div>
<div className="text-xs text-gray-600">{t("citiesCovered")}</div>
</div>
<div className="bg-green-50 p-4 rounded-xl text-center">
<div className="bg-green-50 p-4 rounded-xl text-center" style={{backgroundColor:"#0f172a"}}>
<div className="text-2xl font-bold text-green-600">4.8</div>
<div className="text-xs text-gray-600">{t('customerSatisfaction')}</div>
<div className="text-xs text-gray-600">{t("customerSatisfaction")}</div>
</div>
</div>
)}

View File

@ -128,7 +128,6 @@ const PropertyCard = ({ property, viewMode = 'grid', onLoginRequired }) => {
const { isFavorite: checkFavorite, addFavorite, removeFavorite } = useFavorites();
const [favLoading, setFavLoading] = useState(false);
const [currentImage, setCurrentImage] = useState(0);
const isFav = checkFavorite(property.id);
const toggleFavorite = async (e) => {
@ -142,6 +141,7 @@ const PropertyCard = ({ property, viewMode = 'grid', onLoginRequired }) => {
await addFavorite(property.id);
}
setFavLoading(false);
};
const formatCurrency = (amount) => {
@ -187,6 +187,7 @@ const PropertyCard = ({ property, viewMode = 'grid', onLoginRequired }) => {
src={property.images[currentImage] || '/property-placeholder.jpg'}
alt={property.title}
fill
loading='lazy'
className="object-cover"
/>
{property.images.length > 1 && (

View File

@ -1200,37 +1200,6 @@ export default function PropertyDetailsPage() {
</motion.div>
)}
{/* Contact Card */}
{/* {!isOwnProperty && (
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} className="bg-white rounded-2xl p-5 shadow-sm border border-gray-200">
<div className="flex items-center gap-2 mb-3">
<Phone className="w-4 h-4 text-amber-500" />
<h3 className="font-bold text-gray-900">{t("ownerInfo")}</h3>
</div>
{showContact && contactInfo ? (
<div className="space-y-2">
<div className="flex items-center gap-2.5 p-2.5 bg-gray-50 rounded-xl">
<Phone className="w-4 h-4 text-gray-600 flex-shrink-0" />
<span className="font-medium text-gray-900 text-sm" dir="ltr">{contactInfo.phone || contactInfo.phoneNumber || '—'}</span>
</div>
{contactInfo.whatsAppNumber && (
<a href={`https://wa.me/${contactInfo.whatsAppNumber.replace(/[^0-9]/g, '')}`} target="_blank" rel="noopener noreferrer"
className="flex items-center gap-2.5 p-2.5 bg-green-50 rounded-xl hover:bg-green-100 transition-colors">
<MessageCircle className="w-4 h-4 text-green-600 flex-shrink-0" />
<span className="font-medium text-gray-900 text-sm" dir="ltr">{contactInfo.whatsAppNumber}</span>
</a>
)}
</div>
) : (
<button onClick={fetchContactInfo}
className="w-full bg-gray-800 hover:bg-gray-900 text-white py-2.5 rounded-xl font-medium text-sm transition-colors flex items-center justify-center gap-2">
<Phone className="w-4 h-4" />
{t("showContactInfo")}
</button>
)}
</motion.div>
)} */}
</div>
</div>
</div>

View File

@ -68,8 +68,9 @@ export default function TenantRegisterPage() {
};
try {
const res = await addCustomer(payload, null, null);
console.log(res);
if (res.status === 200 || res.ok) {
const apiMessage = res.message || res.data?.message;
const apiMessage = res.message || res.data?.message || res.data[0]?.message;
toast.success(apiMessage || t("register.accountCreated"), { duration: 4000 });
const loginRes = await loginWithEmail(formData.email, formData.password);
if (loginRes.status === 206) {
@ -83,7 +84,7 @@ export default function TenantRegisterPage() {
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 || res.data[0]?.message || t("register.accountCreationFailed");
toast.error(errMsg);
}
} catch (err) {
@ -107,7 +108,14 @@ export default function TenantRegisterPage() {
variants={staggerContainer}
initial="initial"
animate="animate"
onSubmit={ step === 1? (e) => { e.preventDefault(); handleNextStep(); }: handleSubmit }
onSubmit={
step === 1
? (e) => {
e.preventDefault();
handleNextStep();
}
: handleSubmit
}
className="space-y-6"
>
{step === 1 && <OwnerFormStepOne setErrors={setErrors} setFormData={setFormData} formData={formData} errors={errors} type={"customer"} />}

View File

@ -12,9 +12,7 @@ import {
Send,
Loader2,
CheckCircle,
AlertCircle,
User,
MessageSquare,
Hash,
} from 'lucide-react';
import { submitReport, submitReservationReport, submitSaleReport } from '../utils/api';
@ -138,14 +136,14 @@ export default function ReportsPage() {
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('reports.subjectLabel')}</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<div className="absolute inset-y-0 end-0 pe-3 flex items-center pointer-events-none">
<FileText className={`w-5 h-5 ${errors.subject ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
</div>
<input
type="text"
value={generalForm.subject}
onChange={(e) => { setGeneralForm({ ...generalForm, subject: e.target.value }); if (errors.subject) setErrors({ ...errors, subject: null }); }}
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all ${errors.subject ? 'border-red-500' : 'border-gray-300'}`}
className={`w-full pe-12 ps-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all ${errors.subject ? 'border-red-500' : 'border-gray-300'}`}
placeholder={t('reports.subjectPlaceholder')}
/>
</div>
@ -184,14 +182,14 @@ export default function ReportsPage() {
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('reports.reservationIdLabel')}</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<div className="absolute inset-y-0 end-0 pe-3 flex items-center pointer-events-none">
<Hash className={`w-5 h-5 ${errors.reservationId ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
</div>
<input
type="text"
value={reservationForm.reservationId}
onChange={(e) => { setReservationForm({ ...reservationForm, reservationId: e.target.value }); if (errors.reservationId) setErrors({ ...errors, reservationId: null }); }}
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all ${errors.reservationId ? 'border-red-500' : 'border-gray-300'}`}
className={`w-full pe-12 ps-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all ${errors.reservationId ? 'border-red-500' : 'border-gray-300'}`}
placeholder={t('reports.reservationIdPlaceholder')}
/>
</div>
@ -250,14 +248,14 @@ export default function ReportsPage() {
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('reports.saleIdLabel')}</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<div className="absolute inset-y-0 end-0 pe-3 flex items-center pointer-events-none">
<Hash className={`w-5 h-5 ${errors.saleId ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
</div>
<input
type="text"
value={saleForm.saleId}
onChange={(e) => { setSaleForm({ ...saleForm, saleId: e.target.value }); if (errors.saleId) setErrors({ ...errors, saleId: null }); }}
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all ${errors.saleId ? 'border-red-500' : 'border-gray-300'}`}
className={`w-full pe-12 ps-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all ${errors.saleId ? 'border-red-500' : 'border-gray-300'}`}
placeholder={t('reports.saleIdPlaceholder')}
/>
</div>
@ -314,7 +312,7 @@ export default function ReportsPage() {
};
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="min-h-screen py-8">
<Toaster position="top-center" reverseOrder={false} />
<div className="container mx-auto px-4 max-w-3xl">
<motion.div
@ -370,4 +368,4 @@ export default function ReportsPage() {
</div>
</div>
);
}
}

View File

@ -431,6 +431,7 @@ import {
payDeposit,
} from "../utils/api";
import { addPropertyRating } from "../utils/ratings";
import Loading from "../loading";
const API_BASE =
process.env.NEXT_PUBLIC_API_URL || "https://45.93.137.91.nip.io/api";
@ -1713,12 +1714,11 @@ export default function UserReservationsPage() {
}
};
if (loading)
if (loading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<Loader2 className="w-12 h-12 text-amber-500 animate-spin" />
</div>
<Loading/>
);
}
return (
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">

View File

@ -40,7 +40,7 @@ export default function SupportPage() {
};
return (
<div className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12" dir={i18n.language === "ar" ? "rtl" : "ltr"}>
<div className="min-h-screen bg-gradient-to-b py-12" dir={i18n.language === "ar" ? "rtl" : "ltr"}>
<Toaster position="top-center" reverseOrder={false} />
<div className="container mx-auto px-4 max-w-5xl">
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="text-center mb-12">

View File

@ -1,9 +1,9 @@
'use client';
"use client";
import { useEffect, useState } from 'react';
import { motion } from 'framer-motion';
import { FileText, Shield, CheckCircle, Languages, Loader2, AlertCircle } from 'lucide-react';
import { getARTerms, getENTerms } from '../utils/api';
import { useEffect, useState } from "react";
import { motion } from "framer-motion";
import { FileText, Shield, CheckCircle, Languages, Loader2, AlertCircle } from "lucide-react";
import { getARTerms, getENTerms } from "../utils/api";
const containerVariants = {
hidden: { opacity: 0 },
@ -21,75 +21,63 @@ const itemVariants = {
const FALLBACK_TERMS = {
ar: [
{
title: 'مقدمة',
description:
'مرحباً بك في منصة SweetHome. باستخدامك للمنصة، فإنك توافق على الالتزام بشروط الاستخدام هذه. إذا كنت لا توافق على أي جزء من هذه الشروط، يرجى عدم استخدام المنصة.',
title: "مقدمة",
description: "مرحباً بك في منصة SweetHome. باستخدامك للمنصة، فإنك توافق على الالتزام بشروط الاستخدام هذه. إذا كنت لا توافق على أي جزء من هذه الشروط، يرجى عدم استخدام المنصة.",
},
{
title: 'استخدام المنصة',
description:
'يُسمح باستخدام المنصة للأغراض المشروعة فقط. يلتزم المستخدم بعدم استخدام المنصة في أي نشاط غير قانوني.',
title: "استخدام المنصة",
description: "يُسمح باستخدام المنصة للأغراض المشروعة فقط. يلتزم المستخدم بعدم استخدام المنصة في أي نشاط غير قانوني.",
},
{
title: 'حقوق ومسؤوليات المالك',
description:
'يتحمل المالك مسؤولية دقة المعلومات المقدمة عن العقار بما في ذلك الصور والوصف والسعر والتوفر.',
title: "حقوق ومسؤوليات المالك",
description: "يتحمل المالك مسؤولية دقة المعلومات المقدمة عن العقار بما في ذلك الصور والوصف والسعر والتوفر.",
},
{
title: 'حقوق ومسؤوليات المستأجر',
description:
'يلتزم المستأجر باستخدام العقار بطريقة مسؤولة وعدم التسبب في أي ضرر للممتلكات.',
title: "حقوق ومسؤوليات المستأجر",
description: "يلتزم المستأجر باستخدام العقار بطريقة مسؤولة وعدم التسبب في أي ضرر للممتلكات.",
},
{
title: 'الدفع والعمولات',
description:
'تتقاضى المنصة عمولة على كل حصة ناجحة وفقاً للنسبة المحددة في وقت الحجز.',
title: "الدفع والعمولات",
description: "تتقاضى المنصة عمولة على كل حصة ناجحة وفقاً للنسبة المحددة في وقت الحجز.",
},
{
title: 'خصوصية البيانات',
description:
'نحن نأخذ خصوصية بياناتك على محمل الجد. يتم جمع واستخدام البيانات الشخصية وفقاً لسياسة الخصوصية الخاصة بنا.',
title: "خصوصية البيانات",
description: "نحن نأخذ خصوصية بياناتك على محمل الجد. يتم جمع واستخدام البيانات الشخصية وفقاً لسياسة الخصوصية الخاصة بنا.",
},
],
en: [
{
title: 'Introduction',
description:
'Welcome to SweetHome. By using our platform, you agree to comply with these terms. If you do not agree, please do not use the platform.',
title: "Introduction",
description: "Welcome to SweetHome. By using our platform, you agree to comply with these terms. If you do not agree, please do not use the platform.",
},
{
title: 'Platform Usage',
description:
'The platform may only be used for lawful purposes. Users must not engage in any illegal activity.',
title: "Platform Usage",
description: "The platform may only be used for lawful purposes. Users must not engage in any illegal activity.",
},
{
title: 'Owner Rights & Responsibilities',
description:
'Owners are responsible for the accuracy of property information including images, description, price, and availability.',
title: "Owner Rights & Responsibilities",
description: "Owners are responsible for the accuracy of property information including images, description, price, and availability.",
},
{
title: 'Tenant Rights & Responsibilities',
description:
'Tenants must use the property responsibly and not cause any damage to the property.',
title: "Tenant Rights & Responsibilities",
description: "Tenants must use the property responsibly and not cause any damage to the property.",
},
{
title: 'Payment & Commissions',
description:
'The platform charges a commission on each successful booking according to the rate specified at the time of booking.',
title: "Payment & Commissions",
description: "The platform charges a commission on each successful booking according to the rate specified at the time of booking.",
},
{
title: 'Data Privacy',
description:
'We take your data privacy seriously. Personal data is collected and used in accordance with our Privacy Policy.',
title: "Data Privacy",
description: "We take your data privacy seriously. Personal data is collected and used in accordance with our Privacy Policy.",
},
],
};
export default function TermsPage() {
const [terms, setTerms] = useState([]);
const [language, setLanguage] = useState('ar');
const [language, setLanguage] = useState("ar");
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [error, setError] = useState("");
useEffect(() => {
const controller = new AbortController();
@ -97,9 +85,9 @@ export default function TermsPage() {
const fetchTerms = async () => {
try {
setLoading(true);
setError('');
setError("");
const fetcher = language === 'ar' ? getARTerms : getENTerms;
const fetcher = language === "ar" ? getARTerms : getENTerms;
const data = await fetcher();
if (!data) {
@ -115,14 +103,14 @@ export default function TermsPage() {
}
const mapped = raw.map((item) => ({
title: item.title || item.name || '',
description: item.description || item.content || item.body || item.text || '',
title: item.title || item.name || "",
description: item.description || item.content || item.body || item.text || "",
}));
setTerms(mapped);
} catch {
setTerms(FALLBACK_TERMS[language]);
setError('');
setError("");
} finally {
setLoading(false);
}
@ -134,102 +122,62 @@ export default function TermsPage() {
}, [language]);
return (
<div
dir={language === 'ar' ? 'rtl' : 'ltr'}
className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12"
>
<div dir={language === "ar" ? "rtl" : "ltr"} className="min-h-screen bg-gradient-to-b py-12">
<div className="container mx-auto px-4 max-w-4xl">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center mb-12"
>
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="text-center mb-12">
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-amber-100">
<FileText className="w-10 h-10 text-amber-600" />
</div>
<div className="flex items-center justify-center gap-4 mb-4">
<h1 className="text-4xl font-bold text-gray-900">
{language === 'ar' ? 'شروط الاستخدام' : 'Terms of Use'}
</h1>
<h1 className="text-4xl font-bold text-gray-900">{language === "ar" ? "شروط الاستخدام" : "Terms of Use"}</h1>
<button
type="button"
onClick={() => setLanguage(language === 'ar' ? 'en' : 'ar')}
onClick={() => setLanguage(language === "ar" ? "en" : "ar")}
className="inline-flex items-center gap-2 rounded-full border border-amber-200 bg-white px-4 py-2 text-sm font-semibold text-gray-700 shadow-sm transition hover:shadow-md"
>
<Languages className="h-4 w-4" />
{language === 'ar' ? 'English' : 'العربية'}
{language === "ar" ? "English" : "العربية"}
</button>
</div>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
{language === 'ar'
? 'يرجى قراءة شروط الاستخدام التالية بعناية قبل استخدام المنصة'
: 'Please read the following terms of use carefully before using the platform'}
{language === "ar" ? "يرجى قراءة شروط الاستخدام التالية بعناية قبل استخدام المنصة" : "Please read the following terms of use carefully before using the platform"}
</p>
</motion.div>
{loading && (
<div className="flex items-center justify-center gap-3 mb-8 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-4 text-amber-800">
<Loader2 className="h-5 w-5 animate-spin" />
<span>
{language === 'ar'
? 'جاري تحميل شروط الاستخدام...'
: 'Loading terms of use...'}
</span>
<span>{language === "ar" ? "جاري تحميل شروط الاستخدام..." : "Loading terms of use..."}</span>
</div>
)}
{error && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="mb-8 rounded-2xl border border-red-200 bg-red-50 p-4 flex items-center gap-3 text-red-700"
>
<motion.div initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} className="mb-8 rounded-2xl border border-red-200 bg-red-50 p-4 flex items-center gap-3 text-red-700">
<AlertCircle className="h-5 w-5 shrink-0" />
<span>{error}</span>
</motion.div>
)}
{!loading && terms.length === 0 && !error && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-center py-16 text-gray-500"
>
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-center py-16 text-gray-500">
<FileText className="h-16 w-16 mx-auto mb-4 text-gray-300" />
<p className="text-xl font-medium">
{language === 'ar'
? 'لا توجد شروط استخدام متاحة حالياً'
: 'No terms of use available'}
</p>
<p className="text-xl font-medium">{language === "ar" ? "لا توجد شروط استخدام متاحة حالياً" : "No terms of use available"}</p>
</motion.div>
)}
{terms.length > 0 && (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="space-y-6"
>
<motion.div variants={containerVariants} initial="hidden" animate="visible" className="space-y-6">
{terms.map((term, index) => (
<motion.div
key={index}
variants={itemVariants}
className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow"
>
<motion.div key={index} variants={itemVariants} className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow">
<div className="flex items-start gap-4">
<div className="w-10 h-10 bg-amber-100 rounded-xl flex items-center justify-center shrink-0 mt-1">
<Shield className="w-5 h-5 text-amber-600" />
</div>
<div className="min-w-0 flex-1">
{term.title && (
<h2 className="text-xl font-bold text-gray-900 mb-3">{term.title}</h2>
)}
<p className="text-gray-600 leading-relaxed whitespace-pre-wrap">
{term.description}
</p>
{term.title && <h2 className="text-xl font-bold text-gray-900 mb-3">{term.title}</h2>}
<p className="text-gray-600 leading-relaxed whitespace-pre-wrap">{term.description}</p>
</div>
</div>
</motion.div>
@ -237,21 +185,14 @@ export default function TermsPage() {
</motion.div>
)}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.6 }}
className="mt-8 bg-amber-50 rounded-2xl border border-amber-200 p-6 flex items-start gap-4"
>
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.6 }} className="mt-8 bg-amber-50 rounded-2xl border border-amber-200 p-6 flex items-start gap-4">
<CheckCircle className="w-6 h-6 text-amber-600 shrink-0 mt-0.5" />
<div>
<p className="font-bold text-amber-800 mb-1">
{language === 'ar' ? 'آخر تحديث' : 'Last Updated'}
</p>
<p className="font-bold text-amber-800 mb-1">{language === "ar" ? "آخر تحديث" : "Last Updated"}</p>
<p className="text-amber-700">
{language === 'ar'
? 'تم آخر تحديث لشروط الاستخدام في 1 مايو 2026. يرجى مراجعة هذه الصفحة بشكل دوري للاطلاع على أي تغييرات.'
: 'Last updated on May 1, 2026. Please review this page periodically for any changes.'}
{language === "ar"
? "تم آخر تحديث لشروط الاستخدام في 1 مايو 2026. يرجى مراجعة هذه الصفحة بشكل دوري للاطلاع على أي تغييرات."
: "Last updated on May 1, 2026. Please review this page periodically for any changes."}
</p>
</div>
</motion.div>

View File

@ -345,9 +345,10 @@ export async function editSaleProperty(id, data) {
}
export async function addSaleProperty(data) {
return apiFetch("/SaleProperties/AddSaleProperty", {
method: "POST",
body: data,
body: data ,
});
}
@ -446,7 +447,7 @@ async function multipartAuthFetch(endpoint, formData) {
};
}
export async function addOwner(data, frontImage = null, backImage = null) {
export async function addOwner(data, frontImage = null, backImage = null, licenseImage = null) {
const formData = new FormData();
formData.append("FirstName", data.firstName || data.FirstName || "");
@ -467,6 +468,7 @@ export async function addOwner(data, frontImage = null, backImage = null) {
if (frontImage) formData.append("FrontIdCarImagePath", frontImage);
if (backImage) formData.append("RearIdCarImagePath", backImage);
if (licenseImage) formData.append("RearIdCarImagePath", licenseImage);
return multipartAuthFetch("/Owner/Add", formData);
}

54
middleware.js Normal file
View File

@ -0,0 +1,54 @@
import { NextResponse } from "next/server";
const publicRoutes = ["/", "/properties"];
const authRoutes = ["/login", "/auth/choose-role", "/forgot-password", "/register/owner", "/register/tenant", "/register/agent"];
const ownerRoutes = ["/owner/account-book", "/owner/bookings", "/owner/calendar", "/owner/profits", "/owner/properties/add", "/owner/reservations"];
//ممكن كبو لانو زيادة
const priveteRoute = ["/account-verification", "/booked-properties", "/change-password", "/favorites", "/my-rates", "/notifications", "/payments", "/profile", "/reservations"];
export function middleware(request) {
const { pathname } = request.nextUrl;
const token = request.cookies.get("auth_token")?.value;
const userCookie = request.cookies.get("cached_user")?.value;
let isOwner = null;
if (userCookie) {
try {
const user = JSON.parse(decodeURIComponent(userCookie));
isOwner = user.roles?.includes("Owner");
} catch (error) {
console.error("Cookie parse error:", error);
}
}
const isPublicRoute = publicRoutes.some((route) => pathname === route || (route !== "/" && pathname.startsWith(route)));
const isAuthRoute = authRoutes.some((route) => pathname === route || pathname.startsWith(route));
const IsOwner = ownerRoutes.some((route) => route === pathname || pathname.startsWith(route));
const IsPrivateRoute = priveteRoute.some((route) => pathname === route || pathname.startsWith(route));
if (!token && !isPublicRoute && !isAuthRoute) {
return NextResponse.redirect(new URL("/login", request.url));
}
if (token && isAuthRoute) {
return NextResponse.redirect(new URL("/", request.url));
}
if (IsOwner && !isOwner) {
return NextResponse.redirect(new URL("/", request.url));
}
if (IsPrivateRoute && !token) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico|.*\\.(?:png|jpg|jpeg|gif|svg|ico|webp|css|js|woff|woff2|ttf|eot)$).*)"],
};

View File

@ -1,169 +0,0 @@
: (
// /* OTP Verification Step */
// <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-gray-300 text-sm">
// {t("sendOTPTo")}{" "}
// <span className="text-white 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-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 && (
// <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>
<></>
// )}
// 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);
}