add Middleware + fixed all pages and fixed add propertise

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
'use client';
"use client";
import { motion } from 'framer-motion';
import { AlertTriangle, RefreshCw, Home } from 'lucide-react';
import Link from 'next/link';
import { motion } from "framer-motion";
import { AlertTriangle, RefreshCw, Home } from "lucide-react";
import Link from "next/link";
export default function Error({ error, reset }) {
return (

View File

@ -1,89 +1,43 @@
'use client';
"use client";
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { useTranslation } from 'react-i18next';
import {
Calendar,
Home,
User,
Mail,
Phone,
DollarSign,
CheckCircle,
XCircle,
Clock,
MapPin,
Bed,
Bath,
Square,
CalendarDays,
ChevronLeft,
ChevronRight,
Eye,
MessageCircle,
ArrowLeft,
Loader2,
Filter,
Search,
Download,
TrendingUp,
Users,
Building
} from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
import AuthService from '../../services/AuthService';
import Image from 'next/image';
import { useState, useEffect, useMemo } from "react";
import { motion } from "framer-motion";
import { useRouter } from "next/navigation";
import { useTranslation } from "react-i18next";
import { Calendar, User, Mail, Phone, CheckCircle, XCircle, Clock, MapPin, Bed, Bath, Square, CalendarDays, ChevronLeft, ChevronRight, Eye, Loader2, Search } from "lucide-react";
import toast, { Toaster } from "react-hot-toast";
import { getMyRentListings, getOwnerReservationRequests } from "@/app/utils/api";
import useAuth from "@/app/hooks/useAuth";
const OwnerBookingCalendar = ({ property, onDateSelect, selectedDates }) => {
const { t } = useTranslation();
const [currentMonth, setCurrentMonth] = useState(new Date());
const [hoverDate, setHoverDate] = useState(null);
const daysInMonth = new Date(
currentMonth.getFullYear(),
currentMonth.getMonth() + 1,
0
).getDate();
const daysInMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 0).getDate();
const firstDayOfMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), 1).getDay();
const firstDayOfMonth = new Date(
currentMonth.getFullYear(),
currentMonth.getMonth(),
1
).getDay();
const monthNames = [
t('january'), t('february'), t('march'), t('april'), t('may'), t('june'),
t('july'), t('august'), t('september'), t('october'), t('november'), t('december')
];
const dayNames = [
t('dayFull.sunday'), t('dayFull.monday'), t('dayFull.tuesday'), t('dayFull.wednesday'),
t('dayFull.thursday'), t('dayFull.friday'), t('dayFull.saturday')
];
const monthNames = [t("january"), t("february"), t("march"), t("april"), t("may"), t("june"), t("july"), t("august"), t("september"), t("october"), t("november"), t("december")];
const dayNames = [t("dayFull.sunday"), t("dayFull.monday"), t("dayFull.tuesday"), t("dayFull.wednesday"), t("dayFull.thursday"), t("dayFull.friday"), t("dayFull.saturday")];
const isDateBooked = (date) => {
if (!property?.bookings) return false;
const dateStr = date.toISOString().split('T')[0];
return property.bookings.some(booking => {
if (!property?.bookings || !Array.isArray(property.bookings)) return false;
return property.bookings.some((booking) => {
const start = new Date(booking.startDate);
const end = new Date(booking.endDate);
const current = new Date(date);
return current >= start && current <= end;
return date >= start && date <= end;
});
};
const isDateSelected = (date) => {
if (!selectedDates) return false;
const dateStr = date.toISOString().split('T')[0];
const dateStr = date.toISOString().split("T")[0];
return dateStr === selectedDates.start || dateStr === selectedDates.end;
};
const isInRange = (date) => {
if (!selectedDates?.start || !selectedDates?.end) return false;
const dateStr = date.toISOString().split('T')[0];
const dateStr = date.toISOString().split("T")[0];
return dateStr > selectedDates.start && dateStr < selectedDates.end;
};
@ -101,11 +55,7 @@ const OwnerBookingCalendar = ({ property, onDateSelect, selectedDates }) => {
days.push(<div key={`empty-${i}`} className="p-2" />);
} else {
const dayNumber = i - firstDayOfMonth + 1;
const date = new Date(
currentMonth.getFullYear(),
currentMonth.getMonth(),
dayNumber
);
const date = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), dayNumber);
const isBooked = isDateBooked(date);
const isSelected = isDateSelected(date);
@ -121,18 +71,16 @@ const OwnerBookingCalendar = ({ property, onDateSelect, selectedDates }) => {
onMouseLeave={() => setHoverDate(null)}
className={`
p-2 rounded-lg text-center text-sm transition-all relative
${isBooked ? 'bg-red-100 text-red-500 cursor-not-allowed line-through' : ''}
${isSelected ? 'bg-amber-500 text-white shadow-md' : ''}
${inRange ? 'bg-amber-100' : ''}
${!isBooked && !isSelected ? 'hover:bg-amber-50 hover:text-amber-600 cursor-pointer' : ''}
${isToday && !isSelected && !isBooked ? 'border-2 border-amber-500' : ''}
${isBooked ? "bg-red-100 text-red-500 cursor-not-allowed line-through" : ""}
${isSelected ? "bg-amber-500 text-white shadow-md" : ""}
${inRange ? "bg-amber-100" : ""}
${!isBooked && !isSelected ? "hover:bg-amber-50 hover:text-amber-600 cursor-pointer" : ""}
${isToday && !isSelected && !isBooked ? "border-2 border-amber-500" : ""}
`}
>
{dayNumber}
{isBooked && (
<span className="absolute -top-1 -right-1 w-2 h-2 bg-red-500 rounded-full" />
)}
</button>
{isBooked && <span className="absolute -top-1 -right-1 w-2 h-2 bg-red-500 rounded-full" />}
</button>,
);
}
}
@ -142,10 +90,7 @@ const OwnerBookingCalendar = ({ property, onDateSelect, selectedDates }) => {
return (
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-6">
<button
onClick={() => setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1))}
className="p-2 hover:bg-gray-100 rounded-xl transition-colors"
>
<button onClick={() => setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1))} className="p-2 hover:bg-gray-100 rounded-xl transition-colors">
<ChevronRight className="w-5 h-5 text-gray-600" />
</button>
@ -154,10 +99,7 @@ const OwnerBookingCalendar = ({ property, onDateSelect, selectedDates }) => {
{monthNames[currentMonth.getMonth()]} {currentMonth.getFullYear()}
</h3>
<button
onClick={() => setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1))}
className="p-2 hover:bg-gray-100 rounded-xl transition-colors"
>
<button onClick={() => setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1))} className="p-2 hover:bg-gray-100 rounded-xl transition-colors">
<ChevronLeft className="w-5 h-5 text-gray-600" />
</button>
</div>
@ -168,53 +110,51 @@ const OwnerBookingCalendar = ({ property, onDateSelect, selectedDates }) => {
))}
</div>
<div className="grid grid-cols-7 gap-1">
{renderDays()}
</div>
<div className="grid grid-cols-7 gap-1">{renderDays()}</div>
<div className="flex flex-wrap gap-4 mt-6 pt-4 border-t border-gray-200 text-xs">
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-red-100 rounded" />
<span className="text-gray-600">{t('booked')}</span>
<span className="text-gray-600">{t("booked")}</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-amber-500 rounded" />
<span className="text-gray-600">{t('selected')}</span>
<span className="text-gray-600">{t("selected")}</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-amber-100 rounded" />
<span className="text-gray-600">{t('inRange')}</span>
<span className="text-gray-600">{t("inRange")}</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 border-2 border-amber-500 rounded" />
<span className="text-gray-600">{t('today')}</span>
<span className="text-gray-600">{t("today")}</span>
</div>
</div>
</div>
);
};
const BookingCard = ({ booking, onViewDetails, onContact }) => {
const BookingCard = ({ booking, onViewDetails }) => {
const { t } = useTranslation();
const formatCurrency = (amount) => {
return amount?.toLocaleString() + ' ' + t('syp');
return (amount || 0).toLocaleString() + " " + t("syp");
};
const getStatusBadge = (status) => {
const statusConfig = {
pending: { label: t('pending'), color: 'bg-yellow-100 text-yellow-800', icon: Clock },
confirmed: { label: t('confirmed'), color: 'bg-green-100 text-green-800', icon: CheckCircle },
cancelled: { label: t('cancelled'), color: 'bg-red-100 text-red-800', icon: XCircle },
completed: { label: t('completed'), color: 'bg-gray-100 text-gray-800', icon: CheckCircle }
pending: { label: t("pending"), color: "bg-yellow-100 text-yellow-800", icon: Clock },
confirmed: { label: t("confirmed"), color: "bg-green-100 text-green-800", icon: CheckCircle },
cancelled: { label: t("cancelled"), color: "bg-red-100 text-red-800", icon: XCircle },
completed: { label: t("completed"), color: "bg-gray-100 text-gray-800", icon: CheckCircle },
};
const config = statusConfig[status] || statusConfig.pending;
const Icon = config.icon;
return (
<span className={`inline-flex items-center gap-1 px-2 py-1 rounded-lg text-xs font-medium ${config.color}`}>
<Icon className="w-3 h-3" />
<span className={`inline-flex items-center gap-1 px-2.5 py-1 rounded-lg text-xs font-medium ${config.color}`}>
<Icon className="w-3.5 h-3.5" />
{config.label}
</span>
);
@ -224,71 +164,95 @@ const BookingCard = ({ booking, onViewDetails, onContact }) => {
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-2xl shadow-sm hover:shadow-md transition-all border border-gray-200 overflow-hidden"
className="bg-white rounded-2xl shadow-sm hover:shadow-md transition-all border border-gray-200 overflow-hidden flex flex-col justify-between"
>
<div className="p-5">
<div className="flex justify-between items-start mb-4">
<div className="flex justify-between items-start mb-4 gap-2">
<div>
<div className="flex items-center gap-2 mb-2">
<div className="flex items-center gap-2 mb-2 flex-wrap">
<h3 className="font-bold text-gray-900">{booking.propertyTitle}</h3>
{getStatusBadge(booking.status)}
</div>
<div className="flex items-center gap-1 text-gray-500 text-sm">
<MapPin className="w-4 h-4" />
{booking.location}
<div className="flex items-center gap-1 text-gray-500 text-sm line-clamp-1">
<MapPin className="w-4 h-4 shrink-0 text-amber-500" />
<span>{booking.location}</span>
</div>
</div>
<div className="text-left">
<div className="text-lg font-bold text-amber-600">{formatCurrency(booking.totalAmount)}</div>
<div className="text-xs text-gray-500">{t('totalAmount')}</div>
<div className="text-left shrink-0">
<div className="text-lg font-bold text-amber-600">{formatCurrency(booking.dailyRent)}</div>
<div className="text-xs text-gray-500">{t("dailyRent")}</div>
</div>
</div>
<div className="bg-gray-50 rounded-xl p-3 mb-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-amber-100 rounded-full flex items-center justify-center">
<User className="w-5 h-5 text-amber-600" />
<div className="bg-gray-50 rounded-xl p-3 mb-4 grid grid-cols-3 gap-2 text-center text-xs">
<div className="flex items-center justify-center gap-1 text-gray-700">
<Bed className="w-4 h-4 text-amber-500" />
<span>
{booking.propertyDetails?.bedrooms} {t("rooms")}
</span>
</div>
<div>
<p className="font-medium text-gray-900">{booking.tenantName}</p>
<div className="flex items-center gap-2 text-xs text-gray-500">
<div className="flex items-center justify-center gap-1 text-gray-700">
<Bath className="w-4 h-4 text-amber-500" />
<span>
{booking.propertyDetails?.bathrooms} {t("baths")}
</span>
</div>
<div className="flex items-center justify-center gap-1 text-gray-700">
<Square className="w-4 h-4 text-amber-500" />
<span>
{booking.propertyDetails?.area} {t("sqm")}
</span>
</div>
</div>
{booking.tenantName && (
<div className="bg-amber-50/50 border border-amber-100 rounded-xl p-3 mb-4">
<div className="flex items-center gap-3">
<div className="w-9 h-9 bg-amber-100 rounded-full flex items-center justify-center shrink-0">
<User className="w-4 h-4 text-amber-600" />
</div>
<div className="overflow-hidden text-xs">
<p className="font-semibold text-gray-900 truncate">{booking.tenantName}</p>
<div className="flex items-center gap-3 text-gray-500 mt-0.5">
{booking.tenantPhone && (
<span className="flex items-center gap-1">
<Phone className="w-3 h-3" />
{booking.tenantPhone}
<Mail className="w-3 h-3 mr-1" />
</span>
)}
{booking.tenantEmail && (
<span className="flex items-center gap-1">
<Mail className="w-3 h-3" />
{booking.tenantEmail}
</span>
)}
</div>
</div>
</div>
</div>
)}
<div className="grid grid-cols-2 gap-3 mb-4 text-center">
<div className="bg-gray-50 p-2 rounded-lg">
<div className="text-xs text-gray-500">{t("monthlyRent")}</div>
<div className="text-sm font-semibold text-gray-800">{formatCurrency(booking.monthlyRent)}</div>
</div>
<div className="bg-gray-50 p-2 rounded-lg">
<div className="text-xs text-gray-500">{t("deposit")}</div>
<div className="text-sm font-semibold text-gray-800">{formatCurrency(booking.deposit)}</div>
</div>
</div>
</div>
<div className="grid grid-cols-3 gap-3 mb-4 text-center">
<div className="bg-gray-50 p-2 rounded-lg">
<Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1" />
<div className="text-xs text-gray-500">{t('from')}</div>
<div className="text-sm font-medium">{booking.startDate}</div>
</div>
<div className="bg-gray-50 p-2 rounded-lg">
<Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1" />
<div className="text-xs text-gray-500">{t('to')}</div>
<div className="text-sm font-medium">{booking.endDate}</div>
</div>
<div className="bg-gray-50 p-2 rounded-lg">
<Clock className="w-4 h-4 text-amber-500 mx-auto mb-1" />
<div className="text-xs text-gray-500">{t('duration')}</div>
<div className="text-sm font-medium">{booking.days} {t('days')}</div>
</div>
</div>
<div className="flex gap-3 pt-3 border-t border-gray-100">
<div className="p-5 pt-0">
<button
onClick={() => onViewDetails(booking)}
className="flex-1 bg-gray-100 text-gray-700 py-2 rounded-xl text-sm font-medium hover:bg-gray-200 transition-colors flex items-center justify-center gap-2"
className="w-full bg-gray-100 text-gray-700 py-2.5 rounded-xl text-sm font-medium hover:bg-gray-200 transition-colors flex items-center justify-center gap-2"
>
<Eye className="w-4 h-4" />
{t('details')}
{t("details")}
</button>
</div>
</div>
</motion.div>
);
};
@ -298,7 +262,7 @@ const BookingDetailsModal = ({ booking, isOpen, onClose }) => {
if (!isOpen || !booking) return null;
const formatCurrency = (amount) => {
return amount?.toLocaleString() + ' ' + t('syp');
return (amount || 0).toLocaleString() + " " + t("syp");
};
return (
@ -310,98 +274,83 @@ const BookingDetailsModal = ({ booking, isOpen, onClose }) => {
onClick={onClose}
>
<motion.div
initial={{ scale: 0.9, y: 20 }}
initial={{ scale: 0.95, y: 20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.9, y: 20 }}
exit={{ scale: 0.95, y: 20 }}
className="bg-white rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="sticky top-0 bg-gradient-to-r from-amber-500 to-amber-600 p-6 text-white">
<div className="flex justify-between items-center">
<h2 className="text-xl font-bold">{t('bookingDetails')}</h2>
<button onClick={onClose} className="p-1 hover:bg-white/20 rounded-full">
<div className="sticky top-0 bg-gradient-to-r from-amber-500 to-amber-600 p-6 text-white z-10 flex justify-between items-center">
<div>
<h2 className="text-xl font-bold">{t("bookingDetails")}</h2>
<p className="text-amber-100 text-sm mt-0.5"># {booking.id}</p>
</div>
<button onClick={onClose} className="p-1 hover:bg-white/20 rounded-full transition-colors">
<XCircle className="w-6 h-6" />
</button>
</div>
<p className="text-amber-100 text-sm mt-1">{t('bookingId', { id: booking.id })}</p>
</div>
<div className="p-6 space-y-6">
<div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-3">{t('propertyInfo')}</h3>
<div className="space-y-2">
<p><span className="text-gray-500">{t('property')}:</span> {booking.propertyTitle}</p>
<p><span className="text-gray-500">{t('location')}:</span> {booking.location}</p>
{booking.propertyDetails && (
<div className="flex gap-3 mt-2">
<span className="text-sm bg-white px-2 py-1 rounded-lg">{booking.propertyDetails.bedrooms} {t('rooms')}</span>
<span className="text-sm bg-white px-2 py-1 rounded-lg">{booking.propertyDetails.bathrooms} {t('bathrooms')}</span>
<span className="text-sm bg-white px-2 py-1 rounded-lg">{booking.propertyDetails.area} {t('sqm')}</span>
{booking.images && booking.images.length > 0 && (
<div className="grid grid-cols-2 gap-3">
{booking.images.map((img, idx) => (
<img key={idx} src={img} alt={`property-${idx}`} className="w-full h-36 object-cover rounded-xl border border-gray-100 shadow-sm" />
))}
</div>
)}
<div className="bg-gray-50 p-4 rounded-xl space-y-2">
<h3 className="font-bold text-gray-900 mb-2">{t("propertyInfo")}</h3>
<p className="text-sm">
<span className="text-gray-500">{t("location")}:</span> {booking.location}
</p>
<p className="text-sm">
<span className="text-gray-500">{t("description")}:</span> {booking.parsedDetails?.description || "-"}
</p>
<div className="flex gap-2 pt-2 flex-wrap">
<span className="text-xs bg-white border border-gray-200 px-3 py-1.5 rounded-lg font-medium">
{booking.propertyDetails?.bedrooms} {t("rooms")}
</span>
<span className="text-xs bg-white border border-gray-200 px-3 py-1.5 rounded-lg font-medium">
{booking.propertyDetails?.bathrooms} {t("bathrooms")}
</span>
<span className="text-xs bg-white border border-gray-200 px-3 py-1.5 rounded-lg font-medium">
{booking.propertyDetails?.salons} {t("salons")}
</span>
<span className="text-xs bg-white border border-gray-200 px-3 py-1.5 rounded-lg font-medium">
{booking.propertyDetails?.area} {t("sqm")}
</span>
</div>
</div>
<div className="bg-amber-50/60 border border-amber-200/60 p-4 rounded-xl space-y-2">
<h3 className="font-bold text-amber-900 mb-2">{t("financialInfo")}</h3>
<div className="flex justify-between text-sm">
<span className="text-gray-600">{t("dailyRent")}</span>
<span className="font-semibold text-gray-900">{formatCurrency(booking.dailyRent)}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">{t("monthlyRent")}</span>
<span className="font-semibold text-gray-900">{formatCurrency(booking.monthlyRent)}</span>
</div>
<div className="flex justify-between text-sm pt-2 border-t border-amber-200 font-bold">
<span className="text-gray-900">{t("deposit")}</span>
<span className="text-amber-600 text-base">{formatCurrency(booking.deposit)}</span>
</div>
</div>
{booking.parsedDetails?.services && (
<div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-3">{t('tenantInfo')}</h3>
<div className="space-y-2">
<p><span className="text-gray-500">{t('name')}</span> {booking.tenantName}</p>
<p><span className="text-gray-500">{t('email')}</span> {booking.tenantEmail}</p>
<p><span className="text-gray-500">{t('phone')}</span> {booking.tenantPhone}</p>
<h3 className="font-bold text-gray-900 mb-2">{t("services")}</h3>
<div className="flex flex-wrap gap-2">
{booking.parsedDetails.services.map((srv, i) => (
<span key={i} className="text-xs bg-amber-100 text-amber-800 px-2.5 py-1 rounded-md font-medium">
{srv}
</span>
))}
</div>
</div>
<div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-3">{t('bookingDetails')}</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-gray-500">{t('startDate')}</p>
<p className="font-medium">{booking.startDate}</p>
</div>
<div>
<p className="text-gray-500">{t('endDate')}</p>
<p className="font-medium">{booking.endDate}</p>
</div>
<div>
<p className="text-gray-500">{t('durationDays', { days: booking.days })}</p>
<p className="font-medium">{booking.days} {t('days')}</p>
</div>
<div>
<p className="text-gray-500">{t('bookingStatus')}</p>
<p className="font-medium">{booking.status === 'pending' ? t('pending') :
booking.status === 'confirmed' ? t('confirmed') :
booking.status === 'cancelled' ? t('cancelled') : t('completed')}</p>
</div>
</div>
</div>
<div className="bg-amber-50 p-4 rounded-xl">
<h3 className="font-bold text-amber-700 mb-3">{t('financialInfo')}</h3>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-gray-600">{t('dailyPrice')}</span>
<span className="font-medium">{formatCurrency(booking.dailyPrice)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">{t('durationDays', { days: booking.days })}</span>
<span className="font-medium">{formatCurrency(booking.dailyPrice * booking.days)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">{t('securityDeposit')}</span>
<span className="font-medium">{formatCurrency(booking.securityDeposit || 0)}</span>
</div>
<div className="flex justify-between pt-2 border-t border-amber-200 font-bold">
<span className="text-gray-900">{t('total')}</span>
<span className="text-amber-600 text-lg">{formatCurrency(booking.totalAmount)}</span>
</div>
</div>
</div>
{booking.notes && (
<div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-2">{t('notes')}</h3>
<p className="text-gray-600">{booking.notes}</p>
</div>
)}
</div>
</motion.div>
@ -412,318 +361,236 @@ const BookingDetailsModal = ({ booking, isOpen, onClose }) => {
export default function OwnerBookingsPage() {
const { t, i18n } = useTranslation();
const router = useRouter();
const [user, setUser] = useState(null);
const { name, isOwner } = useAuth();
const [bookings, setBookings] = useState([]);
const [filteredBookings, setFilteredBookings] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [selectedBooking, setSelectedBooking] = useState(null);
const [filterStatus, setFilterStatus] = useState('all');
const [searchTerm, setSearchTerm] = useState('');
const [dateRange, setDateRange] = useState({ start: '', end: '' });
const [filterStatus, setFilterStatus] = useState("all");
const [searchTerm, setSearchTerm] = useState("");
const [dateRange, setDateRange] = useState({ start: "", end: "" });
const [showCalendar, setShowCalendar] = useState(false);
const isRtl = i18n.language === "ar";
useEffect(() => {
const authUser = AuthService.getUser();
if (authUser && AuthService.isOwner()) {
setUser({
name: authUser.name || authUser.email,
email: authUser.email,
role: 'owner',
if (isOwner) {
fetchData();
} else {
router.push("/auth/choose-role");
}
}, [isOwner, router]);
const fetchData = async () => {
try {
const response = await getOwnerReservationRequests();
let rawData = response;
if (response && response.data) {
rawData = response.data;
}
// تحويل البيانات إلى مصفوفة وتصفية العناصر الفارغة (null أو undefined)
const rawList = Array.isArray(rawData) ? rawData : rawData ? [rawData] : [];
const list = rawList.filter(Boolean);
const mappedBookings = list.map((item) => {
let parsedDetails = {};
try {
if (item?.propertyInformation?.detailsJSON) {
parsedDetails = JSON.parse(item.propertyInformation.detailsJSON);
}
} catch (e) {
console.error("Error parsing detailsJSON", e);
}
const statusMap = {
0: "pending",
1: "confirmed",
2: "completed",
3: "cancelled",
};
const statusKey = statusMap[item?.propertyInformation?.status] || "pending";
return {
id: item?.id,
dailyRent: item?.dailyRent,
monthlyRent: item?.monthlyRent,
deposit: item?.deposit,
createdAt: item?.createdAt,
startDate: item?.createdAt?.split("T")[0],
status: statusKey,
propertyTitle: parsedDetails.description || `${t("property")} #${item?.id}`,
location: item?.propertyInformation?.address || "",
images: item?.propertyInformation?.images || [],
propertyDetails: {
bedrooms: item?.propertyInformation?.numberOfBedRooms || 0,
bathrooms: item?.propertyInformation?.numberOfBathRooms || 0,
salons: item?.propertyInformation?.numberOfSalons || 0,
area: item?.propertyInformation?.space || 0,
},
parsedDetails,
tenantName: item?.tenantName || null,
tenantPhone: item?.tenantPhone || null,
tenantEmail: item?.tenantEmail || null,
};
});
loadBookings();
} else {
router.push('/auth/choose-role');
}
}, [router]);
const loadBookings = () => {
const storedBookings = localStorage.getItem('ownerBookings');
if (storedBookings) {
setBookings(JSON.parse(storedBookings));
setFilteredBookings(JSON.parse(storedBookings));
} else {
const mockBookings = [
{
id: 'BK001',
propertyId: 1,
propertyTitle: t('ownerBookings.mockB1Title'),
location: t('ownerBookings.mockB1Location'),
propertyDetails: { bedrooms: 5, bathrooms: 4, area: 450 },
tenantName: t('ownerBookings.mockB1Tenant'),
tenantEmail: 'ahmed@example.com',
tenantPhone: '0933111222',
startDate: '2024-03-10',
endDate: '2024-03-15',
days: 5,
dailyPrice: 500000,
totalAmount: 2500000,
securityDeposit: 500000,
status: 'confirmed',
createdAt: '2024-02-25',
notes: t('ownerBookings.mockB1Notes')
},
{
id: 'BK002',
propertyId: 2,
propertyTitle: t('ownerBookings.mockB2Title'),
location: t('ownerBookings.mockB2Location'),
propertyDetails: { bedrooms: 3, bathrooms: 2, area: 180 },
tenantName: t('ownerBookings.mockB2Tenant'),
tenantEmail: 'sara@example.com',
tenantPhone: '0945123789',
startDate: '2024-03-05',
endDate: '2024-03-08',
days: 3,
dailyPrice: 250000,
totalAmount: 750000,
securityDeposit: 250000,
status: 'pending',
createdAt: '2024-02-24',
notes: t('ownerBookings.mockB2Notes')
},
{
id: 'BK003',
propertyId: 3,
propertyTitle: t('ownerBookings.mockB3Title'),
location: t('ownerBookings.mockB3Location'),
propertyDetails: { bedrooms: 4, bathrooms: 3, area: 300 },
tenantName: t('ownerBookings.mockB3Tenant'),
tenantEmail: 'mohammed@example.com',
tenantPhone: '0956123456',
startDate: '2024-02-20',
endDate: '2024-03-20',
days: 30,
dailyPrice: 350000,
totalAmount: 10500000,
securityDeposit: 500000,
status: 'completed',
createdAt: '2024-02-15',
notes: t('ownerBookings.mockB3Notes')
}
];
setBookings(mockBookings);
setFilteredBookings(mockBookings);
localStorage.setItem('ownerBookings', JSON.stringify(mockBookings));
}
setBookings(mappedBookings);
} catch (error) {
console.error("Error loading listings:", error);
setBookings([]);
} finally {
setIsLoading(false);
}
};
const filteredBookings = useMemo(() => {
return bookings.filter((booking) => {
const matchesStatus = filterStatus === "all" || booking.status === filterStatus;
const matchesSearch =
!searchTerm ||
booking.tenantName?.toLowerCase().includes(searchTerm.toLowerCase()) ||
booking.propertyTitle?.toLowerCase().includes(searchTerm.toLowerCase()) ||
booking.location?.toLowerCase().includes(searchTerm.toLowerCase()) ||
String(booking.id).includes(searchTerm);
const handleViewDetails = (booking) => {
setSelectedBooking(booking);
};
const bookingDate = new Date(booking.startDate || booking.createdAt);
const matchesStart = !dateRange.start || bookingDate >= new Date(dateRange.start);
const matchesEnd = !dateRange.end || bookingDate <= new Date(dateRange.end);
const handleContact = (booking) => {
toast.success(t('openingChat', { name: booking.tenantName }), {
icon: '💬',
style: { background: '#dcfce7', color: '#166534' }
return matchesStatus && matchesSearch && matchesStart && matchesEnd;
});
};
}, [bookings, filterStatus, searchTerm, dateRange]);
const handleStatusChange = (bookingId, newStatus) => {
const updatedBookings = bookings.map(b =>
b.id === bookingId ? { ...b, status: newStatus } : b
);
setBookings(updatedBookings);
setFilteredBookings(updatedBookings);
localStorage.setItem('ownerBookings', JSON.stringify(updatedBookings));
toast.success(t('statusUpdated'));
};
const statusCounts = {
const statusCounts = useMemo(() => {
return {
all: bookings.length,
pending: bookings.filter(b => b.status === 'pending').length,
confirmed: bookings.filter(b => b.status === 'confirmed').length,
completed: bookings.filter(b => b.status === 'completed').length,
cancelled: bookings.filter(b => b.status === 'cancelled').length
pending: bookings.filter((b) => b.status === "pending").length,
confirmed: bookings.filter((b) => b.status === "confirmed").length,
completed: bookings.filter((b) => b.status === "completed").length,
cancelled: bookings.filter((b) => b.status === "cancelled").length,
};
}, [bookings]);
if (isLoading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<Loader2 className="w-12 h-12 text-amber-500 animate-spin mx-auto mb-4" />
<p className="text-gray-600">{t('loading')}</p>
<p className="text-gray-600">{t("loading")}</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 py-8" dir={i18n.language === 'ar' ? 'rtl' : 'ltr'}>
<div className="min-h-screen bg-gray-50 py-8" dir={isRtl ? "rtl" : "ltr"}>
<Toaster position="top-center" reverseOrder={false} />
<BookingDetailsModal
booking={selectedBooking}
isOpen={!!selectedBooking}
onClose={() => setSelectedBooking(null)}
/>
<BookingDetailsModal booking={selectedBooking} isOpen={!!selectedBooking} onClose={() => setSelectedBooking(null)} />
<div className="container mx-auto px-4">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4"
>
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4">
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t('myBookings')}</h1>
<p className="text-gray-600">{t('welcomeBookings', { name: user?.name, count: bookings.length })}</p>
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t("myBookings")}</h1>
<p className="text-gray-600">{t("welcomeBookings", { name: name, count: bookings.length })}</p>
</div>
<div className="flex gap-3">
<button
onClick={() => setShowCalendar(!showCalendar)}
className="px-4 py-2 bg-white border border-gray-300 rounded-xl text-gray-700 hover:bg-gray-50 transition-colors flex items-center gap-2"
className="px-4 py-2 bg-white border border-gray-300 rounded-xl text-gray-700 hover:bg-gray-50 transition-colors flex items-center gap-2 shadow-sm"
>
<Calendar className="w-5 h-5" />
{showCalendar ? t('hideCalendar') : t('showCalendar')}
{showCalendar ? t("hideCalendar") : t("showCalendar")}
</button>
</div>
</motion.div>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-8">
{[
{ id: "all", label: "allBookings", count: statusCounts.all, color: "text-gray-900", active: "border-gray-900 bg-gray-50" },
{ id: "pending", label: "pending", count: statusCounts.pending, color: "text-yellow-600", active: "border-yellow-500 bg-yellow-50" },
{ id: "confirmed", label: "confirmed", count: statusCounts.confirmed, color: "text-green-600", active: "border-green-500 bg-green-50" },
{ id: "completed", label: "completed", count: statusCounts.completed, color: "text-gray-600", active: "border-gray-500 bg-gray-50" },
{ id: "cancelled", label: "cancelled", count: statusCounts.cancelled, color: "text-red-600", active: "border-red-500 bg-red-50" },
].map((item, index) => (
<motion.div
key={item.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="bg-white rounded-xl shadow-sm p-4 text-center border border-gray-200 cursor-pointer hover:shadow-md transition-all"
onClick={() => setFilterStatus('all')}
transition={{ delay: 0.05 * (index + 1) }}
className={`bg-white rounded-xl shadow-sm p-4 text-center border cursor-pointer hover:shadow-md transition-all ${filterStatus === item.id ? item.active : "border-gray-200"}`}
onClick={() => setFilterStatus(item.id)}
>
<div className="text-2xl font-bold text-gray-900">{statusCounts.all}</div>
<div className="text-sm text-gray-600">{t('allBookings')}</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className={`bg-white rounded-xl shadow-sm p-4 text-center border cursor-pointer hover:shadow-md transition-all ${
filterStatus === 'pending' ? 'border-yellow-500 bg-yellow-50' : 'border-gray-200'
}`}
onClick={() => setFilterStatus('pending')}
>
<div className="text-2xl font-bold text-yellow-600">{statusCounts.pending}</div>
<div className="text-sm text-gray-600">{t('pending')}</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className={`bg-white rounded-xl shadow-sm p-4 text-center border cursor-pointer hover:shadow-md transition-all ${
filterStatus === 'confirmed' ? 'border-green-500 bg-green-50' : 'border-gray-200'
}`}
onClick={() => setFilterStatus('confirmed')}
>
<div className="text-2xl font-bold text-green-600">{statusCounts.confirmed}</div>
<div className="text-sm text-gray-600">{t('confirmed')}</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className={`bg-white rounded-xl shadow-sm p-4 text-center border cursor-pointer hover:shadow-md transition-all ${
filterStatus === 'completed' ? 'border-gray-500 bg-gray-50' : 'border-gray-200'
}`}
onClick={() => setFilterStatus('completed')}
>
<div className="text-2xl font-bold text-gray-600">{statusCounts.completed}</div>
<div className="text-sm text-gray-600">{t('completed')}</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className={`bg-white rounded-xl shadow-sm p-4 text-center border cursor-pointer hover:shadow-md transition-all ${
filterStatus === 'cancelled' ? 'border-red-500 bg-red-50' : 'border-gray-200'
}`}
onClick={() => setFilterStatus('cancelled')}
>
<div className="text-2xl font-bold text-red-600">{statusCounts.cancelled}</div>
<div className="text-sm text-gray-600">{t('cancelled')}</div>
<div className={`text-2xl font-bold ${item.color}`}>{item.count}</div>
<div className="text-sm text-gray-600">{t(item.label)}</div>
</motion.div>
))}
</div>
<div className="flex flex-col md:flex-row gap-4 mb-6">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
<Search className={`absolute top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400 ${isRtl ? "right-3" : "left-3"}`} />
<input
type="text"
placeholder={t('searchBookings')}
placeholder={t("searchBookings")}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
className={`w-full py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 ${isRtl ? "pr-10 pl-4" : "pl-10 pr-4"}`}
/>
</div>
<div className="flex gap-3">
<div className="flex flex-wrap md:flex-nowrap gap-3">
<input
type="date"
value={dateRange.start}
onChange={(e) => setDateRange({ ...dateRange, start: e.target.value })}
className="px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
placeholder={t('filterDateFrom')}
/>
<input
type="date"
value={dateRange.end}
onChange={(e) => setDateRange({ ...dateRange, end: e.target.value })}
className="px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
placeholder={t('filterDateTo')}
/>
{(dateRange.start || dateRange.end) && (
<button
onClick={() => setDateRange({ start: '', end: '' })}
className="px-4 py-3 bg-gray-100 text-gray-700 rounded-xl hover:bg-gray-200 transition-colors"
>
{t('clear')}
<button onClick={() => setDateRange({ start: "", end: "" })} className="px-4 py-3 bg-gray-100 text-gray-700 rounded-xl hover:bg-gray-200 transition-colors">
{t("clear")}
</button>
)}
</div>
</div>
{showCalendar && (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-6"
>
<OwnerBookingCalendar
property={{ bookings }}
/>
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="mb-6">
<OwnerBookingCalendar property={{ bookings }} />
</motion.div>
)}
{filteredBookings.length === 0 ? (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300"
>
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
<div className="w-24 h-24 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Calendar className="w-12 h-12 text-amber-600" />
</div>
<h3 className="text-xl font-bold text-gray-900 mb-2">{t('noBookings')}</h3>
<p className="text-gray-600 mb-4">
{filterStatus !== 'all' ? t('noBookingsInCategory') : t('noBookingsReceived')}
</p>
{filterStatus !== 'all' && (
<h3 className="text-xl font-bold text-gray-900 mb-2">{t("noBookings")}</h3>
<p className="text-gray-600 mb-4">{filterStatus !== "all" || searchTerm || dateRange.start || dateRange.end ? t("noBookingsInCategory") : t("noBookingsReceived")}</p>
{(filterStatus !== "all" || searchTerm || dateRange.start || dateRange.end) && (
<button
onClick={() => setFilterStatus('all')}
className="inline-flex items-center gap-2 bg-amber-500 text-white px-6 py-3 rounded-xl font-medium hover:bg-amber-600"
onClick={() => {
setFilterStatus("all");
setSearchTerm("");
setDateRange({ start: "", end: "" });
}}
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('viewAllBookings')}
{t("viewAllBookings")}
</button>
)}
</motion.div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{filteredBookings.map((booking) => (
<BookingCard
key={booking.id}
booking={booking}
onViewDetails={handleViewDetails}
onContact={handleContact}
/>
<BookingCard key={booking.id} booking={booking} onViewDetails={setSelectedBooking} />
))}
</div>
)}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

54
middleware.js Normal file
View File

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

View File

@ -1,169 +0,0 @@
: (
// /* OTP Verification Step */
// <motion.form key="otp" initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} onSubmit={handleVerifyOTP} className="space-y-6">
// <div className="text-center mb-4">
// <Shield className="w-12 h-12 text-amber-500 mx-auto mb-3" />
// <p className="text-gray-300 text-sm">
// {t("sendOTPTo")}{" "}
// <span className="text-white font-medium" dir="ltr">
// {formData.credential}
// </span>
// </p>
// </div>
// <div>
// <label className="block text-sm font-medium text-gray-300 mb-2">{t("otpLabel")}</label>
// <input
// type="text"
// value={otpCode}
// onChange={(e) => {
// setOtpCode(e.target.value);
// if (otpError) setOtpError("");
// }}
// className={`w-full px-4 py-4 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white text-center text-2xl tracking-[0.5em] placeholder-gray-500 transition-all ${
// otpError ? "border-red-500" : "border-gray-700"
// }`}
// placeholder="______"
// maxLength={6}
// dir="ltr"
// />
// {otpError && (
// <motion.p initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} className="text-red-500 text-sm mt-1 text-center">
// {otpError}
// </motion.p>
// )}
// </div>
// <motion.button
// type="submit"
// disabled={isLoading || isSuccess}
// className="relative w-full bg-gradient-to-r from-amber-500 to-amber-600 text-white py-4 rounded-xl font-bold text-lg disabled:opacity-50 disabled:cursor-not-allowed"
// whileHover={{ scale: 1.02 }}
// whileTap={{ scale: 0.98 }}
// >
// <span className="flex items-center justify-center gap-2">
// {isLoading ? (
// <>
// <Loader2 className="w-5 h-5 animate-spin" />
// {t("verifying")}
// </>
// ) : isSuccess ? (
// <>
// <CheckCircle className="w-5 h-5" />
// {t("success")}
// </>
// ) : (
// <>
// <KeyRound className="w-5 h-5" />
// {t("verify")}
// </>
// )}
// </span>
// </motion.button>
// <div className="flex items-center justify-between text-sm">
// <button
// type="button"
// onClick={() => {
// setStep("login");
// setOtpCode("");
// setOtpError("");
// }}
// className="text-gray-400 hover:text-white transition-colors"
// >
// ← {t("back")}
// </button>
// <button type="button" onClick={resendOTP} className="text-amber-400 hover:text-amber-300 transition-colors">
// {t("resendCode")}
// </button>
// </div>
// </motion.form>
<></>
// )}
// Send OTP
try {
if (loginMethod === "email") {
await sendEmailOTP();
} else {
await sendPhoneOTP();
}
} catch (otpErr) {
console.warn("[Login] OTP send failed, proceeding anyway:", otpErr);
}
setStep("otp");
} else if (result.status >= 500) {
console.error("[Login] Server error:", result.status, result.data);
toast.error(t("serverError"), {
style: { background: "#fee2e2", color: "#991b1b" },
});
} else if (result.status === 404) {
console.error("[Login] API endpoint not found:", result.status);
toast.error(t("apiNotFound"), {
style: { background: "#fee2e2", color: "#991b1b" },
});
} else if (result.status === 429) {
toast.error(t("tooManyRequests"), {
style: { background: "#fee2e2", color: "#991b1b" },
});
} else {
console.error("[Login] Unexpected status:", result.status, result.data);
toast.error(result.data?.message || result.data || t("invalidCredentials"), {
style: { background: "#fee2e2", color: "#991b1b" },
});
}
} catch (err) {
console.error("[Login] Error:", err);
toast.error(err.message || t("connectionError"), {
style: { background: "#fee2e2", color: "#991b1b" },
});
toast(t("otpRequired"), {
icon: "🔐",
style: { background: "#fef3c7", color: "#92400e" },
});
// Send OTP
try {
if (loginMethod === "email") {
await sendEmailOTP();
} else {
await sendPhoneOTP();
}
} catch (otpErr) {
console.warn("[Login] OTP send failed, proceeding anyway:", otpErr);
}
setStep("otp");
} else if (result.status >= 500) {
console.error("[Login] Server error:", result.status, result.data);
toast.error(t("serverError"), {
style: { background: "#fee2e2", color: "#991b1b" },
});
} else if (result.status === 404) {
console.error("[Login] API endpoint not found:", result.status);
toast.error(t("apiNotFound"), {
style: { background: "#fee2e2", color: "#991b1b" },
});
} else if (result.status === 429) {
toast.error(t("tooManyRequests"), {
style: { background: "#fee2e2", color: "#991b1b" },
});
} else {
console.error("[Login] Unexpected status:", result.status, result.data);
toast.error(result.data?.message || result.data || t("invalidCredentials"), {
style: { background: "#fee2e2", color: "#991b1b" },
});
}
} catch (err) {
console.error("[Login] Error:", err);
toast.error(err.message || t("connectionError"), {
style: { background: "#fee2e2", color: "#991b1b" },
});
} finally {
setIsLoading(false);
}