add Middleware + fixed all pages and fixed add propertise
This commit is contained in:
@ -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>
|
||||
|
||||
@ -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 }}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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, };
|
||||
|
||||
@ -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 }}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
2642
app/i18n/config.js
2642
app/i18n/config.js
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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
@ -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>
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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"} />}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
54
middleware.js
Normal 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)$).*)"],
|
||||
};
|
||||
@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user