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 { useState, useEffect } from "react";
import { useRouter } from 'next/navigation'; import { useRouter } from "next/navigation";
import Link from 'next/link'; import Link from "next/link";
import { motion } from 'framer-motion'; import { motion } from "framer-motion";
import { useTranslation } from 'react-i18next'; import { useTranslation } from "react-i18next";
import { import { Mail, Phone, Shield, ChevronLeft, Loader2, CheckCircle, XCircle, Send, Key } from "lucide-react";
Mail, import toast, { Toaster } from "react-hot-toast";
Phone, import AuthService from "../services/AuthService";
Shield, import { sendEmailOTP, sendPhoneOTP, verifyEmail, verifyPhone } from "../utils/api";
ChevronLeft, import InteractiveBackground from "../components/Animation/Background";
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';
export default function AccountVerificationPage() { export default function AccountVerificationPage() {
const router = useRouter(); const router = useRouter();
@ -26,8 +17,8 @@ export default function AccountVerificationPage() {
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [emailCode, setEmailCode] = useState(''); const [emailCode, setEmailCode] = useState("");
const [phoneCode, setPhoneCode] = useState(''); const [phoneCode, setPhoneCode] = useState("");
const [sendingEmailOTP, setSendingEmailOTP] = useState(false); const [sendingEmailOTP, setSendingEmailOTP] = useState(false);
const [sendingPhoneOTP, setSendingPhoneOTP] = useState(false); const [sendingPhoneOTP, setSendingPhoneOTP] = useState(false);
const [verifyingEmail, setVerifyingEmail] = useState(false); const [verifyingEmail, setVerifyingEmail] = useState(false);
@ -43,7 +34,7 @@ export default function AccountVerificationPage() {
setUser(authUser); setUser(authUser);
setIsLoading(false); setIsLoading(false);
} else { } else {
router.push('/login'); router.push("/login");
} }
}, [router]); }, [router]);
@ -51,10 +42,10 @@ export default function AccountVerificationPage() {
setSendingEmailOTP(true); setSendingEmailOTP(true);
try { try {
await sendEmailOTP(); await sendEmailOTP();
toast.success(t('accountVerification.emailOtpSent')); toast.success(t("accountVerification.emailOtpSent"));
setShowEmailInput(true); setShowEmailInput(true);
} catch (err) { } catch (err) {
toast.error(err.message || t('accountVerification.otpSendFailed')); toast.error(err.message || t("accountVerification.otpSendFailed"));
} finally { } finally {
setSendingEmailOTP(false); setSendingEmailOTP(false);
} }
@ -62,7 +53,7 @@ export default function AccountVerificationPage() {
const handleVerifyEmail = async () => { const handleVerifyEmail = async () => {
if (!emailCode.trim()) { if (!emailCode.trim()) {
toast.error(t('accountVerification.otpRequired')); toast.error(t("accountVerification.otpRequired"));
return; return;
} }
setVerifyingEmail(true); setVerifyingEmail(true);
@ -70,9 +61,9 @@ export default function AccountVerificationPage() {
await verifyEmail(emailCode.trim()); await verifyEmail(emailCode.trim());
setEmailVerified(true); setEmailVerified(true);
setShowEmailInput(false); setShowEmailInput(false);
toast.success(t('accountVerification.emailVerifiedSuccess')); toast.success(t("accountVerification.emailVerifiedSuccess"));
} catch (err) { } catch (err) {
toast.error(err.message || t('accountVerification.otpInvalid')); toast.error(err.message || t("accountVerification.otpInvalid"));
} finally { } finally {
setVerifyingEmail(false); setVerifyingEmail(false);
} }
@ -82,10 +73,10 @@ export default function AccountVerificationPage() {
setSendingPhoneOTP(true); setSendingPhoneOTP(true);
try { try {
await sendPhoneOTP(); await sendPhoneOTP();
toast.success(t('accountVerification.phoneOtpSent')); toast.success(t("accountVerification.phoneOtpSent"));
setShowPhoneInput(true); setShowPhoneInput(true);
} catch (err) { } catch (err) {
toast.error(err.message || t('accountVerification.otpSendFailed')); toast.error(err.message || t("accountVerification.otpSendFailed"));
} finally { } finally {
setSendingPhoneOTP(false); setSendingPhoneOTP(false);
} }
@ -93,7 +84,7 @@ export default function AccountVerificationPage() {
const handleVerifyPhone = async () => { const handleVerifyPhone = async () => {
if (!phoneCode.trim()) { if (!phoneCode.trim()) {
toast.error(t('accountVerification.otpRequired')); toast.error(t("accountVerification.otpRequired"));
return; return;
} }
setVerifyingPhone(true); setVerifyingPhone(true);
@ -101,74 +92,56 @@ export default function AccountVerificationPage() {
await verifyPhone(phoneCode.trim()); await verifyPhone(phoneCode.trim());
setPhoneVerified(true); setPhoneVerified(true);
setShowPhoneInput(false); setShowPhoneInput(false);
toast.success(t('accountVerification.phoneVerifiedSuccess')); toast.success(t("accountVerification.phoneVerifiedSuccess"));
} catch (err) { } catch (err) {
toast.error(err.message || t('accountVerification.otpInvalid')); toast.error(err.message || t("accountVerification.otpInvalid"));
} finally { } finally {
setVerifyingPhone(false); setVerifyingPhone(false);
} }
}; };
const containerVariants = { const containerVariants = {
hidden: { opacity: 0 }, hidden: { opacity: 0 },
visible: { visible: {
opacity: 1, opacity: 1,
transition: { staggerChildren: 0.1 } transition: { staggerChildren: 0.1 },
} },
}; };
const itemVariants = { const itemVariants = {
hidden: { opacity: 0, y: 20 }, hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 } visible: { opacity: 1, y: 0 },
}; };
return ( 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} /> <Toaster position="top-center" reverseOrder={false} />
<InteractiveBackground />
<div className="container mx-auto px-4 max-w-2xl"> <div className="relative z-10 container mx-auto px-4 max-w-2xl">
<motion.div <motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="flex items-center gap-3 mb-8">
initial={{ opacity: 0, y: -20 }} <Link href="/settings" className="p-2 rounded-xl hover:bg-gray-200 transition-colors text-gray-600">
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" /> <ChevronLeft className="w-5 h-5" />
</Link> </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>
<motion.div <motion.div variants={containerVariants} initial="hidden" animate="visible" className="space-y-6">
variants={containerVariants}
initial="hidden"
animate="visible"
className="space-y-6"
>
<motion.div variants={itemVariants} className="bg-white rounded-2xl shadow-sm overflow-hidden"> <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"> <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" /> <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 && ( {emailVerified && (
<span className="flex items-center gap-1 text-xs text-green-600 bg-green-50 px-2 py-0.5 rounded-full"> <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" /> <CheckCircle className="w-3 h-3" />
{t('accountVerification.verified')} {t("accountVerification.verified")}
</span> </span>
)} )}
</div> </div>
<div className="p-6"> <div className="p-6">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<div> <div>
<p className="text-sm text-gray-600">{user?.email || t('accountVerification.emailNotRegistered')}</p> <p className="text-sm text-gray-600">{user?.email || t("accountVerification.emailNotRegistered")}</p>
<p className="text-xs text-gray-400 mt-1"> <p className="text-xs text-gray-400 mt-1">{emailVerified ? t("accountVerification.emailVerifiedDesc") : t("accountVerification.emailVerifyDesc")}</p>
{emailVerified
? t('accountVerification.emailVerifiedDesc')
: t('accountVerification.emailVerifyDesc')}
</p>
</div> </div>
{!emailVerified && !showEmailInput && ( {!emailVerified && !showEmailInput && (
<button <button
@ -176,29 +149,21 @@ export default function AccountVerificationPage() {
disabled={sendingEmailOTP} 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" 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 ? ( {sendingEmailOTP ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />}
<Loader2 className="w-4 h-4 animate-spin" /> {sendingEmailOTP ? t("accountVerification.sending") : t("accountVerification.sendOtp")}
) : (
<Send className="w-4 h-4" />
)}
{sendingEmailOTP ? t('accountVerification.sending') : t('accountVerification.sendOtp')}
</button> </button>
)} )}
</div> </div>
{showEmailInput && ( {showEmailInput && (
<motion.div <motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} className="space-y-3">
initial={{ opacity: 0, height: 0 }} <p className="text-xs text-gray-500">{t("accountVerification.enter6DigitCode")}</p>
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"> <div className="flex gap-2">
<input <input
type="text" type="text"
value={emailCode} value={emailCode}
onChange={(e) => setEmailCode(e.target.value)} onChange={(e) => setEmailCode(e.target.value)}
placeholder={t('accountVerification.otpPlaceholder')} placeholder={t("accountVerification.otpPlaceholder")}
maxLength={6} 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" 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} 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" 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 ? ( {verifyingEmail ? <Loader2 className="w-4 h-4 animate-spin" /> : <Key className="w-4 h-4" />}
<Loader2 className="w-4 h-4 animate-spin" /> {t("accountVerification.verify")}
) : (
<Key className="w-4 h-4" />
)}
{t('accountVerification.verify')}
</button> </button>
</div> </div>
<button <button onClick={handleSendEmailOTP} disabled={sendingEmailOTP} className="text-xs text-amber-600 hover:text-amber-700">
onClick={handleSendEmailOTP} {t("accountVerification.resendCode")}
disabled={sendingEmailOTP}
className="text-xs text-amber-600 hover:text-amber-700"
>
{t('accountVerification.resendCode')}
</button> </button>
</motion.div> </motion.div>
)} )}
@ -228,7 +185,7 @@ export default function AccountVerificationPage() {
{emailVerified && ( {emailVerified && (
<div className="flex items-center gap-2 text-green-600 bg-green-50 px-4 py-3 rounded-xl"> <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" /> <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>
)} )}
</div> </div>
@ -237,23 +194,19 @@ export default function AccountVerificationPage() {
<motion.div variants={itemVariants} className="bg-white rounded-2xl shadow-sm overflow-hidden"> <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"> <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" /> <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 && ( {phoneVerified && (
<span className="flex items-center gap-1 text-xs text-green-600 bg-green-50 px-2 py-0.5 rounded-full"> <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" /> <CheckCircle className="w-3 h-3" />
{t('accountVerification.verified')} {t("accountVerification.verified")}
</span> </span>
)} )}
</div> </div>
<div className="p-6"> <div className="p-6">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<div> <div>
<p className="text-sm text-gray-600">{user?.phone || t('accountVerification.phoneNotRegistered')}</p> <p className="text-sm text-gray-600">{user?.phone || t("accountVerification.phoneNotRegistered")}</p>
<p className="text-xs text-gray-400 mt-1"> <p className="text-xs text-gray-400 mt-1">{phoneVerified ? t("accountVerification.phoneVerifiedDesc") : t("accountVerification.phoneVerifyDesc")}</p>
{phoneVerified
? t('accountVerification.phoneVerifiedDesc')
: t('accountVerification.phoneVerifyDesc')}
</p>
</div> </div>
{!phoneVerified && !showPhoneInput && ( {!phoneVerified && !showPhoneInput && (
<button <button
@ -261,29 +214,21 @@ export default function AccountVerificationPage() {
disabled={sendingPhoneOTP} 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" 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 ? ( {sendingPhoneOTP ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />}
<Loader2 className="w-4 h-4 animate-spin" /> {sendingPhoneOTP ? t("accountVerification.sending") : t("accountVerification.sendOtp")}
) : (
<Send className="w-4 h-4" />
)}
{sendingPhoneOTP ? t('accountVerification.sending') : t('accountVerification.sendOtp')}
</button> </button>
)} )}
</div> </div>
{showPhoneInput && ( {showPhoneInput && (
<motion.div <motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} className="space-y-3">
initial={{ opacity: 0, height: 0 }} <p className="text-xs text-gray-500">{t("accountVerification.enter6DigitCodePhone")}</p>
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"> <div className="flex gap-2">
<input <input
type="text" type="text"
value={phoneCode} value={phoneCode}
onChange={(e) => setPhoneCode(e.target.value)} onChange={(e) => setPhoneCode(e.target.value)}
placeholder={t('accountVerification.otpPlaceholder')} placeholder={t("accountVerification.otpPlaceholder")}
maxLength={6} 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" 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} 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" 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 ? ( {verifyingPhone ? <Loader2 className="w-4 h-4 animate-spin" /> : <Key className="w-4 h-4" />}
<Loader2 className="w-4 h-4 animate-spin" /> {t("accountVerification.verify")}
) : (
<Key className="w-4 h-4" />
)}
{t('accountVerification.verify')}
</button> </button>
</div> </div>
<button <button onClick={handleSendPhoneOTP} disabled={sendingPhoneOTP} className="text-xs text-amber-600 hover:text-amber-700">
onClick={handleSendPhoneOTP} {t("accountVerification.resendCode")}
disabled={sendingPhoneOTP}
className="text-xs text-amber-600 hover:text-amber-700"
>
{t('accountVerification.resendCode')}
</button> </button>
</motion.div> </motion.div>
)} )}
@ -313,7 +250,7 @@ export default function AccountVerificationPage() {
{phoneVerified && ( {phoneVerified && (
<div className="flex items-center gap-2 text-green-600 bg-green-50 px-4 py-3 rounded-xl"> <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" /> <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>
)} )}
</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"> <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"> <div className="flex items-center gap-3 mb-3">
<Shield className="w-6 h-6" /> <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> </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"> <li className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 flex-shrink-0" /> <CheckCircle className="w-4 h-4 flex-shrink-0" />
{t('accountVerification.reason1')} {t("accountVerification.reason1")}
</li> </li>
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 flex-shrink-0" /> <CheckCircle className="w-4 h-4 flex-shrink-0" />
{t('accountVerification.reason2')} {t("accountVerification.reason2")}
</li> </li>
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 flex-shrink-0" /> <CheckCircle className="w-4 h-4 flex-shrink-0" />
{t('accountVerification.reason3')} {t("accountVerification.reason3")}
</li> </li>
</ul> </ul>
</motion.div> </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 toast, { Toaster } from "react-hot-toast";
import AuthService from "../services/AuthService"; import AuthService from "../services/AuthService";
import { sendGeneralReport } from "../utils/api"; import { sendGeneralReport } from "../utils/api";
import InteractiveBackground from "../components/Animation/Background";
export default function BlockedPage() { export default function BlockedPage() {
const { t } = useTranslation(); const { t } = useTranslation();
@ -49,10 +50,10 @@ export default function BlockedPage() {
}; };
return ( 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} /> <Toaster position="top-center" reverseOrder={false} />
<InteractiveBackground />
<motion.div initial={{ opacity: 0, y: 24 }} animate={{ opacity: 1, y: 0 }} className="w-full max-w-5xl"> <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"> <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"> <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" /> <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> <p className="text-gray-600 text-lg max-w-2xl mx-auto">{t("blocked.description")}</p>
</div> </div>
<div className="grid md:grid-cols-2 gap-6"> <div className="grid md:grid-cols-2 gap-6 relative z-10 ">
<motion.div <motion.div
initial={{ opacity: 0, x: -24 }} initial={{ opacity: 0, x: -24 }}
animate={{ opacity: 1, x: 0 }} 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 ( return (
<div className="min-h-screen bg-gray-50 py-8" dir="rtl"> <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 { useState } from "react";
import { motion } from 'framer-motion'; import { motion } from "framer-motion";
import { useRouter } from 'next/navigation'; import { useRouter } from "next/navigation";
import toast, { Toaster } from 'react-hot-toast'; import toast, { Toaster } from "react-hot-toast";
import { Lock, Eye, EyeOff, ArrowLeft, Shield } from 'lucide-react'; import { Lock, Eye, EyeOff, ArrowLeft, Shield } from "lucide-react";
import { useTranslation } from 'react-i18next'; import { useTranslation } from "react-i18next";
import { changePassword } from '../utils/api'; import { changePassword } from "../utils/api";
import AuthService from '../services/AuthService'; import AuthService from "../services/AuthService";
import InteractiveBackground from "../components/Animation/Background";
export default function ChangePasswordPage() { export default function ChangePasswordPage() {
const router = useRouter(); const router = useRouter();
const { t } = useTranslation(); 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 [show, setShow] = useState({ old: false, new: false, confirm: false });
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@ -24,162 +25,124 @@ export default function ChangePasswordPage() {
e.preventDefault(); e.preventDefault();
if (!AuthService.isAuthenticated()) { if (!AuthService.isAuthenticated()) {
toast.error(t('changePassword.loginRequired')); toast.error(t("changePassword.loginRequired"));
router.push('/login'); router.push("/login");
return; return;
} }
if (form.newPassword !== form.confirmPassword) { if (form.newPassword !== form.confirmPassword) {
toast.error(t('changePassword.passwordsDoNotMatch')); toast.error(t("changePassword.passwordsDoNotMatch"));
return; return;
} }
if (form.newPassword.length < 6) { if (form.newPassword.length < 6) {
toast.error(t('validation.passwordMinLength')); toast.error(t("validation.passwordMinLength"));
return; return;
} }
setIsLoading(true); setIsLoading(true);
try { try {
await changePassword(form.oldPassword, form.newPassword); await changePassword(form.oldPassword, form.newPassword);
toast.success(t('changePassword.changeSuccess')); toast.success(t("changePassword.changeSuccess"));
setTimeout(() => router.push('/profile'), 1200); setTimeout(() => router.push("/profile"), 1200);
} catch (err) { } catch (err) {
toast.error(err?.message || t('changePassword.changeFailed')); toast.error(err?.message || t("changePassword.changeFailed"));
} finally { } finally {
setIsLoading(false); 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 ( 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} /> <Toaster position="top-center" reverseOrder={false} />
<InteractiveBackground />
<motion.div <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 ">
initial={{ opacity: 0, y: 30 }} <motion.div initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} className="mb-6 relative z-10">
animate={{ opacity: 1, y: 0 }} <button onClick={() => router.push("/profile")} className="flex items-center relative z-10 gap-2 text-gray-600 hover:text-amber-600 transition-colors">
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"
>
<ArrowLeft className="w-5 h-5" /> <ArrowLeft className="w-5 h-5" />
<span>{t('changePassword.backToProfile')}</span> <span>{t("changePassword.backToProfile")}</span>
</button> </button>
</motion.div> </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"> <div className="bg-gradient-to-l from-amber-500 to-amber-600 p-8 text-center">
<motion.div <motion.div
initial={{ scale: 0 }} initial={{ scale: 0 }}
animate={{ scale: 1 }} 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" 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" /> <Shield className="w-8 h-8 text-white" />
</motion.div> </motion.div>
<motion.h1 <motion.h1 initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} className="text-3xl font-bold text-white mb-2">
initial={{ y: 20, opacity: 0 }} {t("changePassword.title")}
animate={{ y: 0, opacity: 1 }}
className="text-3xl font-bold text-white mb-2"
>
{t('changePassword.title')}
</motion.h1> </motion.h1>
<motion.p <motion.p initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} transition={{ delay: 0.1 }} className="text-amber-100">
initial={{ y: 20, opacity: 0 }} {t("changePassword.subtitle")}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.1 }}
className="text-amber-100"
>
{t('changePassword.subtitle')}
</motion.p> </motion.p>
</div> </div>
<form onSubmit={handleSubmit} className="p-8 space-y-6"> <form onSubmit={handleSubmit} className="p-8 space-y-6 " style={{ backgroundColor: "#0f172a" }}>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">{t("changePassword.currentPasswordLabel")}</label>
{t('changePassword.currentPasswordLabel')}
</label>
<div className="relative"> <div className="relative">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"> <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" /> <Lock className="w-5 h-5 text-gray-400" />
</div> </div>
<input <input
type={show.old ? 'text' : 'password'} type={show.old ? "text" : "password"}
value={form.oldPassword} value={form.oldPassword}
onChange={handleChange('oldPassword')} onChange={handleChange("oldPassword")}
className={inputClass} className={inputClass}
placeholder={t('changePassword.currentPasswordPlaceholder')} placeholder={t("changePassword.currentPasswordPlaceholder")}
required required
/> />
<button <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">
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" />} {show.old ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button> </button>
</div> </div>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">{t("changePassword.newPasswordLabel")}</label>
{t('changePassword.newPasswordLabel')}
</label>
<div className="relative"> <div className="relative">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"> <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" /> <Lock className="w-5 h-5 text-gray-400" />
</div> </div>
<input <input
type={show.new ? 'text' : 'password'} type={show.new ? "text" : "password"}
value={form.newPassword} value={form.newPassword}
onChange={handleChange('newPassword')} onChange={handleChange("newPassword")}
className={inputClass} className={inputClass}
placeholder={t('changePassword.newPasswordPlaceholder')} placeholder={t("changePassword.newPasswordPlaceholder")}
required required
minLength={6} minLength={6}
/> />
<button <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">
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" />} {show.new ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button> </button>
</div> </div>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">{t("changePassword.confirmNewPasswordLabel")}</label>
{t('changePassword.confirmNewPasswordLabel')}
</label>
<div className="relative"> <div className="relative">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"> <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" /> <Lock className="w-5 h-5 text-gray-400" />
</div> </div>
<input <input
type={show.confirm ? 'text' : 'password'} type={show.confirm ? "text" : "password"}
value={form.confirmPassword} value={form.confirmPassword}
onChange={handleChange('confirmPassword')} onChange={handleChange("confirmPassword")}
className={inputClass} className={inputClass}
placeholder={t('changePassword.confirmPasswordPlaceholder')} placeholder={t("changePassword.confirmPasswordPlaceholder")}
required required
minLength={6} minLength={6}
/> />
<button <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">
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" />} {show.confirm ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button> </button>
</div> </div>
@ -195,10 +158,10 @@ export default function ChangePasswordPage() {
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center gap-2"> <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" /> <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> </div>
) : ( ) : (
t('changePassword.changePassword') t("changePassword.changePassword")
)} )}
</motion.button> </motion.button>
</form> </form>

View File

@ -5,6 +5,7 @@ import { usePathname } from "next/navigation";
import { Home, Building, Calendar, CreditCard, Briefcase, BookOpen } from "lucide-react"; import { Home, Building, Calendar, CreditCard, Briefcase, BookOpen } from "lucide-react";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { label } from "framer-motion/client";
export default function BottomNav({ isOwner, isOwnerOrAgent }) { export default function BottomNav({ isOwner, isOwnerOrAgent }) {
const { t } = useTranslation(); const { t } = useTranslation();
@ -19,8 +20,15 @@ export default function BottomNav({ isOwner, isOwnerOrAgent }) {
const items = [ const items = [
{ href: "/", label: t("home"), icon: Home }, { href: "/", label: t("home"), icon: Home },
{ href: "/properties", label: t("ourProperties"), icon: Building }, { href: "/properties", label: t("ourProperties"), icon: Building },
...(isOwnerOrAgent ? [{ href: "/owner/properties", label: t("myProperties"), icon: Briefcase }] : []), ...(isOwnerOrAgent
{ href: bookingsHref, label: t("reservations"), icon: Calendar }, ? [
{ 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 }, { href: "/payments", label: t("payments"), icon: CreditCard },
...(isOwnerOrAgent ? [{ href: "/owner/account-book", label: t("accountBook"), icon: BookOpen }] : []), ...(isOwnerOrAgent ? [{ href: "/owner/account-book", label: t("accountBook"), icon: BookOpen }] : []),
]; ];
@ -46,9 +54,7 @@ export default function BottomNav({ isOwner, isOwnerOrAgent }) {
<div className="relative"> <div className="relative">
<Icon className="w-6 h-6" /> <Icon className="w-6 h-6" />
{it.badge > 0 && ( {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"> <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>
{it.badge > 9 ? "9+" : it.badge}
</div>
)} )}
</div> </div>
<div className="mt-1 text-xs text-center" aria-hidden> <div className="mt-1 text-xs text-center" aria-hidden>

View File

@ -76,9 +76,9 @@ export default function Owner() {
try { try {
const licenseImage = isCompany ? idImages.license : null; 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) { 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); const loginRes = await loginWithEmail(formData.email, formData.password);
if (loginRes.status === 206) { if (loginRes.status === 206) {
const otpToken = loginRes.data; const otpToken = loginRes.data;
@ -150,15 +150,7 @@ export default function Owner() {
</> </>
)} )}
<motion.div variants={fadeInUp} className="flex gap-3 pt-4"> <motion.div variants={fadeInUp} className="flex gap-3 pt-4">
{step === 1 ? ( {step === 1 ? ( <> <ButtomStepOne type={"owner"} /></>) : ( <> <ButtomStepTwo formData={formData} isLoading={isLoading} setStep={setStep} type={"owner"} /> </>)}
<>
<ButtomStepOne type={"owner"} />
</>
) : (
<>
<ButtomStepTwo formData={formData} isLoading={isLoading} setStep={setStep} type={"owner"} />
</>
)}
</motion.div> </motion.div>
</motion.form> </motion.form>
</div> </div>

View File

@ -18,7 +18,21 @@ const City = Object.freeze({
QAMISHLI: 'القامشلي', QAMISHLI: 'القامشلي',
RURAL_DAMASCUS: 'ريف دمشق', 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 // All cities as a flat array
const CitiesList = Object.freeze(Object.values(City)); const CitiesList = Object.freeze(Object.values(City));
@ -51,4 +65,6 @@ const CityTranslationKeys = Object.freeze({
[City.RURAL_DAMASCUS]: 'city.ruralDamascus', [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 ( 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"> <div className="container mx-auto px-4 max-w-4xl">
<motion.div <motion.div
initial={{ opacity: 0, y: -20 }} initial={{ opacity: 0, y: -20 }}

View File

@ -1,14 +1,15 @@
'use client'; "use client";
import { useEffect, useState } from 'react'; import { useEffect, useState } from "react";
import { useTranslation } from 'react-i18next'; import { useTranslation } from "react-i18next";
import { useRouter } from 'next/navigation'; import { useRouter } from "next/navigation";
import { motion } from 'framer-motion'; import { motion } from "framer-motion";
import Link from 'next/link'; import Link from "next/link";
import Image from 'next/image'; import Image from "next/image";
import { Heart, MapPin, Bed, Bath, Square, X, ImageIcon } from 'lucide-react'; import { Heart, MapPin, Bed, Bath, Square, X, ImageIcon } from "lucide-react";
import { useFavorites } from '@/app/contexts/FavoritesContext'; import { useFavorites } from "@/app/contexts/FavoritesContext";
import AuthService from '@/app/services/AuthService'; import AuthService from "@/app/services/AuthService";
import Loading from "../loading";
export default function FavoritesPage() { export default function FavoritesPage() {
const { t } = useTranslation(); const { t } = useTranslation();
@ -25,38 +26,28 @@ export default function FavoritesPage() {
}, [router]); }, [router]);
const formatCurrency = (amount) => { const formatCurrency = (amount) => {
return amount?.toLocaleString() + ' ' + t('currency-syp-suffix'); return amount?.toLocaleString() + " " + t("currency-syp-suffix");
}; };
if (favoritesLoading && favorites.length === 0) { if (favoritesLoading && favorites.length === 0) {
return ( return <Loading />;
<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 ( return (
<div className="min-h-screen bg-gray-50 py-8"> <div className="min-h-screen bg-gray-50 py-8">
<div className="container mx-auto px-4 max-w-6xl"> <div className="container mx-auto px-4 max-w-6xl">
<div className="mb-8"> <div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t('favorites')}</h1> <h1 className="text-3xl font-bold text-gray-900 mb-2">{t("favorites")}</h1>
<p className="text-gray-600">{t('saved-properties')}</p> <p className="text-gray-600">{t("saved-properties")}</p>
</div> </div>
{favorites.length === 0 ? ( {favorites.length === 0 ? (
<div className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300"> <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" /> <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> <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> <p className="text-gray-500 mb-6">{t("add-favorites-hint")}</p>
<Link <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">
href="/properties" {t("browse-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> </Link>
</div> </div>
) : ( ) : (
@ -70,13 +61,7 @@ export default function FavoritesPage() {
> >
<div className="relative h-48 bg-gray-100"> <div className="relative h-48 bg-gray-100">
{property.images && property.images[0] ? ( {property.images && property.images[0] ? (
<Image <Image src={property.images[0]} alt={property.title} fill className="object-cover" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" />
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"> <div className="w-full h-full flex items-center justify-center">
<ImageIcon className="w-12 h-12 text-gray-400" /> <ImageIcon className="w-12 h-12 text-gray-400" />
@ -95,7 +80,7 @@ export default function FavoritesPage() {
<div> <div>
<div className="flex items-center gap-2 mb-2"> <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"> <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> </span>
</div> </div>
<h3 className="font-bold text-gray-900 mb-1 line-clamp-1">{property.title}</h3> <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>
<div className="text-left"> <div className="text-left">
<div className="text-xl font-bold text-gray-900">{formatCurrency(property.price)}</div> <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>
</div> </div>
@ -129,11 +114,8 @@ export default function FavoritesPage() {
</div> </div>
</div> </div>
<Link <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">
href={`/property/${property.id}`} {t("viewDetails")}
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> </Link>
</div> </div>
</motion.div> </motion.div>
@ -143,4 +125,4 @@ export default function FavoritesPage() {
</div> </div>
</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 { Mail, Key, Lock, CheckCircle, ArrowLeft, RefreshCw } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { requestForgetPasswordOtp, verifyForgetPasswordOtp } from '../utils/api'; import { requestForgetPasswordOtp, verifyForgetPasswordOtp } from '../utils/api';
import InteractiveBackground from '../components/Animation/Background';
export default function ForgotPasswordPage() { export default function ForgotPasswordPage() {
const router = useRouter(); const router = useRouter();
@ -68,6 +69,7 @@ export default function ForgotPasswordPage() {
return ( return (
<div className="min-h-screen flex items-center justify-center p-4 relative overflow-hidden"> <div className="min-h-screen flex items-center justify-center p-4 relative overflow-hidden">
<Toaster position="top-center" reverseOrder={false} /> <Toaster position="top-center" reverseOrder={false} />
<InteractiveBackground/>
<div className="absolute inset-0 overflow-hidden"> <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 -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> <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 { getCustomerRatings } from '../utils/ratings';
import AuthService from '../services/AuthService'; import AuthService from '../services/AuthService';
import StarRating from '../components/ratings/StarRating'; import StarRating from '../components/ratings/StarRating';
import { useEffect, useState } from 'react';
const RATING_FIELDS = [ const RATING_FIELDS = [
{ key: 'cleanRating', label: 'ratings.cleanliness' }, { 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); const overall = calcOverall(ratings);

View File

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

View File

@ -76,7 +76,7 @@ export default function OnboardingPage() {
}; };
return ( 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"> <div className="absolute inset-0 overflow-hidden">
{[...Array(30)].map((_, i) => ( {[...Array(30)].map((_, i) => (
<motion.div <motion.div
@ -144,7 +144,7 @@ export default function OnboardingPage() {
transition={{ type: 'spring', stiffness: 200, delay: 0.1 }} 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" 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> </motion.div>
</div> </div>
</div> </div>
@ -154,7 +154,7 @@ export default function OnboardingPage() {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.15 }} 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} {step.title}
</motion.h1> </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 toast, { Toaster } from "react-hot-toast";
import AuthService from "@/app/services/AuthService"; import AuthService from "@/app/services/AuthService";
import { getOwnerStatistics } from "@/app/utils/api"; import { getOwnerStatistics } from "@/app/utils/api";
import Loading from "@/app/loading";
const StatCard = ({ title, value, icon: Icon, color, subtitle, isNA }) => ( 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"> <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, totalRevenue: data.totalRevenue ?? null,
totalReservations: data.totalReservations ?? null, totalReservations: data.totalReservations ?? null,
activeProperties: data.activeProperties ?? null, activeProperties: data.activeProperties ?? null,
financialRevenue: data.financialRevenue ?? data.totalRevenue ?? null, financialRevenue: data.financialRevenue ?? data.totalRevenue ?? null,
financialCommission: data.financialCommission ?? null, financialCommission: data.financialCommission ?? null,
financialBalance: data.financialBalance ?? null, financialBalance: data.financialBalance ?? null,
directRevenue: data.directRevenue ?? null, directRevenue: data.directRevenue ?? null,
@ -106,14 +105,7 @@ export default function OwnerAccountBookPage() {
const isNA = (val) => val === null || val === undefined || val === ""; const isNA = (val) => val === null || val === undefined || val === "";
if (isLoading) { if (isLoading) {
return ( return <Loading />;
<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 ( return (

View File

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

View File

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

View File

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

View File

@ -1200,37 +1200,6 @@ export default function PropertyDetailsPage() {
</motion.div> </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> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

@ -345,9 +345,10 @@ export async function editSaleProperty(id, data) {
} }
export async function addSaleProperty(data) { export async function addSaleProperty(data) {
return apiFetch("/SaleProperties/AddSaleProperty", { return apiFetch("/SaleProperties/AddSaleProperty", {
method: "POST", 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(); const formData = new FormData();
formData.append("FirstName", data.firstName || data.FirstName || ""); 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 (frontImage) formData.append("FrontIdCarImagePath", frontImage);
if (backImage) formData.append("RearIdCarImagePath", backImage); if (backImage) formData.append("RearIdCarImagePath", backImage);
if (licenseImage) formData.append("RearIdCarImagePath", licenseImage);
return multipartAuthFetch("/Owner/Add", formData); 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);
}