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 { useState, useEffect } from "react";
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from "next/navigation";
|
||||||
import Link from 'next/link';
|
import Link from "next/link";
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from "framer-motion";
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import { Mail, Phone, Shield, ChevronLeft, Loader2, CheckCircle, XCircle, Send, Key } from "lucide-react";
|
||||||
Mail,
|
import toast, { Toaster } from "react-hot-toast";
|
||||||
Phone,
|
import AuthService from "../services/AuthService";
|
||||||
Shield,
|
import { sendEmailOTP, sendPhoneOTP, verifyEmail, verifyPhone } from "../utils/api";
|
||||||
ChevronLeft,
|
import InteractiveBackground from "../components/Animation/Background";
|
||||||
Loader2,
|
|
||||||
CheckCircle,
|
|
||||||
XCircle,
|
|
||||||
Send,
|
|
||||||
Key
|
|
||||||
} from 'lucide-react';
|
|
||||||
import toast, { Toaster } from 'react-hot-toast';
|
|
||||||
import AuthService from '../services/AuthService';
|
|
||||||
import { sendEmailOTP, sendPhoneOTP, verifyEmail, verifyPhone } from '../utils/api';
|
|
||||||
|
|
||||||
export default function AccountVerificationPage() {
|
export default function AccountVerificationPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -26,8 +17,8 @@ export default function AccountVerificationPage() {
|
|||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
const [emailCode, setEmailCode] = useState('');
|
const [emailCode, setEmailCode] = useState("");
|
||||||
const [phoneCode, setPhoneCode] = useState('');
|
const [phoneCode, setPhoneCode] = useState("");
|
||||||
const [sendingEmailOTP, setSendingEmailOTP] = useState(false);
|
const [sendingEmailOTP, setSendingEmailOTP] = useState(false);
|
||||||
const [sendingPhoneOTP, setSendingPhoneOTP] = useState(false);
|
const [sendingPhoneOTP, setSendingPhoneOTP] = useState(false);
|
||||||
const [verifyingEmail, setVerifyingEmail] = useState(false);
|
const [verifyingEmail, setVerifyingEmail] = useState(false);
|
||||||
@ -43,7 +34,7 @@ export default function AccountVerificationPage() {
|
|||||||
setUser(authUser);
|
setUser(authUser);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
} else {
|
} else {
|
||||||
router.push('/login');
|
router.push("/login");
|
||||||
}
|
}
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
@ -51,10 +42,10 @@ export default function AccountVerificationPage() {
|
|||||||
setSendingEmailOTP(true);
|
setSendingEmailOTP(true);
|
||||||
try {
|
try {
|
||||||
await sendEmailOTP();
|
await sendEmailOTP();
|
||||||
toast.success(t('accountVerification.emailOtpSent'));
|
toast.success(t("accountVerification.emailOtpSent"));
|
||||||
setShowEmailInput(true);
|
setShowEmailInput(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err.message || t('accountVerification.otpSendFailed'));
|
toast.error(err.message || t("accountVerification.otpSendFailed"));
|
||||||
} finally {
|
} finally {
|
||||||
setSendingEmailOTP(false);
|
setSendingEmailOTP(false);
|
||||||
}
|
}
|
||||||
@ -62,7 +53,7 @@ export default function AccountVerificationPage() {
|
|||||||
|
|
||||||
const handleVerifyEmail = async () => {
|
const handleVerifyEmail = async () => {
|
||||||
if (!emailCode.trim()) {
|
if (!emailCode.trim()) {
|
||||||
toast.error(t('accountVerification.otpRequired'));
|
toast.error(t("accountVerification.otpRequired"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setVerifyingEmail(true);
|
setVerifyingEmail(true);
|
||||||
@ -70,9 +61,9 @@ export default function AccountVerificationPage() {
|
|||||||
await verifyEmail(emailCode.trim());
|
await verifyEmail(emailCode.trim());
|
||||||
setEmailVerified(true);
|
setEmailVerified(true);
|
||||||
setShowEmailInput(false);
|
setShowEmailInput(false);
|
||||||
toast.success(t('accountVerification.emailVerifiedSuccess'));
|
toast.success(t("accountVerification.emailVerifiedSuccess"));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err.message || t('accountVerification.otpInvalid'));
|
toast.error(err.message || t("accountVerification.otpInvalid"));
|
||||||
} finally {
|
} finally {
|
||||||
setVerifyingEmail(false);
|
setVerifyingEmail(false);
|
||||||
}
|
}
|
||||||
@ -82,10 +73,10 @@ export default function AccountVerificationPage() {
|
|||||||
setSendingPhoneOTP(true);
|
setSendingPhoneOTP(true);
|
||||||
try {
|
try {
|
||||||
await sendPhoneOTP();
|
await sendPhoneOTP();
|
||||||
toast.success(t('accountVerification.phoneOtpSent'));
|
toast.success(t("accountVerification.phoneOtpSent"));
|
||||||
setShowPhoneInput(true);
|
setShowPhoneInput(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err.message || t('accountVerification.otpSendFailed'));
|
toast.error(err.message || t("accountVerification.otpSendFailed"));
|
||||||
} finally {
|
} finally {
|
||||||
setSendingPhoneOTP(false);
|
setSendingPhoneOTP(false);
|
||||||
}
|
}
|
||||||
@ -93,7 +84,7 @@ export default function AccountVerificationPage() {
|
|||||||
|
|
||||||
const handleVerifyPhone = async () => {
|
const handleVerifyPhone = async () => {
|
||||||
if (!phoneCode.trim()) {
|
if (!phoneCode.trim()) {
|
||||||
toast.error(t('accountVerification.otpRequired'));
|
toast.error(t("accountVerification.otpRequired"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setVerifyingPhone(true);
|
setVerifyingPhone(true);
|
||||||
@ -101,74 +92,56 @@ export default function AccountVerificationPage() {
|
|||||||
await verifyPhone(phoneCode.trim());
|
await verifyPhone(phoneCode.trim());
|
||||||
setPhoneVerified(true);
|
setPhoneVerified(true);
|
||||||
setShowPhoneInput(false);
|
setShowPhoneInput(false);
|
||||||
toast.success(t('accountVerification.phoneVerifiedSuccess'));
|
toast.success(t("accountVerification.phoneVerifiedSuccess"));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err.message || t('accountVerification.otpInvalid'));
|
toast.error(err.message || t("accountVerification.otpInvalid"));
|
||||||
} finally {
|
} finally {
|
||||||
setVerifyingPhone(false);
|
setVerifyingPhone(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const containerVariants = {
|
const containerVariants = {
|
||||||
hidden: { opacity: 0 },
|
hidden: { opacity: 0 },
|
||||||
visible: {
|
visible: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
transition: { staggerChildren: 0.1 }
|
transition: { staggerChildren: 0.1 },
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const itemVariants = {
|
const itemVariants = {
|
||||||
hidden: { opacity: 0, y: 20 },
|
hidden: { opacity: 0, y: 20 },
|
||||||
visible: { opacity: 1, y: 0 }
|
visible: { opacity: 1, y: 0 },
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-900 py-8" dir="rtl">
|
<div className="min-h-screen py-8" dir="rtl">
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
<InteractiveBackground />
|
||||||
<div className="container mx-auto px-4 max-w-2xl">
|
<div className="relative z-10 container mx-auto px-4 max-w-2xl">
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="flex items-center gap-3 mb-8">
|
||||||
initial={{ opacity: 0, y: -20 }}
|
<Link href="/settings" className="p-2 rounded-xl hover:bg-gray-200 transition-colors text-gray-600">
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="flex items-center gap-3 mb-8"
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href="/settings"
|
|
||||||
className="p-2 rounded-xl hover:bg-gray-200 transition-colors text-gray-600"
|
|
||||||
>
|
|
||||||
<ChevronLeft className="w-5 h-5" />
|
<ChevronLeft className="w-5 h-5" />
|
||||||
</Link>
|
</Link>
|
||||||
<h1 className="text-2xl font-bold text-gray-900">{t('accountVerification.title')}</h1>
|
<h1 className="text-2xl font-bold text-gray-900">{t("accountVerification.title")}</h1>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div variants={containerVariants} initial="hidden" animate="visible" className="space-y-6">
|
||||||
variants={containerVariants}
|
|
||||||
initial="hidden"
|
|
||||||
animate="visible"
|
|
||||||
className="space-y-6"
|
|
||||||
>
|
|
||||||
<motion.div variants={itemVariants} className="bg-white rounded-2xl shadow-sm overflow-hidden">
|
<motion.div variants={itemVariants} className="bg-white rounded-2xl shadow-sm overflow-hidden">
|
||||||
<div className="px-6 py-4 border-b border-gray-100 flex items-center gap-3">
|
<div className="px-6 py-4 border-b border-gray-100 flex items-center gap-3">
|
||||||
<Mail className="w-5 h-5 text-amber-600" />
|
<Mail className="w-5 h-5 text-amber-600" />
|
||||||
<h2 className="text-lg font-semibold text-gray-800">{t('accountVerification.emailSection')}</h2>
|
<h2 className="text-lg font-semibold text-gray-800">{t("accountVerification.emailSection")}</h2>
|
||||||
{emailVerified && (
|
{emailVerified && (
|
||||||
<span className="flex items-center gap-1 text-xs text-green-600 bg-green-50 px-2 py-0.5 rounded-full">
|
<span className="flex items-center gap-1 text-xs text-green-600 bg-green-50 px-2 py-0.5 rounded-full">
|
||||||
<CheckCircle className="w-3 h-3" />
|
<CheckCircle className="w-3 h-3" />
|
||||||
{t('accountVerification.verified')}
|
{t("accountVerification.verified")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600">{user?.email || t('accountVerification.emailNotRegistered')}</p>
|
<p className="text-sm text-gray-600">{user?.email || t("accountVerification.emailNotRegistered")}</p>
|
||||||
<p className="text-xs text-gray-400 mt-1">
|
<p className="text-xs text-gray-400 mt-1">{emailVerified ? t("accountVerification.emailVerifiedDesc") : t("accountVerification.emailVerifyDesc")}</p>
|
||||||
{emailVerified
|
|
||||||
? t('accountVerification.emailVerifiedDesc')
|
|
||||||
: t('accountVerification.emailVerifyDesc')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
{!emailVerified && !showEmailInput && (
|
{!emailVerified && !showEmailInput && (
|
||||||
<button
|
<button
|
||||||
@ -176,29 +149,21 @@ export default function AccountVerificationPage() {
|
|||||||
disabled={sendingEmailOTP}
|
disabled={sendingEmailOTP}
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-amber-500 text-white rounded-xl hover:bg-amber-600 transition-colors text-sm disabled:opacity-50"
|
className="flex items-center gap-2 px-4 py-2 bg-amber-500 text-white rounded-xl hover:bg-amber-600 transition-colors text-sm disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{sendingEmailOTP ? (
|
{sendingEmailOTP ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />}
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
{sendingEmailOTP ? t("accountVerification.sending") : t("accountVerification.sendOtp")}
|
||||||
) : (
|
|
||||||
<Send className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
{sendingEmailOTP ? t('accountVerification.sending') : t('accountVerification.sendOtp')}
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showEmailInput && (
|
{showEmailInput && (
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} className="space-y-3">
|
||||||
initial={{ opacity: 0, height: 0 }}
|
<p className="text-xs text-gray-500">{t("accountVerification.enter6DigitCode")}</p>
|
||||||
animate={{ opacity: 1, height: 'auto' }}
|
|
||||||
className="space-y-3"
|
|
||||||
>
|
|
||||||
<p className="text-xs text-gray-500">{t('accountVerification.enter6DigitCode')}</p>
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={emailCode}
|
value={emailCode}
|
||||||
onChange={(e) => setEmailCode(e.target.value)}
|
onChange={(e) => setEmailCode(e.target.value)}
|
||||||
placeholder={t('accountVerification.otpPlaceholder')}
|
placeholder={t("accountVerification.otpPlaceholder")}
|
||||||
maxLength={6}
|
maxLength={6}
|
||||||
className="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none text-center text-lg tracking-widest"
|
className="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none text-center text-lg tracking-widest"
|
||||||
/>
|
/>
|
||||||
@ -207,20 +172,12 @@ export default function AccountVerificationPage() {
|
|||||||
disabled={verifyingEmail}
|
disabled={verifyingEmail}
|
||||||
className="px-6 py-3 bg-green-500 text-white rounded-xl hover:bg-green-600 transition-colors text-sm font-medium disabled:opacity-50 flex items-center gap-2"
|
className="px-6 py-3 bg-green-500 text-white rounded-xl hover:bg-green-600 transition-colors text-sm font-medium disabled:opacity-50 flex items-center gap-2"
|
||||||
>
|
>
|
||||||
{verifyingEmail ? (
|
{verifyingEmail ? <Loader2 className="w-4 h-4 animate-spin" /> : <Key className="w-4 h-4" />}
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
{t("accountVerification.verify")}
|
||||||
) : (
|
|
||||||
<Key className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
{t('accountVerification.verify')}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button onClick={handleSendEmailOTP} disabled={sendingEmailOTP} className="text-xs text-amber-600 hover:text-amber-700">
|
||||||
onClick={handleSendEmailOTP}
|
{t("accountVerification.resendCode")}
|
||||||
disabled={sendingEmailOTP}
|
|
||||||
className="text-xs text-amber-600 hover:text-amber-700"
|
|
||||||
>
|
|
||||||
{t('accountVerification.resendCode')}
|
|
||||||
</button>
|
</button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
@ -228,7 +185,7 @@ export default function AccountVerificationPage() {
|
|||||||
{emailVerified && (
|
{emailVerified && (
|
||||||
<div className="flex items-center gap-2 text-green-600 bg-green-50 px-4 py-3 rounded-xl">
|
<div className="flex items-center gap-2 text-green-600 bg-green-50 px-4 py-3 rounded-xl">
|
||||||
<CheckCircle className="w-5 h-5" />
|
<CheckCircle className="w-5 h-5" />
|
||||||
<span className="text-sm font-medium">{t('accountVerification.emailVerifiedSuccess')}</span>
|
<span className="text-sm font-medium">{t("accountVerification.emailVerifiedSuccess")}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -237,23 +194,19 @@ export default function AccountVerificationPage() {
|
|||||||
<motion.div variants={itemVariants} className="bg-white rounded-2xl shadow-sm overflow-hidden">
|
<motion.div variants={itemVariants} className="bg-white rounded-2xl shadow-sm overflow-hidden">
|
||||||
<div className="px-6 py-4 border-b border-gray-100 flex items-center gap-3">
|
<div className="px-6 py-4 border-b border-gray-100 flex items-center gap-3">
|
||||||
<Phone className="w-5 h-5 text-amber-600" />
|
<Phone className="w-5 h-5 text-amber-600" />
|
||||||
<h2 className="text-lg font-semibold text-gray-800">{t('accountVerification.phoneSection')}</h2>
|
<h2 className="text-lg font-semibold text-gray-800">{t("accountVerification.phoneSection")}</h2>
|
||||||
{phoneVerified && (
|
{phoneVerified && (
|
||||||
<span className="flex items-center gap-1 text-xs text-green-600 bg-green-50 px-2 py-0.5 rounded-full">
|
<span className="flex items-center gap-1 text-xs text-green-600 bg-green-50 px-2 py-0.5 rounded-full">
|
||||||
<CheckCircle className="w-3 h-3" />
|
<CheckCircle className="w-3 h-3" />
|
||||||
{t('accountVerification.verified')}
|
{t("accountVerification.verified")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600">{user?.phone || t('accountVerification.phoneNotRegistered')}</p>
|
<p className="text-sm text-gray-600">{user?.phone || t("accountVerification.phoneNotRegistered")}</p>
|
||||||
<p className="text-xs text-gray-400 mt-1">
|
<p className="text-xs text-gray-400 mt-1">{phoneVerified ? t("accountVerification.phoneVerifiedDesc") : t("accountVerification.phoneVerifyDesc")}</p>
|
||||||
{phoneVerified
|
|
||||||
? t('accountVerification.phoneVerifiedDesc')
|
|
||||||
: t('accountVerification.phoneVerifyDesc')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
{!phoneVerified && !showPhoneInput && (
|
{!phoneVerified && !showPhoneInput && (
|
||||||
<button
|
<button
|
||||||
@ -261,29 +214,21 @@ export default function AccountVerificationPage() {
|
|||||||
disabled={sendingPhoneOTP}
|
disabled={sendingPhoneOTP}
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-amber-500 text-white rounded-xl hover:bg-amber-600 transition-colors text-sm disabled:opacity-50"
|
className="flex items-center gap-2 px-4 py-2 bg-amber-500 text-white rounded-xl hover:bg-amber-600 transition-colors text-sm disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{sendingPhoneOTP ? (
|
{sendingPhoneOTP ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />}
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
{sendingPhoneOTP ? t("accountVerification.sending") : t("accountVerification.sendOtp")}
|
||||||
) : (
|
|
||||||
<Send className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
{sendingPhoneOTP ? t('accountVerification.sending') : t('accountVerification.sendOtp')}
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showPhoneInput && (
|
{showPhoneInput && (
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} className="space-y-3">
|
||||||
initial={{ opacity: 0, height: 0 }}
|
<p className="text-xs text-gray-500">{t("accountVerification.enter6DigitCodePhone")}</p>
|
||||||
animate={{ opacity: 1, height: 'auto' }}
|
|
||||||
className="space-y-3"
|
|
||||||
>
|
|
||||||
<p className="text-xs text-gray-500">{t('accountVerification.enter6DigitCodePhone')}</p>
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={phoneCode}
|
value={phoneCode}
|
||||||
onChange={(e) => setPhoneCode(e.target.value)}
|
onChange={(e) => setPhoneCode(e.target.value)}
|
||||||
placeholder={t('accountVerification.otpPlaceholder')}
|
placeholder={t("accountVerification.otpPlaceholder")}
|
||||||
maxLength={6}
|
maxLength={6}
|
||||||
className="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none text-center text-lg tracking-widest"
|
className="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none text-center text-lg tracking-widest"
|
||||||
/>
|
/>
|
||||||
@ -292,20 +237,12 @@ export default function AccountVerificationPage() {
|
|||||||
disabled={verifyingPhone}
|
disabled={verifyingPhone}
|
||||||
className="px-6 py-3 bg-green-500 text-white rounded-xl hover:bg-green-600 transition-colors text-sm font-medium disabled:opacity-50 flex items-center gap-2"
|
className="px-6 py-3 bg-green-500 text-white rounded-xl hover:bg-green-600 transition-colors text-sm font-medium disabled:opacity-50 flex items-center gap-2"
|
||||||
>
|
>
|
||||||
{verifyingPhone ? (
|
{verifyingPhone ? <Loader2 className="w-4 h-4 animate-spin" /> : <Key className="w-4 h-4" />}
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
{t("accountVerification.verify")}
|
||||||
) : (
|
|
||||||
<Key className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
{t('accountVerification.verify')}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button onClick={handleSendPhoneOTP} disabled={sendingPhoneOTP} className="text-xs text-amber-600 hover:text-amber-700">
|
||||||
onClick={handleSendPhoneOTP}
|
{t("accountVerification.resendCode")}
|
||||||
disabled={sendingPhoneOTP}
|
|
||||||
className="text-xs text-amber-600 hover:text-amber-700"
|
|
||||||
>
|
|
||||||
{t('accountVerification.resendCode')}
|
|
||||||
</button>
|
</button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
@ -313,7 +250,7 @@ export default function AccountVerificationPage() {
|
|||||||
{phoneVerified && (
|
{phoneVerified && (
|
||||||
<div className="flex items-center gap-2 text-green-600 bg-green-50 px-4 py-3 rounded-xl">
|
<div className="flex items-center gap-2 text-green-600 bg-green-50 px-4 py-3 rounded-xl">
|
||||||
<CheckCircle className="w-5 h-5" />
|
<CheckCircle className="w-5 h-5" />
|
||||||
<span className="text-sm font-medium">{t('accountVerification.phoneVerifiedSuccess')}</span>
|
<span className="text-sm font-medium">{t("accountVerification.phoneVerifiedSuccess")}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -322,20 +259,20 @@ export default function AccountVerificationPage() {
|
|||||||
<motion.div variants={itemVariants} className="bg-gradient-to-br from-amber-500 to-amber-600 rounded-2xl p-6 text-white">
|
<motion.div variants={itemVariants} className="bg-gradient-to-br from-amber-500 to-amber-600 rounded-2xl p-6 text-white">
|
||||||
<div className="flex items-center gap-3 mb-3">
|
<div className="flex items-center gap-3 mb-3">
|
||||||
<Shield className="w-6 h-6" />
|
<Shield className="w-6 h-6" />
|
||||||
<h3 className="text-lg font-semibold">{t('accountVerification.whyVerify')}</h3>
|
<h3 className="text-lg font-semibold">{t("accountVerification.whyVerify")}</h3>
|
||||||
</div>
|
</div>
|
||||||
<ul className="space-y-2 text-sm text-amber-50">
|
<ul className="space-y-2 text-sm " style={{ color: "#0f172a", fontWeight: "400" }}>
|
||||||
<li className="flex items-center gap-2">
|
<li className="flex items-center gap-2">
|
||||||
<CheckCircle className="w-4 h-4 flex-shrink-0" />
|
<CheckCircle className="w-4 h-4 flex-shrink-0" />
|
||||||
{t('accountVerification.reason1')}
|
{t("accountVerification.reason1")}
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-center gap-2">
|
<li className="flex items-center gap-2">
|
||||||
<CheckCircle className="w-4 h-4 flex-shrink-0" />
|
<CheckCircle className="w-4 h-4 flex-shrink-0" />
|
||||||
{t('accountVerification.reason2')}
|
{t("accountVerification.reason2")}
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-center gap-2">
|
<li className="flex items-center gap-2">
|
||||||
<CheckCircle className="w-4 h-4 flex-shrink-0" />
|
<CheckCircle className="w-4 h-4 flex-shrink-0" />
|
||||||
{t('accountVerification.reason3')}
|
{t("accountVerification.reason3")}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { ShieldAlert, LogOut, MessageSquare, Send, Loader2 } from "lucide-react"
|
|||||||
import toast, { Toaster } from "react-hot-toast";
|
import toast, { Toaster } from "react-hot-toast";
|
||||||
import AuthService from "../services/AuthService";
|
import AuthService from "../services/AuthService";
|
||||||
import { sendGeneralReport } from "../utils/api";
|
import { sendGeneralReport } from "../utils/api";
|
||||||
|
import InteractiveBackground from "../components/Animation/Background";
|
||||||
|
|
||||||
export default function BlockedPage() {
|
export default function BlockedPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -49,10 +50,10 @@ export default function BlockedPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-900 flex items-center justify-center p-4" dir="rtl">
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4" dir="rtl">
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
<InteractiveBackground />
|
||||||
<motion.div initial={{ opacity: 0, y: 24 }} animate={{ opacity: 1, y: 0 }} className="w-full max-w-5xl">
|
<motion.div initial={{ opacity: 0, y: 24 }} animate={{ opacity: 1, y: 0 }} className="w-full max-w-5xl relative z-10">
|
||||||
<div className="text-center mb-10">
|
<div className="text-center mb-10">
|
||||||
<motion.div initial={{ scale: 0.9 }} animate={{ scale: 1 }} className="w-24 h-24 bg-red-100 rounded-3xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-red-100">
|
<motion.div initial={{ scale: 0.9 }} animate={{ scale: 1 }} className="w-24 h-24 bg-red-100 rounded-3xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-red-100">
|
||||||
<ShieldAlert className="w-12 h-12 text-red-600" />
|
<ShieldAlert className="w-12 h-12 text-red-600" />
|
||||||
@ -61,7 +62,7 @@ export default function BlockedPage() {
|
|||||||
<p className="text-gray-600 text-lg max-w-2xl mx-auto">{t("blocked.description")}</p>
|
<p className="text-gray-600 text-lg max-w-2xl mx-auto">{t("blocked.description")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-6">
|
<div className="grid md:grid-cols-2 gap-6 relative z-10 ">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, x: -24 }}
|
initial={{ opacity: 0, x: -24 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
|||||||
@ -144,13 +144,7 @@ export default function BookedPropertiesPage() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center" dir="rtl">
|
|
||||||
<Loader2 className="w-12 h-12 text-amber-500 animate-spin" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">
|
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">
|
||||||
|
|||||||
@ -1,18 +1,19 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from "framer-motion";
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from "next/navigation";
|
||||||
import toast, { Toaster } from 'react-hot-toast';
|
import toast, { Toaster } from "react-hot-toast";
|
||||||
import { Lock, Eye, EyeOff, ArrowLeft, Shield } from 'lucide-react';
|
import { Lock, Eye, EyeOff, ArrowLeft, Shield } from "lucide-react";
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
import { changePassword } from '../utils/api';
|
import { changePassword } from "../utils/api";
|
||||||
import AuthService from '../services/AuthService';
|
import AuthService from "../services/AuthService";
|
||||||
|
import InteractiveBackground from "../components/Animation/Background";
|
||||||
|
|
||||||
export default function ChangePasswordPage() {
|
export default function ChangePasswordPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [form, setForm] = useState({ oldPassword: '', newPassword: '', confirmPassword: '' });
|
const [form, setForm] = useState({ oldPassword: "", newPassword: "", confirmPassword: "" });
|
||||||
const [show, setShow] = useState({ old: false, new: false, confirm: false });
|
const [show, setShow] = useState({ old: false, new: false, confirm: false });
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
@ -24,162 +25,124 @@ export default function ChangePasswordPage() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!AuthService.isAuthenticated()) {
|
if (!AuthService.isAuthenticated()) {
|
||||||
toast.error(t('changePassword.loginRequired'));
|
toast.error(t("changePassword.loginRequired"));
|
||||||
router.push('/login');
|
router.push("/login");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (form.newPassword !== form.confirmPassword) {
|
if (form.newPassword !== form.confirmPassword) {
|
||||||
toast.error(t('changePassword.passwordsDoNotMatch'));
|
toast.error(t("changePassword.passwordsDoNotMatch"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (form.newPassword.length < 6) {
|
if (form.newPassword.length < 6) {
|
||||||
toast.error(t('validation.passwordMinLength'));
|
toast.error(t("validation.passwordMinLength"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
await changePassword(form.oldPassword, form.newPassword);
|
await changePassword(form.oldPassword, form.newPassword);
|
||||||
toast.success(t('changePassword.changeSuccess'));
|
toast.success(t("changePassword.changeSuccess"));
|
||||||
setTimeout(() => router.push('/profile'), 1200);
|
setTimeout(() => router.push("/profile"), 1200);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err?.message || t('changePassword.changeFailed'));
|
toast.error(err?.message || t("changePassword.changeFailed"));
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const inputClass = "w-full pr-12 pl-4 py-3 bg-gray-50 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400 transition-all";
|
const inputClass =
|
||||||
|
"w-full pr-12 pl-4 py-3 bg-gray-50 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400 transition-all";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-900 flex items-center justify-center p-4">
|
<div className="min-h-screen relative z-10 flex items-center justify-center p-4" style={{ backgroundColor: "#0f172a" }}>
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
<InteractiveBackground />
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, y: 30 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }} className="w-full max-w-md relative z-10 ">
|
||||||
initial={{ opacity: 0, y: 30 }}
|
<motion.div initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} className="mb-6 relative z-10">
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<button onClick={() => router.push("/profile")} className="flex items-center relative z-10 gap-2 text-gray-600 hover:text-amber-600 transition-colors">
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
className="w-full max-w-md"
|
|
||||||
>
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, x: -20 }}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
className="mb-6"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
onClick={() => router.push('/profile')}
|
|
||||||
className="flex items-center gap-2 text-gray-600 hover:text-amber-600 transition-colors"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="w-5 h-5" />
|
<ArrowLeft className="w-5 h-5" />
|
||||||
<span>{t('changePassword.backToProfile')}</span>
|
<span>{t("changePassword.backToProfile")}</span>
|
||||||
</button>
|
</button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<div className="bg-white rounded-3xl shadow-xl overflow-hidden">
|
<div className=" rounded-3xl shadow-xl overflow-hidden" style={{ backgroundColor: "#1e293b" }}>
|
||||||
<div className="bg-gradient-to-l from-amber-500 to-amber-600 p-8 text-center">
|
<div className="bg-gradient-to-l from-amber-500 to-amber-600 p-8 text-center">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ scale: 0 }}
|
initial={{ scale: 0 }}
|
||||||
animate={{ scale: 1 }}
|
animate={{ scale: 1 }}
|
||||||
transition={{ type: 'spring', stiffness: 200 }}
|
transition={{ type: "spring", stiffness: 200 }}
|
||||||
className="w-16 h-16 bg-white/20 rounded-full flex items-center justify-center mx-auto mb-4"
|
className="w-16 h-16 bg-white/20 rounded-full flex items-center justify-center mx-auto mb-4"
|
||||||
>
|
>
|
||||||
<Shield className="w-8 h-8 text-white" />
|
<Shield className="w-8 h-8 text-white" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<motion.h1
|
<motion.h1 initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} className="text-3xl font-bold text-white mb-2">
|
||||||
initial={{ y: 20, opacity: 0 }}
|
{t("changePassword.title")}
|
||||||
animate={{ y: 0, opacity: 1 }}
|
|
||||||
className="text-3xl font-bold text-white mb-2"
|
|
||||||
>
|
|
||||||
{t('changePassword.title')}
|
|
||||||
</motion.h1>
|
</motion.h1>
|
||||||
<motion.p
|
<motion.p initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} transition={{ delay: 0.1 }} className="text-amber-100">
|
||||||
initial={{ y: 20, opacity: 0 }}
|
{t("changePassword.subtitle")}
|
||||||
animate={{ y: 0, opacity: 1 }}
|
|
||||||
transition={{ delay: 0.1 }}
|
|
||||||
className="text-amber-100"
|
|
||||||
>
|
|
||||||
{t('changePassword.subtitle')}
|
|
||||||
</motion.p>
|
</motion.p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="p-8 space-y-6">
|
<form onSubmit={handleSubmit} className="p-8 space-y-6 " style={{ backgroundColor: "#0f172a" }}>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">{t("changePassword.currentPasswordLabel")}</label>
|
||||||
{t('changePassword.currentPasswordLabel')}
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
<Lock className="w-5 h-5 text-gray-400" />
|
<Lock className="w-5 h-5 text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type={show.old ? 'text' : 'password'}
|
type={show.old ? "text" : "password"}
|
||||||
value={form.oldPassword}
|
value={form.oldPassword}
|
||||||
onChange={handleChange('oldPassword')}
|
onChange={handleChange("oldPassword")}
|
||||||
className={inputClass}
|
className={inputClass}
|
||||||
placeholder={t('changePassword.currentPasswordPlaceholder')}
|
placeholder={t("changePassword.currentPasswordPlaceholder")}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<button
|
<button type="button" onClick={() => toggleShow("old")} className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600">
|
||||||
type="button"
|
|
||||||
onClick={() => toggleShow('old')}
|
|
||||||
className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600"
|
|
||||||
>
|
|
||||||
{show.old ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
{show.old ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">{t("changePassword.newPasswordLabel")}</label>
|
||||||
{t('changePassword.newPasswordLabel')}
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
<Lock className="w-5 h-5 text-gray-400" />
|
<Lock className="w-5 h-5 text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type={show.new ? 'text' : 'password'}
|
type={show.new ? "text" : "password"}
|
||||||
value={form.newPassword}
|
value={form.newPassword}
|
||||||
onChange={handleChange('newPassword')}
|
onChange={handleChange("newPassword")}
|
||||||
className={inputClass}
|
className={inputClass}
|
||||||
placeholder={t('changePassword.newPasswordPlaceholder')}
|
placeholder={t("changePassword.newPasswordPlaceholder")}
|
||||||
required
|
required
|
||||||
minLength={6}
|
minLength={6}
|
||||||
/>
|
/>
|
||||||
<button
|
<button type="button" onClick={() => toggleShow("new")} className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600">
|
||||||
type="button"
|
|
||||||
onClick={() => toggleShow('new')}
|
|
||||||
className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600"
|
|
||||||
>
|
|
||||||
{show.new ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
{show.new ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">{t("changePassword.confirmNewPasswordLabel")}</label>
|
||||||
{t('changePassword.confirmNewPasswordLabel')}
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
<Lock className="w-5 h-5 text-gray-400" />
|
<Lock className="w-5 h-5 text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type={show.confirm ? 'text' : 'password'}
|
type={show.confirm ? "text" : "password"}
|
||||||
value={form.confirmPassword}
|
value={form.confirmPassword}
|
||||||
onChange={handleChange('confirmPassword')}
|
onChange={handleChange("confirmPassword")}
|
||||||
className={inputClass}
|
className={inputClass}
|
||||||
placeholder={t('changePassword.confirmPasswordPlaceholder')}
|
placeholder={t("changePassword.confirmPasswordPlaceholder")}
|
||||||
required
|
required
|
||||||
minLength={6}
|
minLength={6}
|
||||||
/>
|
/>
|
||||||
<button
|
<button type="button" onClick={() => toggleShow("confirm")} className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600">
|
||||||
type="button"
|
|
||||||
onClick={() => toggleShow('confirm')}
|
|
||||||
className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600"
|
|
||||||
>
|
|
||||||
{show.confirm ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
{show.confirm ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -195,10 +158,10 @@ export default function ChangePasswordPage() {
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||||
<span>{t('changePassword.saving')}</span>
|
<span>{t("changePassword.saving")}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
t('changePassword.changePassword')
|
t("changePassword.changePassword")
|
||||||
)}
|
)}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { usePathname } from "next/navigation";
|
|||||||
import { Home, Building, Calendar, CreditCard, Briefcase, BookOpen } from "lucide-react";
|
import { Home, Building, Calendar, CreditCard, Briefcase, BookOpen } from "lucide-react";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { label } from "framer-motion/client";
|
||||||
|
|
||||||
export default function BottomNav({ isOwner, isOwnerOrAgent }) {
|
export default function BottomNav({ isOwner, isOwnerOrAgent }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -19,8 +20,15 @@ export default function BottomNav({ isOwner, isOwnerOrAgent }) {
|
|||||||
const items = [
|
const items = [
|
||||||
{ href: "/", label: t("home"), icon: Home },
|
{ href: "/", label: t("home"), icon: Home },
|
||||||
{ href: "/properties", label: t("ourProperties"), icon: Building },
|
{ href: "/properties", label: t("ourProperties"), icon: Building },
|
||||||
...(isOwnerOrAgent ? [{ href: "/owner/properties", label: t("myProperties"), icon: Briefcase }] : []),
|
...(isOwnerOrAgent
|
||||||
{ href: bookingsHref, label: t("reservations"), icon: Calendar },
|
? [
|
||||||
|
{ href: "/owner/properties", label: t("myProperties"), icon: Briefcase },
|
||||||
|
{ href: "/owner/bookings", label: "عقاراتي المحجوزة", icon: Briefcase },
|
||||||
|
{ href: "/reservations", label: "حجوزاتي", icon: Calendar },
|
||||||
|
]
|
||||||
|
: [{ href: "/booked-properties", label: "حجوزاتي", icon: Briefcase }]),
|
||||||
|
{ href: bookingsHref, label: "طلبات الحجز", icon: Calendar },
|
||||||
|
|
||||||
{ href: "/payments", label: t("payments"), icon: CreditCard },
|
{ href: "/payments", label: t("payments"), icon: CreditCard },
|
||||||
...(isOwnerOrAgent ? [{ href: "/owner/account-book", label: t("accountBook"), icon: BookOpen }] : []),
|
...(isOwnerOrAgent ? [{ href: "/owner/account-book", label: t("accountBook"), icon: BookOpen }] : []),
|
||||||
];
|
];
|
||||||
@ -46,9 +54,7 @@ export default function BottomNav({ isOwner, isOwnerOrAgent }) {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Icon className="w-6 h-6" />
|
<Icon className="w-6 h-6" />
|
||||||
{it.badge > 0 && (
|
{it.badge > 0 && (
|
||||||
<div className="absolute -top-2 -right-2 bg-red-500 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center">
|
<div className="absolute -top-2 -right-2 bg-red-500 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center">{it.badge > 9 ? "9+" : it.badge}</div>
|
||||||
{it.badge > 9 ? "9+" : it.badge}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-xs text-center" aria-hidden>
|
<div className="mt-1 text-xs text-center" aria-hidden>
|
||||||
|
|||||||
@ -76,9 +76,9 @@ export default function Owner() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const licenseImage = isCompany ? idImages.license : null;
|
const licenseImage = isCompany ? idImages.license : null;
|
||||||
const res = await addOwner(payload, idImages.front, idImages.back);
|
const res = await addOwner(payload, idImages.front, idImages.back,licenseImage);
|
||||||
if (res.status === 200 || res.ok) {
|
if (res.status === 200 || res.ok) {
|
||||||
toast.success(res.message || t("register.accountCreated"), { duration: 4000 });
|
toast.success(res.message || t("register.accountCreated")||res.data[0].message , { duration: 4000 });
|
||||||
const loginRes = await loginWithEmail(formData.email, formData.password);
|
const loginRes = await loginWithEmail(formData.email, formData.password);
|
||||||
if (loginRes.status === 206) {
|
if (loginRes.status === 206) {
|
||||||
const otpToken = loginRes.data;
|
const otpToken = loginRes.data;
|
||||||
@ -150,15 +150,7 @@ export default function Owner() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<motion.div variants={fadeInUp} className="flex gap-3 pt-4">
|
<motion.div variants={fadeInUp} className="flex gap-3 pt-4">
|
||||||
{step === 1 ? (
|
{step === 1 ? ( <> <ButtomStepOne type={"owner"} /></>) : ( <> <ButtomStepTwo formData={formData} isLoading={isLoading} setStep={setStep} type={"owner"} /> </>)}
|
||||||
<>
|
|
||||||
<ButtomStepOne type={"owner"} />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ButtomStepTwo formData={formData} isLoading={isLoading} setStep={setStep} type={"owner"} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.form>
|
</motion.form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -18,7 +18,21 @@ const City = Object.freeze({
|
|||||||
QAMISHLI: 'القامشلي',
|
QAMISHLI: 'القامشلي',
|
||||||
RURAL_DAMASCUS: 'ريف دمشق',
|
RURAL_DAMASCUS: 'ريف دمشق',
|
||||||
});
|
});
|
||||||
|
export const CityEnum = Object.freeze({
|
||||||
|
DAMASCUS: 0,
|
||||||
|
ALEPPO: 1,
|
||||||
|
HOMS: 2,
|
||||||
|
LATAKIA: 3,
|
||||||
|
DARAA: 4,
|
||||||
|
TARTOUS: 5,
|
||||||
|
SUWEIDA: 6,
|
||||||
|
DEIR_EZZOR: 7,
|
||||||
|
RAQQA: 8,
|
||||||
|
IDLIB: 9,
|
||||||
|
HASAKAH: 10,
|
||||||
|
Qamishli: 11,
|
||||||
|
RURAL_DAMASCUS: 12,
|
||||||
|
});
|
||||||
// All cities as a flat array
|
// All cities as a flat array
|
||||||
const CitiesList = Object.freeze(Object.values(City));
|
const CitiesList = Object.freeze(Object.values(City));
|
||||||
|
|
||||||
@ -51,4 +65,6 @@ const CityTranslationKeys = Object.freeze({
|
|||||||
[City.RURAL_DAMASCUS]: 'city.ruralDamascus',
|
[City.RURAL_DAMASCUS]: 'city.ruralDamascus',
|
||||||
});
|
});
|
||||||
|
|
||||||
export { City, CitiesList, extractCity, CityTranslationKeys };
|
|
||||||
|
|
||||||
|
export { City, CitiesList, extractCity, CityTranslationKeys, };
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export default function FAQPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12" dir="rtl">
|
<div className="min-h-screen py-12" dir="rtl">
|
||||||
<div className="container mx-auto px-4 max-w-4xl">
|
<div className="container mx-auto px-4 max-w-4xl">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: -20 }}
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
|||||||
@ -1,14 +1,15 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from "next/navigation";
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from "framer-motion";
|
||||||
import Link from 'next/link';
|
import Link from "next/link";
|
||||||
import Image from 'next/image';
|
import Image from "next/image";
|
||||||
import { Heart, MapPin, Bed, Bath, Square, X, ImageIcon } from 'lucide-react';
|
import { Heart, MapPin, Bed, Bath, Square, X, ImageIcon } from "lucide-react";
|
||||||
import { useFavorites } from '@/app/contexts/FavoritesContext';
|
import { useFavorites } from "@/app/contexts/FavoritesContext";
|
||||||
import AuthService from '@/app/services/AuthService';
|
import AuthService from "@/app/services/AuthService";
|
||||||
|
import Loading from "../loading";
|
||||||
|
|
||||||
export default function FavoritesPage() {
|
export default function FavoritesPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -25,38 +26,28 @@ export default function FavoritesPage() {
|
|||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
const formatCurrency = (amount) => {
|
const formatCurrency = (amount) => {
|
||||||
return amount?.toLocaleString() + ' ' + t('currency-syp-suffix');
|
return amount?.toLocaleString() + " " + t("currency-syp-suffix");
|
||||||
};
|
};
|
||||||
|
|
||||||
if (favoritesLoading && favorites.length === 0) {
|
if (favoritesLoading && favorites.length === 0) {
|
||||||
return (
|
return <Loading />;
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="w-16 h-16 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
|
||||||
<p className="text-gray-600">{t('loading')}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 py-8">
|
<div className="min-h-screen bg-gray-50 py-8">
|
||||||
<div className="container mx-auto px-4 max-w-6xl">
|
<div className="container mx-auto px-4 max-w-6xl">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t('favorites')}</h1>
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t("favorites")}</h1>
|
||||||
<p className="text-gray-600">{t('saved-properties')}</p>
|
<p className="text-gray-600">{t("saved-properties")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{favorites.length === 0 ? (
|
{favorites.length === 0 ? (
|
||||||
<div className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
|
<div className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
|
||||||
<Heart className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
<Heart className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||||
<h3 className="text-xl font-bold text-gray-700 mb-2">{t('no-favorites')}</h3>
|
<h3 className="text-xl font-bold text-gray-700 mb-2">{t("no-favorites")}</h3>
|
||||||
<p className="text-gray-500 mb-6">{t('add-favorites-hint')}</p>
|
<p className="text-gray-500 mb-6">{t("add-favorites-hint")}</p>
|
||||||
<Link
|
<Link href="/properties" className="inline-flex items-center gap-2 bg-amber-500 text-white px-6 py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors">
|
||||||
href="/properties"
|
{t("browse-properties")}
|
||||||
className="inline-flex items-center gap-2 bg-amber-500 text-white px-6 py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors"
|
|
||||||
>
|
|
||||||
{t('browse-properties')}
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -70,13 +61,7 @@ export default function FavoritesPage() {
|
|||||||
>
|
>
|
||||||
<div className="relative h-48 bg-gray-100">
|
<div className="relative h-48 bg-gray-100">
|
||||||
{property.images && property.images[0] ? (
|
{property.images && property.images[0] ? (
|
||||||
<Image
|
<Image src={property.images[0]} alt={property.title} fill className="object-cover" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" />
|
||||||
src={property.images[0]}
|
|
||||||
alt={property.title}
|
|
||||||
fill
|
|
||||||
className="object-cover"
|
|
||||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full flex items-center justify-center">
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
<ImageIcon className="w-12 h-12 text-gray-400" />
|
<ImageIcon className="w-12 h-12 text-gray-400" />
|
||||||
@ -95,7 +80,7 @@ export default function FavoritesPage() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-lg text-xs font-medium">
|
<span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-lg text-xs font-medium">
|
||||||
{property.type === 'apartment' ? t('buildingType.apartment') : property.type === 'villa' ? t('buildingType.villa') : t('buildingType.house')}
|
{property.type === "apartment" ? t("buildingType.apartment") : property.type === "villa" ? t("buildingType.villa") : t("buildingType.house")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-bold text-gray-900 mb-1 line-clamp-1">{property.title}</h3>
|
<h3 className="font-bold text-gray-900 mb-1 line-clamp-1">{property.title}</h3>
|
||||||
@ -108,7 +93,7 @@ export default function FavoritesPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<div className="text-xl font-bold text-gray-900">{formatCurrency(property.price)}</div>
|
<div className="text-xl font-bold text-gray-900">{formatCurrency(property.price)}</div>
|
||||||
<div className="text-xs text-gray-500">/{property.priceUnit === 'daily' ? t('day') : t('month')}</div>
|
<div className="text-xs text-gray-500">/{property.priceUnit === "daily" ? t("day") : t("month")}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -129,11 +114,8 @@ export default function FavoritesPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link
|
<Link href={`/property/${property.id}`} className="block w-full bg-amber-500 text-white py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors text-center">
|
||||||
href={`/property/${property.id}`}
|
{t("viewDetails")}
|
||||||
className="block w-full bg-amber-500 text-white py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors text-center"
|
|
||||||
>
|
|
||||||
{t('viewDetails')}
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import toast, { Toaster } from 'react-hot-toast';
|
|||||||
import { Mail, Key, Lock, CheckCircle, ArrowLeft, RefreshCw } from 'lucide-react';
|
import { Mail, Key, Lock, CheckCircle, ArrowLeft, RefreshCw } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { requestForgetPasswordOtp, verifyForgetPasswordOtp } from '../utils/api';
|
import { requestForgetPasswordOtp, verifyForgetPasswordOtp } from '../utils/api';
|
||||||
|
import InteractiveBackground from '../components/Animation/Background';
|
||||||
|
|
||||||
export default function ForgotPasswordPage() {
|
export default function ForgotPasswordPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -68,6 +69,7 @@ export default function ForgotPasswordPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center p-4 relative overflow-hidden">
|
<div className="min-h-screen flex items-center justify-center p-4 relative overflow-hidden">
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
<InteractiveBackground/>
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
<div className="absolute -top-40 -right-40 w-80 h-80 bg-amber-400 rounded-full opacity-20 blur-3xl animate-pulse"></div>
|
<div className="absolute -top-40 -right-40 w-80 h-80 bg-amber-400 rounded-full opacity-20 blur-3xl animate-pulse"></div>
|
||||||
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-orange-400 rounded-full opacity-20 blur-3xl animate-pulse delay-1000"></div>
|
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-orange-400 rounded-full opacity-20 blur-3xl animate-pulse delay-1000"></div>
|
||||||
|
|||||||
4198
app/i18n/config.js
4198
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 { getCustomerRatings } from '../utils/ratings';
|
||||||
import AuthService from '../services/AuthService';
|
import AuthService from '../services/AuthService';
|
||||||
import StarRating from '../components/ratings/StarRating';
|
import StarRating from '../components/ratings/StarRating';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
const RATING_FIELDS = [
|
const RATING_FIELDS = [
|
||||||
{ key: 'cleanRating', label: 'ratings.cleanliness' },
|
{ key: 'cleanRating', label: 'ratings.cleanliness' },
|
||||||
@ -99,13 +100,7 @@ export default function MyRatesPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center" dir="rtl">
|
|
||||||
<Loader2 className="w-12 h-12 text-amber-500 animate-spin" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const overall = calcOverall(ratings);
|
const overall = calcOverall(ratings);
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from "next/navigation";
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import { Bell, CheckCircle, XCircle, Calendar, MessageCircle, CheckCheck, Loader2 } from 'lucide-react';
|
import { Bell, CheckCircle, XCircle, Calendar, MessageCircle, CheckCheck, Loader2 } from "lucide-react";
|
||||||
import AuthService from '@/app/services/AuthService';
|
import AuthService from "@/app/services/AuthService";
|
||||||
import { getUserNotifications } from '@/app/utils/api';
|
import { getUserNotifications } from "@/app/utils/api";
|
||||||
|
import Loading from "../loading";
|
||||||
|
|
||||||
export default function NotificationsPage() {
|
export default function NotificationsPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -18,7 +19,7 @@ export default function NotificationsPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!AuthService.isAuthenticated()) {
|
if (!AuthService.isAuthenticated()) {
|
||||||
router.push('/login');
|
router.push("/login");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fetchNotifications();
|
fetchNotifications();
|
||||||
@ -33,36 +34,27 @@ export default function NotificationsPage() {
|
|||||||
setNotifications(items);
|
setNotifications(items);
|
||||||
setUnreadCount(items.length);
|
setUnreadCount(items.length);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching notifications:', err);
|
console.error("Error fetching notifications:", err);
|
||||||
setError(err.message || t('fetch-notifications-failed'));
|
setError(err.message || t("fetch-notifications-failed"));
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const markAsRead = (id) => {
|
const markAsRead = (id) => {
|
||||||
setNotifications(prev =>
|
setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, read: true } : n)));
|
||||||
prev.map(n => (n.id === id ? { ...n, read: true } : n))
|
setUnreadCount((prev) => Math.max(0, prev - 1));
|
||||||
);
|
|
||||||
setUnreadCount(prev => Math.max(0, prev - 1));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const markAllAsRead = () => {
|
const markAllAsRead = () => {
|
||||||
setNotifications(prev => prev.map(n => ({ ...n, read: true })));
|
setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
|
||||||
setUnreadCount(0);
|
setUnreadCount(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
<div>
|
||||||
<motion.div
|
<Loading />
|
||||||
initial={{ opacity: 0, scale: 0.9 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
className="text-center"
|
|
||||||
>
|
|
||||||
<Loader2 className="w-12 h-12 text-amber-500 mx-auto mb-4 animate-spin" />
|
|
||||||
<p className="text-gray-600">{t('loading')}</p>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -70,13 +62,9 @@ export default function NotificationsPage() {
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="text-center">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="text-center"
|
|
||||||
>
|
|
||||||
<XCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
|
<XCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
|
||||||
<h3 className="text-xl font-bold text-gray-700 mb-2">{t('loading-error')}</h3>
|
<h3 className="text-xl font-bold text-gray-700 mb-2">{t("loading-error")}</h3>
|
||||||
<p className="text-gray-500 mb-4">{error}</p>
|
<p className="text-gray-500 mb-4">{error}</p>
|
||||||
<motion.button
|
<motion.button
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
@ -84,7 +72,7 @@ export default function NotificationsPage() {
|
|||||||
onClick={fetchNotifications}
|
onClick={fetchNotifications}
|
||||||
className="px-6 py-2 bg-amber-500 text-white rounded-xl font-medium hover:bg-amber-600 transition-colors"
|
className="px-6 py-2 bg-amber-500 text-white rounded-xl font-medium hover:bg-amber-600 transition-colors"
|
||||||
>
|
>
|
||||||
{t('retry')}
|
{t("retry")}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
@ -94,16 +82,10 @@ export default function NotificationsPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 py-8">
|
<div className="min-h-screen bg-gray-50 py-8">
|
||||||
<div className="container mx-auto px-4 max-w-4xl">
|
<div className="container mx-auto px-4 max-w-4xl">
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="flex justify-between items-center mb-8">
|
||||||
initial={{ opacity: 0, y: -20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="flex justify-between items-center mb-8"
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t('notifications')}</h1>
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t("notifications")}</h1>
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600">{unreadCount > 0 ? t("unread-notifications", { count: unreadCount }) : t("all-notifications-read")}</p>
|
||||||
{unreadCount > 0 ? t('unread-notifications', { count: unreadCount }) : t('all-notifications-read')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
{unreadCount > 0 && (
|
{unreadCount > 0 && (
|
||||||
<motion.button
|
<motion.button
|
||||||
@ -113,20 +95,16 @@ export default function NotificationsPage() {
|
|||||||
className="flex items-center gap-2 px-4 py-2 bg-white border border-gray-200 rounded-xl text-sm font-medium text-gray-700 hover:bg-gray-50 transition-all shadow-sm"
|
className="flex items-center gap-2 px-4 py-2 bg-white border border-gray-200 rounded-xl text-sm font-medium text-gray-700 hover:bg-gray-50 transition-all shadow-sm"
|
||||||
>
|
>
|
||||||
<CheckCheck className="w-4 h-4 text-amber-500" />
|
<CheckCheck className="w-4 h-4 text-amber-500" />
|
||||||
{t('mark-all-read')}
|
{t("mark-all-read")}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
)}
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{notifications.length === 0 ? (
|
{notifications.length === 0 ? (
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300"
|
|
||||||
>
|
|
||||||
<Bell className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
<Bell className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||||
<h3 className="text-xl font-bold text-gray-700 mb-2">{t('no-notifications')}</h3>
|
<h3 className="text-xl font-bold text-gray-700 mb-2">{t("no-notifications")}</h3>
|
||||||
<p className="text-gray-500">{t('notifications-empty-hint')}</p>
|
<p className="text-gray-500">{t("notifications-empty-hint")}</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@ -137,32 +115,20 @@ export default function NotificationsPage() {
|
|||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, x: 100 }}
|
exit={{ opacity: 0, x: 100 }}
|
||||||
transition={{ delay: index * 0.05, type: 'spring', stiffness: 100 }}
|
transition={{ delay: index * 0.05, type: "spring", stiffness: 100 }}
|
||||||
whileHover={{ scale: 1.01 }}
|
whileHover={{ scale: 1.01 }}
|
||||||
onClick={() => markAsRead(notification.id)}
|
onClick={() => markAsRead(notification.id)}
|
||||||
className={`bg-white rounded-2xl shadow-sm border transition-all hover:shadow-md cursor-pointer ${
|
className={`bg-white rounded-2xl shadow-sm border transition-all hover:shadow-md cursor-pointer ${!notification.read ? "border-amber-200 bg-amber-50/50" : "border-gray-200"}`}
|
||||||
!notification.read ? 'border-amber-200 bg-amber-50/50' : 'border-gray-200'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className="p-5 flex gap-4">
|
<div className="p-5 flex gap-4">
|
||||||
<div className={`w-12 h-12 rounded-full flex items-center justify-center shrink-0 ${
|
<div className={`w-12 h-12 rounded-full flex items-center justify-center shrink-0 ${!notification.read ? "bg-amber-100" : "bg-gray-100"}`}>
|
||||||
!notification.read ? 'bg-amber-100' : 'bg-gray-100'
|
{!notification.read ? <Bell className="w-6 h-6 text-amber-600" /> : <CheckCircle className="w-6 h-6 text-gray-400" />}
|
||||||
}`}>
|
|
||||||
{!notification.read ? (
|
|
||||||
<Bell className="w-6 h-6 text-amber-600" />
|
|
||||||
) : (
|
|
||||||
<CheckCircle className="w-6 h-6 text-gray-400" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex justify-between items-start gap-4">
|
<div className="flex justify-between items-start gap-4">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<h3 className={`text-base ${!notification.read ? 'font-bold text-gray-900' : 'font-medium text-gray-700'}`}>
|
<h3 className={`text-base ${!notification.read ? "font-bold text-gray-900" : "font-medium text-gray-700"}`}>{notification.title}</h3>
|
||||||
{notification.title}
|
{notification.message && <p className="text-gray-500 text-sm mt-1 line-clamp-2">{notification.message}</p>}
|
||||||
</h3>
|
|
||||||
{notification.message && (
|
|
||||||
<p className="text-gray-500 text-sm mt-1 line-clamp-2">{notification.message}</p>
|
|
||||||
)}
|
|
||||||
<div className="flex items-center gap-2 mt-2">
|
<div className="flex items-center gap-2 mt-2">
|
||||||
{notification.date && (
|
{notification.date && (
|
||||||
<span className="text-xs text-gray-400 flex items-center gap-1">
|
<span className="text-xs text-gray-400 flex items-center gap-1">
|
||||||
@ -178,9 +144,7 @@ export default function NotificationsPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!notification.read && (
|
{!notification.read && <span className="w-2.5 h-2.5 bg-amber-500 rounded-full shrink-0 mt-2" />}
|
||||||
<span className="w-2.5 h-2.5 bg-amber-500 rounded-full shrink-0 mt-2" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -76,7 +76,7 @@ export default function OnboardingPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div dir={i18n.language === 'ar' ? 'rtl' : 'ltr'} className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center p-4 relative overflow-hidden">
|
<div dir={i18n.language === 'ar' ? 'rtl' : 'ltr'} className="min-h-screen flex items-center justify-center p-4 relative overflow-hidden">
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
{[...Array(30)].map((_, i) => (
|
{[...Array(30)].map((_, i) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
@ -144,7 +144,7 @@ export default function OnboardingPage() {
|
|||||||
transition={{ type: 'spring', stiffness: 200, delay: 0.1 }}
|
transition={{ type: 'spring', stiffness: 200, delay: 0.1 }}
|
||||||
className="w-24 h-24 mx-auto bg-white/20 rounded-3xl flex items-center justify-center backdrop-blur-sm"
|
className="w-24 h-24 mx-auto bg-white/20 rounded-3xl flex items-center justify-center backdrop-blur-sm"
|
||||||
>
|
>
|
||||||
<IconComponent className="w-12 h-12 text-white" />
|
<IconComponent className="w-12 h-12 text-amber-50" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -154,7 +154,7 @@ export default function OnboardingPage() {
|
|||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 0.15 }}
|
transition={{ delay: 0.15 }}
|
||||||
className="text-3xl font-bold text-white mb-4"
|
className="text-3xl font-bold text-amber-50 mb-4"
|
||||||
>
|
>
|
||||||
{step.title}
|
{step.title}
|
||||||
</motion.h1>
|
</motion.h1>
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { DollarSign, TrendingUp, Calendar, Loader2, Star } from "lucide-react";
|
|||||||
import toast, { Toaster } from "react-hot-toast";
|
import toast, { Toaster } from "react-hot-toast";
|
||||||
import AuthService from "@/app/services/AuthService";
|
import AuthService from "@/app/services/AuthService";
|
||||||
import { getOwnerStatistics } from "@/app/utils/api";
|
import { getOwnerStatistics } from "@/app/utils/api";
|
||||||
|
import Loading from "@/app/loading";
|
||||||
|
|
||||||
const StatCard = ({ title, value, icon: Icon, color, subtitle, isNA }) => (
|
const StatCard = ({ title, value, icon: Icon, color, subtitle, isNA }) => (
|
||||||
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-all">
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-all">
|
||||||
@ -64,9 +65,7 @@ export default function OwnerAccountBookPage() {
|
|||||||
totalRevenue: data.totalRevenue ?? null,
|
totalRevenue: data.totalRevenue ?? null,
|
||||||
totalReservations: data.totalReservations ?? null,
|
totalReservations: data.totalReservations ?? null,
|
||||||
activeProperties: data.activeProperties ?? null,
|
activeProperties: data.activeProperties ?? null,
|
||||||
|
|
||||||
financialRevenue: data.financialRevenue ?? data.totalRevenue ?? null,
|
financialRevenue: data.financialRevenue ?? data.totalRevenue ?? null,
|
||||||
|
|
||||||
financialCommission: data.financialCommission ?? null,
|
financialCommission: data.financialCommission ?? null,
|
||||||
financialBalance: data.financialBalance ?? null,
|
financialBalance: data.financialBalance ?? null,
|
||||||
directRevenue: data.directRevenue ?? null,
|
directRevenue: data.directRevenue ?? null,
|
||||||
@ -106,14 +105,7 @@ export default function OwnerAccountBookPage() {
|
|||||||
const isNA = (val) => val === null || val === undefined || val === "";
|
const isNA = (val) => val === null || val === undefined || val === "";
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return <Loading />;
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
|
||||||
<div className="text-center">
|
|
||||||
<Loader2 className="w-12 h-12 text-amber-500 animate-spin mx-auto mb-4" />
|
|
||||||
<p className="text-gray-600">{t("accountBook.loading")}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from "framer-motion";
|
||||||
import { AlertTriangle, RefreshCw, Home } from 'lucide-react';
|
import { AlertTriangle, RefreshCw, Home } from "lucide-react";
|
||||||
import Link from 'next/link';
|
import Link from "next/link";
|
||||||
|
|
||||||
export default function Error({ error, reset }) {
|
export default function Error({ error, reset }) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,93 +1,69 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState, useCallback } from 'react';
|
import { useEffect, useState, useCallback } from "react";
|
||||||
import Link from 'next/link';
|
import Link from "next/link";
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from "framer-motion";
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import { Home, MapPin, Phone, ShieldCheck, CreditCard, Banknote, Building, ArrowLeftRight, Loader2, Check, X, Calendar, Clock, LogIn, Lock, Info, Landmark, Wallet } from "lucide-react";
|
||||||
Home,
|
import toast, { Toaster } from "react-hot-toast";
|
||||||
MapPin,
|
import AuthService from "@/app/services/AuthService";
|
||||||
Phone,
|
import { payDeposit, getMyTransaction, getPaymentTypes } from "@/app/utils/api";
|
||||||
ShieldCheck,
|
import Loading from "../loading";
|
||||||
CreditCard,
|
|
||||||
Banknote,
|
|
||||||
Building,
|
|
||||||
ArrowLeftRight,
|
|
||||||
Loader2,
|
|
||||||
Check,
|
|
||||||
X,
|
|
||||||
Calendar,
|
|
||||||
Clock,
|
|
||||||
LogIn,
|
|
||||||
Lock,
|
|
||||||
Info,
|
|
||||||
Landmark,
|
|
||||||
Wallet,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import toast, { Toaster } from 'react-hot-toast';
|
|
||||||
import AuthService from '@/app/services/AuthService';
|
|
||||||
import { payDeposit, getMyTransaction, getPaymentTypes } from '@/app/utils/api';
|
|
||||||
|
|
||||||
const STATUS_MAP = ['pending', 'ownerConfirmed', 'depositPaid', 'depositConfirmed', 'completed', 'cancelled'];
|
const STATUS_MAP = ["pending", "ownerConfirmed", "depositPaid", "depositConfirmed", "completed", "cancelled"];
|
||||||
|
|
||||||
function getStatusConfig(t) {
|
function getStatusConfig(t) {
|
||||||
return {
|
return {
|
||||||
pending: { label: t('bookingStatus.pending'), color: 'bg-yellow-100 text-yellow-800 border-yellow-300', icon: Clock },
|
pending: { label: t("bookingStatus.pending"), color: "bg-yellow-100 text-yellow-800 border-yellow-300", icon: Clock },
|
||||||
ownerConfirmed: { label: t('bookingStatus.ownerConfirmed'), color: 'bg-blue-100 text-blue-800 border-blue-300', icon: ShieldCheck },
|
ownerConfirmed: { label: t("bookingStatus.ownerConfirmed"), color: "bg-blue-100 text-blue-800 border-blue-300", icon: ShieldCheck },
|
||||||
depositPaid: { label: t('bookingStatus.depositPaid'), color: 'bg-orange-100 text-orange-800 border-orange-300', icon: Wallet },
|
depositPaid: { label: t("bookingStatus.depositPaid"), color: "bg-orange-100 text-orange-800 border-orange-300", icon: Wallet },
|
||||||
depositConfirmed: { label: t('bookingStatus.depositConfirmed'), color: 'bg-green-100 text-green-800 border-green-300', icon: Check },
|
depositConfirmed: { label: t("bookingStatus.depositConfirmed"), color: "bg-green-100 text-green-800 border-green-300", icon: Check },
|
||||||
completed: { label: t('bookingStatus.completed'), color: 'bg-teal-100 text-teal-800 border-teal-300', icon: Check },
|
completed: { label: t("bookingStatus.completed"), color: "bg-teal-100 text-teal-800 border-teal-300", icon: Check },
|
||||||
cancelled: { label: t('bookingStatus.cancelled'), color: 'bg-red-100 text-red-800 border-red-300', icon: X },
|
cancelled: { label: t("bookingStatus.cancelled"), color: "bg-red-100 text-red-800 border-red-300", icon: X },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPaymentMethods(t) {
|
function getPaymentMethods(t) {
|
||||||
return [
|
return [
|
||||||
{ id: 'cash', label: t('payments.method.cash'), desc: t('payments.method.cashDesc'), icon: Banknote, active: true },
|
{ id: "cash", label: t("payments.method.cash"), desc: t("payments.method.cashDesc"), icon: Banknote, active: true },
|
||||||
{ id: 'office', label: t('payments.method.office'), desc: t('payments.method.officeDesc'), icon: Building, active: true },
|
{ id: "office", label: t("payments.method.office"), desc: t("payments.method.officeDesc"), icon: Building, active: true },
|
||||||
{ id: 'transfer', label: t('payments.method.transfer'), desc: t('payments.method.transferDesc'), icon: ArrowLeftRight, active: false },
|
{ id: "transfer", label: t("payments.method.transfer"), desc: t("payments.method.transferDesc"), icon: ArrowLeftRight, active: false },
|
||||||
{ id: 'electronic', label: t('payments.method.electronic'), desc: t('payments.method.electronicDesc'), icon: CreditCard, active: false },
|
{ id: "electronic", label: t("payments.method.electronic"), desc: t("payments.method.electronicDesc"), icon: CreditCard, active: false },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatCurrency(v, sign = '') {
|
function formatCurrency(v, sign = "") {
|
||||||
return `${sign} ${Number(v ?? 0).toLocaleString()}`;
|
return `${sign} ${Number(v ?? 0).toLocaleString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(date) {
|
function formatDate(date) {
|
||||||
if (!date) return '';
|
if (!date) return "";
|
||||||
const d = new Date(date);
|
const d = new Date(date);
|
||||||
if (Number.isNaN(d.getTime())) return '';
|
if (Number.isNaN(d.getTime())) return "";
|
||||||
return d.toLocaleDateString('en-GB');
|
return d.toLocaleDateString("en-GB");
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeText(value) {
|
function normalizeText(value) {
|
||||||
return String(value ?? '').trim().toLowerCase();
|
return String(value ?? "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
function isHaramText(value) {
|
function isHaramText(value) {
|
||||||
const text = normalizeText(value);
|
const text = normalizeText(value);
|
||||||
return text.includes('haram') || text.includes('هرم') || text.includes('هرام');
|
return text.includes("haram") || text.includes("هرم") || text.includes("هرام");
|
||||||
}
|
}
|
||||||
|
|
||||||
function isReceiptRequiredPayment(value, method = null) {
|
function isReceiptRequiredPayment(value, method = null) {
|
||||||
const haystack = [
|
const haystack = [value, method?.name, method?.label, method?.id, method?.description].filter(Boolean).join(" ");
|
||||||
value,
|
|
||||||
method?.name,
|
|
||||||
method?.label,
|
|
||||||
method?.id,
|
|
||||||
method?.description,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' ');
|
|
||||||
|
|
||||||
const text = normalizeText(haystack);
|
const text = normalizeText(haystack);
|
||||||
return (
|
return (
|
||||||
text.includes('transfer') ||
|
text.includes("transfer") ||
|
||||||
text.includes('تحويل') ||
|
text.includes("تحويل") ||
|
||||||
text.includes('حوالة') ||
|
text.includes("حوالة") ||
|
||||||
text.includes('bank') ||
|
text.includes("bank") ||
|
||||||
text.includes('بنك') ||
|
text.includes("بنك") ||
|
||||||
isHaramText(value) ||
|
isHaramText(value) ||
|
||||||
isHaramText(method?.name) ||
|
isHaramText(method?.name) ||
|
||||||
isHaramText(method?.label) ||
|
isHaramText(method?.label) ||
|
||||||
@ -126,12 +102,12 @@ export default function PaymentsPage() {
|
|||||||
endDate: reservation.endDate,
|
endDate: reservation.endDate,
|
||||||
totalPrice: reservation.totalPrice ?? transaction.amount ?? 0,
|
totalPrice: reservation.totalPrice ?? transaction.amount ?? 0,
|
||||||
depositAmount: transaction.amount ?? reservation.totalPrice ?? 0,
|
depositAmount: transaction.amount ?? reservation.totalPrice ?? 0,
|
||||||
currencySign: currency.sign || t('currency.syp'),
|
currencySign: currency.sign || t("currency.syp"),
|
||||||
currencyName: currency.name || '',
|
currencyName: currency.name || "",
|
||||||
currencyRate: currency.rate,
|
currencyRate: currency.rate,
|
||||||
propertyName: propertyInfo.name || propertyInfo.address || reservation.propertyName || `${t('payments.propertyLabel')} #${reservation.id || ''}`,
|
propertyName: propertyInfo.name || propertyInfo.address || reservation.propertyName || `${t("payments.propertyLabel")} #${reservation.id || ""}`,
|
||||||
propertyAddress: propertyInfo.address || reservation.propertyAddress || '',
|
propertyAddress: propertyInfo.address || reservation.propertyAddress || "",
|
||||||
propertyCity: propertyInfo.city || reservation.city || '',
|
propertyCity: propertyInfo.city || reservation.city || "",
|
||||||
_deposit: deposit,
|
_deposit: deposit,
|
||||||
_reservation: reservation,
|
_reservation: reservation,
|
||||||
};
|
};
|
||||||
@ -140,7 +116,7 @@ export default function PaymentsPage() {
|
|||||||
setReservations(mapped);
|
setReservations(mapped);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
toast.error(t('payments.loadingTransactions'));
|
toast.error(t("payments.loadingTransactions"));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -158,7 +134,7 @@ export default function PaymentsPage() {
|
|||||||
setSelectedPayment(firstActive.name ?? firstActive.id ?? null);
|
setSelectedPayment(firstActive.name ?? firstActive.id ?? null);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Payments] failed to load payment methods', err);
|
console.error("[Payments] failed to load payment methods", err);
|
||||||
setPaymentMethods([]);
|
setPaymentMethods([]);
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingPaymentMethods(false);
|
setLoadingPaymentMethods(false);
|
||||||
@ -180,21 +156,17 @@ export default function PaymentsPage() {
|
|||||||
}, [loadPaymentMethods]);
|
}, [loadPaymentMethods]);
|
||||||
|
|
||||||
const resolvePaymentTypeId = (paymentKey) => {
|
const resolvePaymentTypeId = (paymentKey) => {
|
||||||
const method = paymentMethods.find(
|
const method = paymentMethods.find((m) => String(m.id) === String(paymentKey) || String(m.name) === String(paymentKey));
|
||||||
(m) => String(m.id) === String(paymentKey) || String(m.name) === String(paymentKey),
|
|
||||||
);
|
|
||||||
return method?.id ?? paymentKey;
|
return method?.id ?? paymentKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePayDeposit = async (reservation, paymentKey, paymentImageFile) => {
|
const handlePayDeposit = async (reservation, paymentKey, paymentImageFile) => {
|
||||||
const paymentTypeId = resolvePaymentTypeId(paymentKey);
|
const paymentTypeId = resolvePaymentTypeId(paymentKey);
|
||||||
const selectedMethod = paymentMethods.find(
|
const selectedMethod = paymentMethods.find((m) => String(m.id) === String(paymentTypeId) || String(m.name) === String(paymentTypeId));
|
||||||
(m) => String(m.id) === String(paymentTypeId) || String(m.name) === String(paymentTypeId),
|
|
||||||
);
|
|
||||||
const requiresReceiptUpload = isReceiptRequiredPayment(paymentKey, selectedMethod);
|
const requiresReceiptUpload = isReceiptRequiredPayment(paymentKey, selectedMethod);
|
||||||
|
|
||||||
if (requiresReceiptUpload && !paymentImageFile) {
|
if (requiresReceiptUpload && !paymentImageFile) {
|
||||||
toast.error(t('payments.receiptRequired'));
|
toast.error(t("payments.receiptRequired"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,46 +177,33 @@ export default function PaymentsPage() {
|
|||||||
paymentTypeId,
|
paymentTypeId,
|
||||||
paymentImage: paymentImageFile,
|
paymentImage: paymentImageFile,
|
||||||
});
|
});
|
||||||
toast.success(t('payments.depositPaidSuccess'));
|
toast.success(t("payments.depositPaidSuccess"));
|
||||||
loadReservations();
|
loadReservations();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err?.message || t('payments.paymentFailed'));
|
toast.error(err?.message || t("payments.paymentFailed"));
|
||||||
} finally {
|
} finally {
|
||||||
setPayingId(null);
|
setPayingId(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const canPay = (status) => STATUS_MAP[status] === 'ownerConfirmed';
|
const canPay = (status) => STATUS_MAP[status] === "ownerConfirmed";
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return <Loading />;
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center" dir={i18n.dir()}>
|
|
||||||
<Loader2 className="w-12 h-12 text-amber-500 animate-spin" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGuest) {
|
if (isGuest) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-linear-to-b from-amber-50/50 to-white flex items-center justify-center p-4" dir={i18n.dir()}>
|
<div className="min-h-screen bg-linear-to-b from-amber-50/50 to-white flex items-center justify-center p-4" dir={i18n.dir()}>
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} className="bg-white rounded-3xl shadow-xl border border-gray-200 p-10 max-w-md w-full text-center">
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
className="bg-white rounded-3xl shadow-xl border border-gray-200 p-10 max-w-md w-full text-center"
|
|
||||||
>
|
|
||||||
<div className="w-20 h-20 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
<div className="w-20 h-20 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
<Lock className="w-10 h-10 text-amber-600" />
|
<Lock className="w-10 h-10 text-amber-600" />
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-3">{t('payments.title')}</h2>
|
<h2 className="text-2xl font-bold text-gray-900 mb-3">{t("payments.title")}</h2>
|
||||||
<p className="text-gray-600 leading-relaxed mb-8">
|
<p className="text-gray-600 leading-relaxed mb-8">{t("payments.loginRequiredDesc")}</p>
|
||||||
{t('payments.loginRequiredDesc')}
|
<Link href="/login" className="inline-flex items-center gap-2 bg-amber-500 hover:bg-amber-600 text-white px-8 py-3 rounded-2xl text-lg font-semibold transition shadow-lg shadow-amber-200">
|
||||||
</p>
|
|
||||||
<Link
|
|
||||||
href="/login"
|
|
||||||
className="inline-flex items-center gap-2 bg-amber-500 hover:bg-amber-600 text-white px-8 py-3 rounded-2xl text-lg font-semibold transition shadow-lg shadow-amber-200"
|
|
||||||
>
|
|
||||||
<LogIn className="w-5 h-5" />
|
<LogIn className="w-5 h-5" />
|
||||||
{t('login')}
|
{t("login")}
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
@ -258,20 +217,16 @@ export default function PaymentsPage() {
|
|||||||
<div className="min-h-screen bg-gray-50 py-8" dir={i18n.dir()}>
|
<div className="min-h-screen bg-gray-50 py-8" dir={i18n.dir()}>
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
<div className="container mx-auto px-4 max-w-4xl">
|
<div className="container mx-auto px-4 max-w-4xl">
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="mb-8">
|
||||||
initial={{ opacity: 0, y: -20 }}
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t("payments.title")}</h1>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<p className="text-gray-600">{t("payments.description")}</p>
|
||||||
className="mb-8"
|
|
||||||
>
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t('payments.title')}</h1>
|
|
||||||
<p className="text-gray-600">{t('payments.description')}</p>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{payables.length > 0 && (
|
{payables.length > 0 && (
|
||||||
<div className="space-y-6 mb-10">
|
<div className="space-y-6 mb-10">
|
||||||
<h2 className="text-xl font-bold text-gray-800 flex items-center gap-2">
|
<h2 className="text-xl font-bold text-gray-800 flex items-center gap-2">
|
||||||
<Wallet className="w-5 h-5 text-amber-500" />
|
<Wallet className="w-5 h-5 text-amber-500" />
|
||||||
{t('payments.payablesTitle')}
|
{t("payments.payablesTitle")}
|
||||||
</h2>
|
</h2>
|
||||||
{payables.map((r, i) => (
|
{payables.map((r, i) => (
|
||||||
<PaymentCard
|
<PaymentCard
|
||||||
@ -292,21 +247,16 @@ export default function PaymentsPage() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h2 className="text-xl font-bold text-gray-800 flex items-center gap-2">
|
<h2 className="text-xl font-bold text-gray-800 flex items-center gap-2">
|
||||||
<Clock className="w-5 h-5 text-gray-500" />
|
<Clock className="w-5 h-5 text-gray-500" />
|
||||||
{t('payments.previousReservationsTitle')}
|
{t("payments.previousReservationsTitle")}
|
||||||
</h2>
|
</h2>
|
||||||
{others.map((r, i) => {
|
{others.map((r, i) => {
|
||||||
const statusKey = STATUS_MAP[r.status] || 'pending';
|
const statusKey = STATUS_MAP[r.status] || "pending";
|
||||||
const cfg = getStatusConfig(t)[statusKey];
|
const cfg = getStatusConfig(t)[statusKey];
|
||||||
const Icon = cfg.icon;
|
const Icon = cfg.icon;
|
||||||
const amount = r.depositAmount || r.totalPrice || 0;
|
const amount = r.depositAmount || r.totalPrice || 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div key={r.id || i} initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="bg-white rounded-2xl shadow-sm border border-gray-200 p-5">
|
||||||
key={r.id || i}
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="bg-white rounded-2xl shadow-sm border border-gray-200 p-5"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between gap-3 mb-3">
|
<div className="flex items-center justify-between gap-3 mb-3">
|
||||||
<span className="text-sm font-medium text-gray-400">#{r.reservationId || r.id}</span>
|
<span className="text-sm font-medium text-gray-400">#{r.reservationId || r.id}</span>
|
||||||
<span className={`inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium border ${cfg.color}`}>
|
<span className={`inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium border ${cfg.color}`}>
|
||||||
@ -319,9 +269,7 @@ export default function PaymentsPage() {
|
|||||||
<Calendar className="w-4 h-4" />
|
<Calendar className="w-4 h-4" />
|
||||||
{formatDate(r.startDate)} - {formatDate(r.endDate)}
|
{formatDate(r.startDate)} - {formatDate(r.endDate)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-lg font-bold text-gray-900">
|
<div className="text-lg font-bold text-gray-900">{formatCurrency(amount, r.currencySign)}</div>
|
||||||
{formatCurrency(amount, r.currencySign)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
@ -330,14 +278,10 @@ export default function PaymentsPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{reservations.length === 0 && (
|
{reservations.length === 0 && (
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300"
|
|
||||||
>
|
|
||||||
<CreditCard className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
<CreditCard className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||||
<h3 className="text-xl font-bold text-gray-700 mb-2">{t('payments.noTransactions')}</h3>
|
<h3 className="text-xl font-bold text-gray-700 mb-2">{t("payments.noTransactions")}</h3>
|
||||||
<p className="text-gray-500">{t('payments.noTransactionsDesc')}</p>
|
<p className="text-gray-500">{t("payments.noTransactionsDesc")}</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -353,8 +297,8 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
|
|||||||
const [showCashDialog, setShowCashDialog] = useState(false);
|
const [showCashDialog, setShowCashDialog] = useState(false);
|
||||||
const [showReceiptModal, setShowReceiptModal] = useState(false);
|
const [showReceiptModal, setShowReceiptModal] = useState(false);
|
||||||
const [localPaymentImage, setLocalPaymentImage] = useState(null);
|
const [localPaymentImage, setLocalPaymentImage] = useState(null);
|
||||||
const [receiptPreviewUrl, setReceiptPreviewUrl] = useState('');
|
const [receiptPreviewUrl, setReceiptPreviewUrl] = useState("");
|
||||||
const [receiptFileName, setReceiptFileName] = useState('');
|
const [receiptFileName, setReceiptFileName] = useState("");
|
||||||
|
|
||||||
const methods = paymentMethods.length > 0 ? paymentMethods : getPaymentMethods(t);
|
const methods = paymentMethods.length > 0 ? paymentMethods : getPaymentMethods(t);
|
||||||
|
|
||||||
@ -371,7 +315,7 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!localPaymentImage) {
|
if (!localPaymentImage) {
|
||||||
setReceiptPreviewUrl('');
|
setReceiptPreviewUrl("");
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -391,19 +335,19 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
|
|||||||
|
|
||||||
setShowReceiptModal(false);
|
setShowReceiptModal(false);
|
||||||
setLocalPaymentImage(null);
|
setLocalPaymentImage(null);
|
||||||
setReceiptPreviewUrl('');
|
setReceiptPreviewUrl("");
|
||||||
setReceiptFileName('');
|
setReceiptFileName("");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReceiptSelection = (event) => {
|
const handleReceiptSelection = (event) => {
|
||||||
const file = event.target.files?.[0] || null;
|
const file = event.target.files?.[0] || null;
|
||||||
setLocalPaymentImage(file);
|
setLocalPaymentImage(file);
|
||||||
setReceiptFileName(file?.name || '');
|
setReceiptFileName(file?.name || "");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReceiptConfirm = () => {
|
const handleReceiptConfirm = () => {
|
||||||
if (!localPaymentImage) {
|
if (!localPaymentImage) {
|
||||||
toast.error(t('payments.receiptRequired'));
|
toast.error(t("payments.receiptRequired"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -412,11 +356,7 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="bg-white rounded-3xl shadow-md border border-gray-200 overflow-hidden">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="bg-white rounded-3xl shadow-md border border-gray-200 overflow-hidden"
|
|
||||||
>
|
|
||||||
{/* Property details */}
|
{/* Property details */}
|
||||||
<div className="p-6 border-b border-gray-100">
|
<div className="p-6 border-b border-gray-100">
|
||||||
<div className="flex items-start gap-4 mb-4">
|
<div className="flex items-start gap-4 mb-4">
|
||||||
@ -424,13 +364,11 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
|
|||||||
<Home className="w-7 h-7 text-amber-600" />
|
<Home className="w-7 h-7 text-amber-600" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<h3 className="text-xl font-bold text-gray-900 mb-1">
|
<h3 className="text-xl font-bold text-gray-900 mb-1">{r.propertyName}</h3>
|
||||||
{r.propertyName}
|
|
||||||
</h3>
|
|
||||||
{(r.propertyAddress || r.propertyCity) && (
|
{(r.propertyAddress || r.propertyCity) && (
|
||||||
<p className="text-sm text-gray-500 flex items-center gap-1">
|
<p className="text-sm text-gray-500 flex items-center gap-1">
|
||||||
<MapPin className="w-3.5 h-3.5" />
|
<MapPin className="w-3.5 h-3.5" />
|
||||||
{[r.propertyCity, r.propertyAddress].filter(Boolean).join(' - ')}
|
{[r.propertyCity, r.propertyAddress].filter(Boolean).join(" - ")}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -442,8 +380,7 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
|
|||||||
{formatDate(r.startDate)} - {formatDate(r.endDate)}
|
{formatDate(r.startDate)} - {formatDate(r.endDate)}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1.5">
|
<span className="flex items-center gap-1.5">
|
||||||
<Clock className="w-4 h-4 text-gray-400" />
|
<Clock className="w-4 h-4 text-gray-400" />#{r.reservationId || r.id}
|
||||||
#{r.reservationId || r.id}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -451,13 +388,9 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
|
|||||||
{/* Deposit amount */}
|
{/* Deposit amount */}
|
||||||
<div className="px-6 py-5 border-b border-gray-100">
|
<div className="px-6 py-5 border-b border-gray-100">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-sm text-gray-500 mb-1">{t('payments.depositAmount')}</p>
|
<p className="text-sm text-gray-500 mb-1">{t("payments.depositAmount")}</p>
|
||||||
<p className="text-4xl font-bold text-amber-600">
|
<p className="text-4xl font-bold text-amber-600">{formatCurrency(amount, r.currencySign)}</p>
|
||||||
{formatCurrency(amount, r.currencySign)}
|
<p className="text-xs text-gray-400 mt-2">{t("payments.depositPaidToPlatform")}</p>
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-400 mt-2">
|
|
||||||
{t('payments.depositPaidToPlatform')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -465,24 +398,18 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
|
|||||||
<div className="px-6 py-4 bg-amber-50 border-b border-amber-100">
|
<div className="px-6 py-4 bg-amber-50 border-b border-amber-100">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<Info className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" />
|
<Info className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" />
|
||||||
<p className="text-sm text-amber-800 leading-relaxed">
|
<p className="text-sm text-amber-800 leading-relaxed">{t("payments.cashOnlyNotice")}</p>
|
||||||
{t('payments.cashOnlyNotice')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Payment method options */}
|
{/* Payment method options */}
|
||||||
<div className="px-6 py-5 border-b border-gray-100">
|
<div className="px-6 py-5 border-b border-gray-100">
|
||||||
<p className="text-sm font-bold text-gray-700 mb-3">{t('payments.paymentMethodLabel')}</p>
|
<p className="text-sm font-bold text-gray-700 mb-3">{t("payments.paymentMethodLabel")}</p>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
{loadingPaymentMethods ? (
|
{loadingPaymentMethods ? (
|
||||||
<div className="col-span-full rounded-2xl border border-dashed border-gray-200 bg-gray-50 p-4 text-sm text-gray-600">
|
<div className="col-span-full rounded-2xl border border-dashed border-gray-200 bg-gray-50 p-4 text-sm text-gray-600">{t("payments.loadingPaymentMethods")}</div>
|
||||||
{t('payments.loadingPaymentMethods')}
|
|
||||||
</div>
|
|
||||||
) : methods.length === 0 ? (
|
) : methods.length === 0 ? (
|
||||||
<div className="col-span-full rounded-2xl border border-dashed border-gray-200 bg-gray-50 p-4 text-sm text-gray-600">
|
<div className="col-span-full rounded-2xl border border-dashed border-gray-200 bg-gray-50 p-4 text-sm text-gray-600">{t("payments.noPaymentMethodsAvailable")}</div>
|
||||||
{t('payments.noPaymentMethodsAvailable')}
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
methods.map((method, index) => {
|
methods.map((method, index) => {
|
||||||
const optionId = method.name ?? method.id ?? `payment-method-${index}`;
|
const optionId = method.name ?? method.id ?? `payment-method-${index}`;
|
||||||
@ -498,34 +425,20 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
|
|||||||
onClick={() => isActive && handleSelectMethod(optionId, method)}
|
onClick={() => isActive && handleSelectMethod(optionId, method)}
|
||||||
className={`relative flex items-start gap-3 p-4 rounded-2xl border-2 text-right transition-all ${
|
className={`relative flex items-start gap-3 p-4 rounded-2xl border-2 text-right transition-all ${
|
||||||
!isActive
|
!isActive
|
||||||
? 'border-gray-100 bg-gray-50 opacity-50 cursor-not-allowed'
|
? "border-gray-100 bg-gray-50 opacity-50 cursor-not-allowed"
|
||||||
: isSelected
|
: isSelected
|
||||||
? 'border-amber-500 bg-amber-50 shadow-sm'
|
? "border-amber-500 bg-amber-50 shadow-sm"
|
||||||
: 'border-gray-200 bg-white hover:border-amber-300 cursor-pointer'
|
: "border-gray-200 bg-white hover:border-amber-300 cursor-pointer"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div className={`w-10 h-10 rounded-xl flex items-center justify-center shrink-0 ${isSelected ? "bg-amber-500 text-white" : "bg-gray-100 text-gray-500"}`}>
|
||||||
className={`w-10 h-10 rounded-xl flex items-center justify-center shrink-0 ${
|
|
||||||
isSelected ? 'bg-amber-500 text-white' : 'bg-gray-100 text-gray-500'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Icon className="w-5 h-5" />
|
<Icon className="w-5 h-5" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className={`text-sm font-bold ${isSelected ? 'text-amber-900' : 'text-gray-800'}`}>
|
<p className={`text-sm font-bold ${isSelected ? "text-amber-900" : "text-gray-800"}`}>{method.name || method.label || t("payments.paymentMethodName")}</p>
|
||||||
{method.name || method.label || t('payments.paymentMethodName')}
|
{method.description && <p className="text-xs text-gray-500 mt-0.5 leading-relaxed">{method.description}</p>}
|
||||||
</p>
|
<span className={`inline-block mt-1 text-[10px] font-medium px-2 py-0.5 rounded-full ${isActive ? "bg-amber-100 text-amber-700" : "bg-gray-200 text-gray-500"}`}>
|
||||||
{method.description && (
|
{isActive ? t("payments.methodActive") : t("payments.methodInactive")}
|
||||||
<p className="text-xs text-gray-500 mt-0.5 leading-relaxed">
|
|
||||||
{method.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<span
|
|
||||||
className={`inline-block mt-1 text-[10px] font-medium px-2 py-0.5 rounded-full ${
|
|
||||||
isActive ? 'bg-amber-100 text-amber-700' : 'bg-gray-200 text-gray-500'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isActive ? t('payments.methodActive') : t('payments.methodInactive')}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
@ -544,7 +457,7 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
|
|||||||
<div className="px-6 py-4 bg-amber-50 border-b border-amber-100">
|
<div className="px-6 py-4 bg-amber-50 border-b border-amber-100">
|
||||||
<div className="flex items-start gap-2 text-sm text-amber-800">
|
<div className="flex items-start gap-2 text-sm text-amber-800">
|
||||||
<Info className="w-4 h-4 shrink-0 mt-0.5" />
|
<Info className="w-4 h-4 shrink-0 mt-0.5" />
|
||||||
<p>{t('payments.receiptUploadRequiredHint')}</p>
|
<p>{t("payments.receiptUploadRequiredHint")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -562,12 +475,12 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
|
|||||||
{payingId === r.id ? (
|
{payingId === r.id ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="w-5 h-5 animate-spin" />
|
<Loader2 className="w-5 h-5 animate-spin" />
|
||||||
{t('payments.processingPayment')}
|
{t("payments.processingPayment")}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Banknote className="w-5 h-5" />
|
<Banknote className="w-5 h-5" />
|
||||||
{t('payments.payButton', { amount: formatCurrency(amount, r.currencySign) })}
|
{t("payments.payButton", { amount: formatCurrency(amount, r.currencySign) })}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
@ -575,24 +488,18 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
|
|||||||
|
|
||||||
{showReceiptModal && (
|
{showReceiptModal && (
|
||||||
<div className="fixed inset-0 z-60 flex items-center justify-center bg-black/60 px-4 py-8">
|
<div className="fixed inset-0 z-60 flex items-center justify-center bg-black/60 px-4 py-8">
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} className="w-full max-w-md rounded-3xl bg-white p-6 shadow-2xl border border-gray-200">
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
className="w-full max-w-md rounded-3xl bg-white p-6 shadow-2xl border border-gray-200"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-bold text-gray-900">{t('payments.receiptDialogTitle')}</h3>
|
<h3 className="text-xl font-bold text-gray-900">{t("payments.receiptDialogTitle")}</h3>
|
||||||
<p className="text-sm text-gray-600 mt-1">{t('payments.receiptDialogDescription')}</p>
|
<p className="text-sm text-gray-600 mt-1">{t("payments.receiptDialogDescription")}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-12 h-12 bg-amber-100 rounded-2xl flex items-center justify-center">
|
<div className="w-12 h-12 bg-amber-100 rounded-2xl flex items-center justify-center">
|
||||||
<CreditCard className="w-6 h-6 text-amber-600" />
|
<CreditCard className="w-6 h-6 text-amber-600" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className="block text-sm font-bold text-gray-700 mb-2">
|
<label className="block text-sm font-bold text-gray-700 mb-2">{t("payments.uploadReceipt")}</label>
|
||||||
{t('payments.uploadReceipt')}
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
@ -602,34 +509,22 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
|
|||||||
|
|
||||||
{receiptFileName && (
|
{receiptFileName && (
|
||||||
<div className="mt-3 rounded-2xl border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800">
|
<div className="mt-3 rounded-2xl border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800">
|
||||||
{t('payments.receiptSelected')} {receiptFileName}
|
{t("payments.receiptSelected")} {receiptFileName}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{localPaymentImage && receiptPreviewUrl && (
|
{localPaymentImage && receiptPreviewUrl && (
|
||||||
<div className="mt-4 overflow-hidden rounded-2xl border border-gray-200 bg-gray-50 p-2">
|
<div className="mt-4 overflow-hidden rounded-2xl border border-gray-200 bg-gray-50 p-2">
|
||||||
<img
|
<img src={receiptPreviewUrl} alt={t("payments.uploadReceipt")} className="h-48 w-full rounded-xl object-cover" />
|
||||||
src={receiptPreviewUrl}
|
|
||||||
alt={t('payments.uploadReceipt')}
|
|
||||||
className="h-48 w-full rounded-xl object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-6 flex flex-col gap-3 sm:flex-row">
|
<div className="mt-6 flex flex-col gap-3 sm:flex-row">
|
||||||
<button
|
<button type="button" onClick={() => setShowReceiptModal(false)} className="w-full rounded-xl border border-gray-300 px-4 py-3 text-sm font-semibold text-gray-700 hover:bg-gray-100">
|
||||||
type="button"
|
{t("payments.closeButton")}
|
||||||
onClick={() => setShowReceiptModal(false)}
|
|
||||||
className="w-full rounded-xl border border-gray-300 px-4 py-3 text-sm font-semibold text-gray-700 hover:bg-gray-100"
|
|
||||||
>
|
|
||||||
{t('payments.closeButton')}
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button" onClick={handleReceiptConfirm} className="w-full rounded-xl bg-amber-500 px-4 py-3 text-sm font-semibold text-white hover:bg-amber-600">
|
||||||
type="button"
|
{t("payments.continueToPay")}
|
||||||
onClick={handleReceiptConfirm}
|
|
||||||
className="w-full rounded-xl bg-amber-500 px-4 py-3 text-sm font-semibold text-white hover:bg-amber-600"
|
|
||||||
>
|
|
||||||
{t('payments.continueToPay')}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@ -644,30 +539,26 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
|
|||||||
className="w-full bg-white border-2 border-amber-200 hover:border-amber-400 text-amber-700 font-bold py-3 px-6 rounded-2xl text-base transition-all flex items-center justify-center gap-2"
|
className="w-full bg-white border-2 border-amber-200 hover:border-amber-400 text-amber-700 font-bold py-3 px-6 rounded-2xl text-base transition-all flex items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
<Building className="w-5 h-5" />
|
<Building className="w-5 h-5" />
|
||||||
{t('payments.payCashButton')}
|
{t("payments.payCashButton")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cash payment dialog */}
|
{/* Cash payment dialog */}
|
||||||
{showCashDialog && (
|
{showCashDialog && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 px-4 py-8">
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 px-4 py-8">
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} className="w-full max-w-md rounded-3xl bg-white p-8 shadow-2xl border border-gray-200 text-center">
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
className="w-full max-w-md rounded-3xl bg-white p-8 shadow-2xl border border-gray-200 text-center"
|
|
||||||
>
|
|
||||||
<div className="w-16 h-16 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-5">
|
<div className="w-16 h-16 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-5">
|
||||||
<Clock className="w-8 h-8 text-amber-600" />
|
<Clock className="w-8 h-8 text-amber-600" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-xl font-bold text-gray-900 mb-3">{t('payments.cashPendingDialogTitle')}</h3>
|
<h3 className="text-xl font-bold text-gray-900 mb-3">{t("payments.cashPendingDialogTitle")}</h3>
|
||||||
<p className="text-gray-600 leading-relaxed mb-6">
|
<p className="text-gray-600 leading-relaxed mb-6">
|
||||||
{t('payments.cashPendingDialogDesc1')}
|
{t("payments.cashPendingDialogDesc1")}
|
||||||
<br />
|
<br />
|
||||||
{t('payments.cashPendingDialogDesc2')}
|
{t("payments.cashPendingDialogDesc2")}
|
||||||
</p>
|
</p>
|
||||||
<div className="bg-gray-50 rounded-2xl p-4 mb-6 text-right">
|
<div className="bg-gray-50 rounded-2xl p-4 mb-6 text-right">
|
||||||
<p className="text-sm font-bold text-gray-800 mb-1">{t('payments.platformOfficeLabel')}</p>
|
<p className="text-sm font-bold text-gray-800 mb-1">{t("payments.platformOfficeLabel")}</p>
|
||||||
<p className="text-sm text-gray-600">{t('payments.platformOfficeFullAddress')}</p>
|
<p className="text-sm text-gray-600">{t("payments.platformOfficeFullAddress")}</p>
|
||||||
<p className="text-sm text-gray-600 flex items-center gap-1 mt-1" dir="ltr">
|
<p className="text-sm text-gray-600 flex items-center gap-1 mt-1" dir="ltr">
|
||||||
<Phone className="w-3.5 h-3.5" />
|
<Phone className="w-3.5 h-3.5" />
|
||||||
+963567823411
|
+963567823411
|
||||||
@ -679,13 +570,10 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
|
|||||||
onClick={() => setShowCashDialog(false)}
|
onClick={() => setShowCashDialog(false)}
|
||||||
className="w-full px-5 py-3 rounded-xl border border-gray-300 text-gray-700 hover:bg-gray-100 transition-colors font-medium"
|
className="w-full px-5 py-3 rounded-xl border border-gray-300 text-gray-700 hover:bg-gray-100 transition-colors font-medium"
|
||||||
>
|
>
|
||||||
{t('payments.closeButton')}
|
{t("payments.closeButton")}
|
||||||
</button>
|
</button>
|
||||||
<Link
|
<Link href="/reservations" className="w-full px-5 py-3 rounded-xl bg-amber-500 text-white font-semibold text-center hover:bg-amber-600 transition-colors">
|
||||||
href="/reservations"
|
{t("payments.myReservationsLink")}
|
||||||
className="w-full px-5 py-3 rounded-xl bg-amber-500 text-white font-semibold text-center hover:bg-amber-600 transition-colors"
|
|
||||||
>
|
|
||||||
{t('payments.myReservationsLink')}
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@ -697,10 +585,8 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
|
|||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<Landmark className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" />
|
<Landmark className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-bold text-gray-800 mb-1">{t('payments.cashPaymentLocationLabel')}</p>
|
<p className="text-sm font-bold text-gray-800 mb-1">{t("payments.cashPaymentLocationLabel")}</p>
|
||||||
<p className="text-sm text-gray-600 leading-relaxed">
|
<p className="text-sm text-gray-600 leading-relaxed">{t("payments.platformOfficeFullAddress")}</p>
|
||||||
{t('payments.platformOfficeFullAddress')}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-600 flex items-center gap-1 mt-1">
|
<p className="text-sm text-gray-600 flex items-center gap-1 mt-1">
|
||||||
<Phone className="w-3.5 h-3.5" />
|
<Phone className="w-3.5 h-3.5" />
|
||||||
<span dir="ltr">+963567823411</span>
|
<span dir="ltr">+963567823411</span>
|
||||||
|
|||||||
@ -5,24 +5,11 @@ import { motion } from "framer-motion";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import { User, Mail, Phone, MessageCircle, Check, X, ArrowLeft, Building, Home, Calendar, MapPin, Loader2, Pencil } from "lucide-react";
|
||||||
User,
|
|
||||||
Mail,
|
|
||||||
Phone,
|
|
||||||
MessageCircle,
|
|
||||||
Check,
|
|
||||||
X,
|
|
||||||
ArrowLeft,
|
|
||||||
Building,
|
|
||||||
Home,
|
|
||||||
Calendar,
|
|
||||||
MapPin,
|
|
||||||
Loader2,
|
|
||||||
Pencil,
|
|
||||||
} from "lucide-react";
|
|
||||||
import toast, { Toaster } from "react-hot-toast";
|
import toast, { Toaster } from "react-hot-toast";
|
||||||
import AuthService from "../services/AuthService";
|
import AuthService from "../services/AuthService";
|
||||||
import { getCustomerByUserId, getOwnerByUserId } from "../utils/api";
|
import { getCustomerByUserId, getOwnerByUserId } from "../utils/api";
|
||||||
|
import InteractiveBackground from "../components/Animation/Background";
|
||||||
|
|
||||||
export default function ProfilePage() {
|
export default function ProfilePage() {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
@ -59,30 +46,23 @@ export default function ProfilePage() {
|
|||||||
|
|
||||||
async function fetchProfile() {
|
async function fetchProfile() {
|
||||||
try {
|
try {
|
||||||
const fetchFn =
|
const fetchFn = userData.role === "owner" ? getOwnerByUserId : getCustomerByUserId;
|
||||||
userData.role === "owner" ? getOwnerByUserId : getCustomerByUserId;
|
|
||||||
const profile = await fetchFn(userData.id);
|
const profile = await fetchFn(userData.id);
|
||||||
|
|
||||||
if (profile) {
|
if (profile) {
|
||||||
const profileData = {
|
const profileData = {
|
||||||
name:
|
name: profile.fullName || profile.name || `${profile.firstName || ""} ${profile.lastName || ""}`.trim() || userData.name || "",
|
||||||
profile.fullName ||
|
|
||||||
profile.name ||
|
|
||||||
`${profile.firstName || ""} ${profile.lastName || ""}`.trim() ||
|
|
||||||
userData.name ||
|
|
||||||
"",
|
|
||||||
email: profile.email || userData.email || "",
|
email: profile.email || userData.email || "",
|
||||||
phone:
|
phone: profile.phone || profile.phoneNumber || userData.phone || "",
|
||||||
profile.phone || profile.phoneNumber || userData.phone || "",
|
|
||||||
whatsapp: profile.whatsAppNumber || profile.whatsapp || "",
|
whatsapp: profile.whatsAppNumber || profile.whatsapp || "",
|
||||||
bio: profile.bio || "",
|
bio: profile.bio || "",
|
||||||
location: profile.address || profile.location || "",
|
location: profile.address || profile.location || "",
|
||||||
joinedDate: profile.createdAt
|
joinedDate: profile.createdAt
|
||||||
? new Date(profile.createdAt).toLocaleDateString(i18n.language === 'ar' ? 'ar-SA' : 'en-US', {
|
? new Date(profile.createdAt).toLocaleDateString(i18n.language === "ar" ? "ar-SA" : "en-US", {
|
||||||
month: "long",
|
month: "long",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
})
|
})
|
||||||
: new Date().toLocaleDateString(i18n.language === 'ar' ? 'ar-SA' : 'en-US', {
|
: new Date().toLocaleDateString(i18n.language === "ar" ? "ar-SA" : "en-US", {
|
||||||
month: "long",
|
month: "long",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
}),
|
}),
|
||||||
@ -109,7 +89,7 @@ export default function ProfilePage() {
|
|||||||
whatsapp: "",
|
whatsapp: "",
|
||||||
bio: "",
|
bio: "",
|
||||||
location: "",
|
location: "",
|
||||||
joinedDate: new Date().toLocaleDateString(i18n.language === 'ar' ? 'ar-SA' : 'en-US', {
|
joinedDate: new Date().toLocaleDateString(i18n.language === "ar" ? "ar-SA" : "en-US", {
|
||||||
month: "long",
|
month: "long",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
}),
|
}),
|
||||||
@ -146,7 +126,7 @@ export default function ProfilePage() {
|
|||||||
setFormData(updatedData);
|
setFormData(updatedData);
|
||||||
localStorage.setItem("userProfile", JSON.stringify(updatedData));
|
localStorage.setItem("userProfile", JSON.stringify(updatedData));
|
||||||
setEditingBio(false);
|
setEditingBio(false);
|
||||||
toast.success(t('bio-updated-success'));
|
toast.success(t("bio-updated-success"));
|
||||||
};
|
};
|
||||||
|
|
||||||
const fadeInUp = {
|
const fadeInUp = {
|
||||||
@ -160,37 +140,25 @@ export default function ProfilePage() {
|
|||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Loader2 className="w-12 h-12 text-amber-500 animate-spin mx-auto mb-4" />
|
<Loader2 className="w-12 h-12 text-amber-500 animate-spin mx-auto mb-4" />
|
||||||
<p className="text-gray-600">{t('loading-profile')}</p>
|
<p className="text-gray-600">{t("loading-profile")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div dir={i18n.language === 'ar' ? 'rtl' : 'ltr'} className="min-h-screen bg-gray-50 py-8">
|
<div dir={i18n.language === "ar" ? "rtl" : "ltr"} className="min-h-screen bg-gray-50 py-8">
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
<InteractiveBackground />
|
||||||
<div className="container mx-auto px-4 max-w-4xl">
|
<div className="relative z-10 container mx-auto px-4 max-w-2xl">
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="flex justify-between items-center mb-6">
|
||||||
initial={{ opacity: 0, y: -20 }}
|
<Link href="/" className="flex items-center gap-2 text-gray-600 hover:text-amber-600 transition-colors">
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="flex justify-between items-center mb-6"
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href="/"
|
|
||||||
className="flex items-center gap-2 text-gray-600 hover:text-amber-600 transition-colors"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="w-5 h-5" />
|
<ArrowLeft className="w-5 h-5" />
|
||||||
<span>{t('backToHome')}</span>
|
<span>{t("backToHome")}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div variants={fadeInUp} initial="initial" animate="animate" className="bg-white rounded-3xl shadow-xl overflow-hidden">
|
||||||
variants={fadeInUp}
|
|
||||||
initial="initial"
|
|
||||||
animate="animate"
|
|
||||||
className="bg-white rounded-3xl shadow-xl overflow-hidden"
|
|
||||||
>
|
|
||||||
<div className="h-48 bg-gradient-to-r from-amber-500 to-amber-600 relative overflow-hidden">
|
<div className="h-48 bg-gradient-to-r from-amber-500 to-amber-600 relative overflow-hidden">
|
||||||
<motion.div
|
<motion.div
|
||||||
className="absolute inset-0 bg-white/10"
|
className="absolute inset-0 bg-white/10"
|
||||||
@ -209,43 +177,34 @@ export default function ProfilePage() {
|
|||||||
<div className="flex justify-center -mt-16 mb-6">
|
<div className="flex justify-center -mt-16 mb-6">
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<div className="w-32 h-32 rounded-full border-4 border-white bg-gradient-to-br from-amber-500 to-amber-600 flex items-center justify-center text-white text-4xl font-bold shadow-xl overflow-hidden">
|
<div className="w-32 h-32 rounded-full border-4 border-white bg-gradient-to-br from-amber-500 to-amber-600 flex items-center justify-center text-white text-4xl font-bold shadow-xl overflow-hidden">
|
||||||
{avatarPreview ? (
|
{avatarPreview ? <img src={avatarPreview} alt={formData.name} className="w-full h-full object-cover" /> : formData.name?.charAt(0).toUpperCase() || "U"}
|
||||||
<img
|
|
||||||
src={avatarPreview}
|
|
||||||
alt={formData.name}
|
|
||||||
className="w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
formData.name?.charAt(0).toUpperCase() || "U"
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-center mb-6">
|
<div className="text-center mb-6">
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<h1 className="text-3xl font-bold text-gray-900">
|
<h1 className="text-3xl font-bold text-gray-900">{formData.name}</h1>
|
||||||
{formData.name}
|
|
||||||
</h1>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-center gap-2 text-gray-500 mt-2">
|
<div className="flex items-center justify-center gap-2 text-gray-500 mt-2">
|
||||||
<MapPin className="w-4 h-4" />
|
<MapPin className="w-4 h-4" />
|
||||||
<span>{formData.location || t('addressNotSpecified')}</span>
|
<span>{formData.location || t("addressNotSpecified")}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-center gap-2 text-gray-500 mt-1">
|
<div className="flex items-center justify-center gap-2 text-gray-500 mt-1">
|
||||||
<Calendar className="w-4 h-4" />
|
<Calendar className="w-4 h-4" />
|
||||||
<span> {t('member-since')} {formData.joinedDate}</span>
|
<span>
|
||||||
|
{" "}
|
||||||
|
{t("member-since")} {formData.joinedDate}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div className="bg-gray-50 p-4 rounded-xl group">
|
<div className="bg-gray-50 p-4 rounded-xl group">
|
||||||
<div className="flex justify-between items-start mb-2">
|
<div className="flex justify-between items-start mb-2">
|
||||||
<label className="text-sm font-medium text-gray-600">
|
<label className="text-sm font-medium text-gray-600">{t("emailLabel")}</label>
|
||||||
{t('emailLabel')}
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-gray-900">
|
<div className="flex items-center gap-2 text-gray-900">
|
||||||
<Mail className="w-5 h-5 text-gray-400" />
|
<Mail className="w-5 h-5 text-gray-400" />
|
||||||
@ -255,42 +214,36 @@ export default function ProfilePage() {
|
|||||||
|
|
||||||
<div className="bg-gray-50 p-4 rounded-xl group">
|
<div className="bg-gray-50 p-4 rounded-xl group">
|
||||||
<div className="flex justify-between items-start mb-2">
|
<div className="flex justify-between items-start mb-2">
|
||||||
<label className="text-sm font-medium text-gray-600">
|
<label className="text-sm font-medium text-gray-600">{t("phoneLabel")}</label>
|
||||||
{t('phoneLabel')}
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-gray-900">
|
<div className="flex items-center gap-2 text-gray-900">
|
||||||
<Phone className="w-5 h-5 text-gray-400" />
|
<Phone className="w-5 h-5 text-gray-400" />
|
||||||
<span>{formData.phone || t('not-specified')}</span>
|
<span>{formData.phone || t("not-specified")}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-gray-50 p-4 rounded-xl group">
|
<div className="bg-gray-50 p-4 rounded-xl group">
|
||||||
<div className="flex justify-between items-start mb-2">
|
<div className="flex justify-between items-start mb-2">
|
||||||
<label className="text-sm font-medium text-gray-600">
|
<label className="text-sm font-medium text-gray-600">{t("whatsapp-label")}</label>
|
||||||
{t('whatsapp-label')}
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-gray-900">
|
<div className="flex items-center gap-2 text-gray-900">
|
||||||
<MessageCircle className="w-5 h-5 text-gray-400" />
|
<MessageCircle className="w-5 h-5 text-gray-400" />
|
||||||
<span>{formData.whatsapp || t('not-specified')}</span>
|
<span>{formData.whatsapp || t("not-specified")}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-gray-50 p-4 rounded-xl">
|
<div className="bg-gray-50 p-4 rounded-xl">
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-2">
|
<label className="block text-sm font-medium text-gray-600 mb-2">{t("account-type")}</label>
|
||||||
{t('account-type')}
|
|
||||||
</label>
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{user?.role === "owner" ? (
|
{user?.role === "owner" ? (
|
||||||
<>
|
<>
|
||||||
<Building className="w-5 h-5 text-amber-500" />
|
<Building className="w-5 h-5 text-amber-500" />
|
||||||
<span className="text-gray-900">{t('property-owner')}</span>
|
<span className="text-gray-900">{t("property-owner")}</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Home className="w-5 h-5 text-blue-500" />
|
<Home className="w-5 h-5 text-blue-500" />
|
||||||
<span className="text-gray-900">{t('customer')}</span>
|
<span className="text-gray-900">{t("customer")}</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -348,17 +301,17 @@ export default function ProfilePage() {
|
|||||||
|
|
||||||
{user?.role === "owner" && (
|
{user?.role === "owner" && (
|
||||||
<div className="grid grid-cols-3 gap-4 mt-6">
|
<div className="grid grid-cols-3 gap-4 mt-6">
|
||||||
<div className="bg-amber-50 p-4 rounded-xl text-center">
|
<div className=" p-4 rounded-xl text-center" style={{backgroundColor:"#0f172a"}} >
|
||||||
<div className="text-2xl font-bold text-amber-600">12</div>
|
<div className="text-2xl font-bold text-amber-600">12</div>
|
||||||
<div className="text-xs text-gray-600">{t('propertiesListed')}</div>
|
<div className="text-xs text-gray-600">{t("propertiesListed")}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-blue-50 p-4 rounded-xl text-center">
|
<div className="bg-blue-50 p-4 rounded-xl text-center"style={{backgroundColor:"#0f172a"}} >
|
||||||
<div className="text-2xl font-bold text-blue-600">8</div>
|
<div className="text-2xl font-bold text-blue-600">8</div>
|
||||||
<div className="text-xs text-gray-600">{t('citiesCovered')}</div>
|
<div className="text-xs text-gray-600">{t("citiesCovered")}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-green-50 p-4 rounded-xl text-center">
|
<div className="bg-green-50 p-4 rounded-xl text-center" style={{backgroundColor:"#0f172a"}}>
|
||||||
<div className="text-2xl font-bold text-green-600">4.8</div>
|
<div className="text-2xl font-bold text-green-600">4.8</div>
|
||||||
<div className="text-xs text-gray-600">{t('customerSatisfaction')}</div>
|
<div className="text-xs text-gray-600">{t("customerSatisfaction")}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -128,7 +128,6 @@ const PropertyCard = ({ property, viewMode = 'grid', onLoginRequired }) => {
|
|||||||
const { isFavorite: checkFavorite, addFavorite, removeFavorite } = useFavorites();
|
const { isFavorite: checkFavorite, addFavorite, removeFavorite } = useFavorites();
|
||||||
const [favLoading, setFavLoading] = useState(false);
|
const [favLoading, setFavLoading] = useState(false);
|
||||||
const [currentImage, setCurrentImage] = useState(0);
|
const [currentImage, setCurrentImage] = useState(0);
|
||||||
|
|
||||||
const isFav = checkFavorite(property.id);
|
const isFav = checkFavorite(property.id);
|
||||||
|
|
||||||
const toggleFavorite = async (e) => {
|
const toggleFavorite = async (e) => {
|
||||||
@ -142,6 +141,7 @@ const PropertyCard = ({ property, viewMode = 'grid', onLoginRequired }) => {
|
|||||||
await addFavorite(property.id);
|
await addFavorite(property.id);
|
||||||
}
|
}
|
||||||
setFavLoading(false);
|
setFavLoading(false);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatCurrency = (amount) => {
|
const formatCurrency = (amount) => {
|
||||||
@ -187,6 +187,7 @@ const PropertyCard = ({ property, viewMode = 'grid', onLoginRequired }) => {
|
|||||||
src={property.images[currentImage] || '/property-placeholder.jpg'}
|
src={property.images[currentImage] || '/property-placeholder.jpg'}
|
||||||
alt={property.title}
|
alt={property.title}
|
||||||
fill
|
fill
|
||||||
|
loading='lazy'
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
/>
|
/>
|
||||||
{property.images.length > 1 && (
|
{property.images.length > 1 && (
|
||||||
|
|||||||
@ -1200,37 +1200,6 @@ export default function PropertyDetailsPage() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Contact Card */}
|
|
||||||
{/* {!isOwnProperty && (
|
|
||||||
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} className="bg-white rounded-2xl p-5 shadow-sm border border-gray-200">
|
|
||||||
<div className="flex items-center gap-2 mb-3">
|
|
||||||
<Phone className="w-4 h-4 text-amber-500" />
|
|
||||||
<h3 className="font-bold text-gray-900">{t("ownerInfo")}</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showContact && contactInfo ? (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center gap-2.5 p-2.5 bg-gray-50 rounded-xl">
|
|
||||||
<Phone className="w-4 h-4 text-gray-600 flex-shrink-0" />
|
|
||||||
<span className="font-medium text-gray-900 text-sm" dir="ltr">{contactInfo.phone || contactInfo.phoneNumber || '—'}</span>
|
|
||||||
</div>
|
|
||||||
{contactInfo.whatsAppNumber && (
|
|
||||||
<a href={`https://wa.me/${contactInfo.whatsAppNumber.replace(/[^0-9]/g, '')}`} target="_blank" rel="noopener noreferrer"
|
|
||||||
className="flex items-center gap-2.5 p-2.5 bg-green-50 rounded-xl hover:bg-green-100 transition-colors">
|
|
||||||
<MessageCircle className="w-4 h-4 text-green-600 flex-shrink-0" />
|
|
||||||
<span className="font-medium text-gray-900 text-sm" dir="ltr">{contactInfo.whatsAppNumber}</span>
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button onClick={fetchContactInfo}
|
|
||||||
className="w-full bg-gray-800 hover:bg-gray-900 text-white py-2.5 rounded-xl font-medium text-sm transition-colors flex items-center justify-center gap-2">
|
|
||||||
<Phone className="w-4 h-4" />
|
|
||||||
{t("showContactInfo")}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
)} */}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -68,8 +68,9 @@ export default function TenantRegisterPage() {
|
|||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const res = await addCustomer(payload, null, null);
|
const res = await addCustomer(payload, null, null);
|
||||||
|
console.log(res);
|
||||||
if (res.status === 200 || res.ok) {
|
if (res.status === 200 || res.ok) {
|
||||||
const apiMessage = res.message || res.data?.message;
|
const apiMessage = res.message || res.data?.message || res.data[0]?.message;
|
||||||
toast.success(apiMessage || t("register.accountCreated"), { duration: 4000 });
|
toast.success(apiMessage || t("register.accountCreated"), { duration: 4000 });
|
||||||
const loginRes = await loginWithEmail(formData.email, formData.password);
|
const loginRes = await loginWithEmail(formData.email, formData.password);
|
||||||
if (loginRes.status === 206) {
|
if (loginRes.status === 206) {
|
||||||
@ -83,7 +84,7 @@ export default function TenantRegisterPage() {
|
|||||||
toast.success(loginRes.message || t("register.loginSuccess"));
|
toast.success(loginRes.message || t("register.loginSuccess"));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const errMsg = res.message || res.data?.message || t("register.accountCreationFailed");
|
const errMsg = res.message || res.data?.message || res.data[0]?.message || t("register.accountCreationFailed");
|
||||||
toast.error(errMsg);
|
toast.error(errMsg);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -107,7 +108,14 @@ export default function TenantRegisterPage() {
|
|||||||
variants={staggerContainer}
|
variants={staggerContainer}
|
||||||
initial="initial"
|
initial="initial"
|
||||||
animate="animate"
|
animate="animate"
|
||||||
onSubmit={ step === 1? (e) => { e.preventDefault(); handleNextStep(); }: handleSubmit }
|
onSubmit={
|
||||||
|
step === 1
|
||||||
|
? (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleNextStep();
|
||||||
|
}
|
||||||
|
: handleSubmit
|
||||||
|
}
|
||||||
className="space-y-6"
|
className="space-y-6"
|
||||||
>
|
>
|
||||||
{step === 1 && <OwnerFormStepOne setErrors={setErrors} setFormData={setFormData} formData={formData} errors={errors} type={"customer"} />}
|
{step === 1 && <OwnerFormStepOne setErrors={setErrors} setFormData={setFormData} formData={formData} errors={errors} type={"customer"} />}
|
||||||
|
|||||||
@ -12,9 +12,7 @@ import {
|
|||||||
Send,
|
Send,
|
||||||
Loader2,
|
Loader2,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
AlertCircle,
|
|
||||||
User,
|
User,
|
||||||
MessageSquare,
|
|
||||||
Hash,
|
Hash,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { submitReport, submitReservationReport, submitSaleReport } from '../utils/api';
|
import { submitReport, submitReservationReport, submitSaleReport } from '../utils/api';
|
||||||
@ -138,14 +136,14 @@ export default function ReportsPage() {
|
|||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">{t('reports.subjectLabel')}</label>
|
<label className="block text-sm font-medium text-gray-700 mb-2">{t('reports.subjectLabel')}</label>
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
<div className="absolute inset-y-0 end-0 pe-3 flex items-center pointer-events-none">
|
||||||
<FileText className={`w-5 h-5 ${errors.subject ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
|
<FileText className={`w-5 h-5 ${errors.subject ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={generalForm.subject}
|
value={generalForm.subject}
|
||||||
onChange={(e) => { setGeneralForm({ ...generalForm, subject: e.target.value }); if (errors.subject) setErrors({ ...errors, subject: null }); }}
|
onChange={(e) => { setGeneralForm({ ...generalForm, subject: e.target.value }); if (errors.subject) setErrors({ ...errors, subject: null }); }}
|
||||||
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all ${errors.subject ? 'border-red-500' : 'border-gray-300'}`}
|
className={`w-full pe-12 ps-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all ${errors.subject ? 'border-red-500' : 'border-gray-300'}`}
|
||||||
placeholder={t('reports.subjectPlaceholder')}
|
placeholder={t('reports.subjectPlaceholder')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -184,14 +182,14 @@ export default function ReportsPage() {
|
|||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">{t('reports.reservationIdLabel')}</label>
|
<label className="block text-sm font-medium text-gray-700 mb-2">{t('reports.reservationIdLabel')}</label>
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
<div className="absolute inset-y-0 end-0 pe-3 flex items-center pointer-events-none">
|
||||||
<Hash className={`w-5 h-5 ${errors.reservationId ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
|
<Hash className={`w-5 h-5 ${errors.reservationId ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={reservationForm.reservationId}
|
value={reservationForm.reservationId}
|
||||||
onChange={(e) => { setReservationForm({ ...reservationForm, reservationId: e.target.value }); if (errors.reservationId) setErrors({ ...errors, reservationId: null }); }}
|
onChange={(e) => { setReservationForm({ ...reservationForm, reservationId: e.target.value }); if (errors.reservationId) setErrors({ ...errors, reservationId: null }); }}
|
||||||
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all ${errors.reservationId ? 'border-red-500' : 'border-gray-300'}`}
|
className={`w-full pe-12 ps-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all ${errors.reservationId ? 'border-red-500' : 'border-gray-300'}`}
|
||||||
placeholder={t('reports.reservationIdPlaceholder')}
|
placeholder={t('reports.reservationIdPlaceholder')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -250,14 +248,14 @@ export default function ReportsPage() {
|
|||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">{t('reports.saleIdLabel')}</label>
|
<label className="block text-sm font-medium text-gray-700 mb-2">{t('reports.saleIdLabel')}</label>
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
<div className="absolute inset-y-0 end-0 pe-3 flex items-center pointer-events-none">
|
||||||
<Hash className={`w-5 h-5 ${errors.saleId ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
|
<Hash className={`w-5 h-5 ${errors.saleId ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={saleForm.saleId}
|
value={saleForm.saleId}
|
||||||
onChange={(e) => { setSaleForm({ ...saleForm, saleId: e.target.value }); if (errors.saleId) setErrors({ ...errors, saleId: null }); }}
|
onChange={(e) => { setSaleForm({ ...saleForm, saleId: e.target.value }); if (errors.saleId) setErrors({ ...errors, saleId: null }); }}
|
||||||
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all ${errors.saleId ? 'border-red-500' : 'border-gray-300'}`}
|
className={`w-full pe-12 ps-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all ${errors.saleId ? 'border-red-500' : 'border-gray-300'}`}
|
||||||
placeholder={t('reports.saleIdPlaceholder')}
|
placeholder={t('reports.saleIdPlaceholder')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -314,7 +312,7 @@ export default function ReportsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 py-8">
|
<div className="min-h-screen py-8">
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
<div className="container mx-auto px-4 max-w-3xl">
|
<div className="container mx-auto px-4 max-w-3xl">
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@ -431,6 +431,7 @@ import {
|
|||||||
payDeposit,
|
payDeposit,
|
||||||
} from "../utils/api";
|
} from "../utils/api";
|
||||||
import { addPropertyRating } from "../utils/ratings";
|
import { addPropertyRating } from "../utils/ratings";
|
||||||
|
import Loading from "../loading";
|
||||||
|
|
||||||
const API_BASE =
|
const API_BASE =
|
||||||
process.env.NEXT_PUBLIC_API_URL || "https://45.93.137.91.nip.io/api";
|
process.env.NEXT_PUBLIC_API_URL || "https://45.93.137.91.nip.io/api";
|
||||||
@ -1713,12 +1714,11 @@ export default function UserReservationsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading)
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
<Loading/>
|
||||||
<Loader2 className="w-12 h-12 text-amber-500 animate-spin" />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">
|
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">
|
||||||
|
|||||||
@ -40,7 +40,7 @@ export default function SupportPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12" dir={i18n.language === "ar" ? "rtl" : "ltr"}>
|
<div className="min-h-screen bg-gradient-to-b py-12" dir={i18n.language === "ar" ? "rtl" : "ltr"}>
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
<div className="container mx-auto px-4 max-w-5xl">
|
<div className="container mx-auto px-4 max-w-5xl">
|
||||||
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="text-center mb-12">
|
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="text-center mb-12">
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from "react";
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from "framer-motion";
|
||||||
import { FileText, Shield, CheckCircle, Languages, Loader2, AlertCircle } from 'lucide-react';
|
import { FileText, Shield, CheckCircle, Languages, Loader2, AlertCircle } from "lucide-react";
|
||||||
import { getARTerms, getENTerms } from '../utils/api';
|
import { getARTerms, getENTerms } from "../utils/api";
|
||||||
|
|
||||||
const containerVariants = {
|
const containerVariants = {
|
||||||
hidden: { opacity: 0 },
|
hidden: { opacity: 0 },
|
||||||
@ -21,75 +21,63 @@ const itemVariants = {
|
|||||||
const FALLBACK_TERMS = {
|
const FALLBACK_TERMS = {
|
||||||
ar: [
|
ar: [
|
||||||
{
|
{
|
||||||
title: 'مقدمة',
|
title: "مقدمة",
|
||||||
description:
|
description: "مرحباً بك في منصة SweetHome. باستخدامك للمنصة، فإنك توافق على الالتزام بشروط الاستخدام هذه. إذا كنت لا توافق على أي جزء من هذه الشروط، يرجى عدم استخدام المنصة.",
|
||||||
'مرحباً بك في منصة SweetHome. باستخدامك للمنصة، فإنك توافق على الالتزام بشروط الاستخدام هذه. إذا كنت لا توافق على أي جزء من هذه الشروط، يرجى عدم استخدام المنصة.',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'استخدام المنصة',
|
title: "استخدام المنصة",
|
||||||
description:
|
description: "يُسمح باستخدام المنصة للأغراض المشروعة فقط. يلتزم المستخدم بعدم استخدام المنصة في أي نشاط غير قانوني.",
|
||||||
'يُسمح باستخدام المنصة للأغراض المشروعة فقط. يلتزم المستخدم بعدم استخدام المنصة في أي نشاط غير قانوني.',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'حقوق ومسؤوليات المالك',
|
title: "حقوق ومسؤوليات المالك",
|
||||||
description:
|
description: "يتحمل المالك مسؤولية دقة المعلومات المقدمة عن العقار بما في ذلك الصور والوصف والسعر والتوفر.",
|
||||||
'يتحمل المالك مسؤولية دقة المعلومات المقدمة عن العقار بما في ذلك الصور والوصف والسعر والتوفر.',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'حقوق ومسؤوليات المستأجر',
|
title: "حقوق ومسؤوليات المستأجر",
|
||||||
description:
|
description: "يلتزم المستأجر باستخدام العقار بطريقة مسؤولة وعدم التسبب في أي ضرر للممتلكات.",
|
||||||
'يلتزم المستأجر باستخدام العقار بطريقة مسؤولة وعدم التسبب في أي ضرر للممتلكات.',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'الدفع والعمولات',
|
title: "الدفع والعمولات",
|
||||||
description:
|
description: "تتقاضى المنصة عمولة على كل حصة ناجحة وفقاً للنسبة المحددة في وقت الحجز.",
|
||||||
'تتقاضى المنصة عمولة على كل حصة ناجحة وفقاً للنسبة المحددة في وقت الحجز.',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'خصوصية البيانات',
|
title: "خصوصية البيانات",
|
||||||
description:
|
description: "نحن نأخذ خصوصية بياناتك على محمل الجد. يتم جمع واستخدام البيانات الشخصية وفقاً لسياسة الخصوصية الخاصة بنا.",
|
||||||
'نحن نأخذ خصوصية بياناتك على محمل الجد. يتم جمع واستخدام البيانات الشخصية وفقاً لسياسة الخصوصية الخاصة بنا.',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
en: [
|
en: [
|
||||||
{
|
{
|
||||||
title: 'Introduction',
|
title: "Introduction",
|
||||||
description:
|
description: "Welcome to SweetHome. By using our platform, you agree to comply with these terms. If you do not agree, please do not use the platform.",
|
||||||
'Welcome to SweetHome. By using our platform, you agree to comply with these terms. If you do not agree, please do not use the platform.',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Platform Usage',
|
title: "Platform Usage",
|
||||||
description:
|
description: "The platform may only be used for lawful purposes. Users must not engage in any illegal activity.",
|
||||||
'The platform may only be used for lawful purposes. Users must not engage in any illegal activity.',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Owner Rights & Responsibilities',
|
title: "Owner Rights & Responsibilities",
|
||||||
description:
|
description: "Owners are responsible for the accuracy of property information including images, description, price, and availability.",
|
||||||
'Owners are responsible for the accuracy of property information including images, description, price, and availability.',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Tenant Rights & Responsibilities',
|
title: "Tenant Rights & Responsibilities",
|
||||||
description:
|
description: "Tenants must use the property responsibly and not cause any damage to the property.",
|
||||||
'Tenants must use the property responsibly and not cause any damage to the property.',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Payment & Commissions',
|
title: "Payment & Commissions",
|
||||||
description:
|
description: "The platform charges a commission on each successful booking according to the rate specified at the time of booking.",
|
||||||
'The platform charges a commission on each successful booking according to the rate specified at the time of booking.',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Data Privacy',
|
title: "Data Privacy",
|
||||||
description:
|
description: "We take your data privacy seriously. Personal data is collected and used in accordance with our Privacy Policy.",
|
||||||
'We take your data privacy seriously. Personal data is collected and used in accordance with our Privacy Policy.',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TermsPage() {
|
export default function TermsPage() {
|
||||||
const [terms, setTerms] = useState([]);
|
const [terms, setTerms] = useState([]);
|
||||||
const [language, setLanguage] = useState('ar');
|
const [language, setLanguage] = useState("ar");
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
@ -97,9 +85,9 @@ export default function TermsPage() {
|
|||||||
const fetchTerms = async () => {
|
const fetchTerms = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError("");
|
||||||
|
|
||||||
const fetcher = language === 'ar' ? getARTerms : getENTerms;
|
const fetcher = language === "ar" ? getARTerms : getENTerms;
|
||||||
const data = await fetcher();
|
const data = await fetcher();
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@ -115,14 +103,14 @@ export default function TermsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mapped = raw.map((item) => ({
|
const mapped = raw.map((item) => ({
|
||||||
title: item.title || item.name || '',
|
title: item.title || item.name || "",
|
||||||
description: item.description || item.content || item.body || item.text || '',
|
description: item.description || item.content || item.body || item.text || "",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setTerms(mapped);
|
setTerms(mapped);
|
||||||
} catch {
|
} catch {
|
||||||
setTerms(FALLBACK_TERMS[language]);
|
setTerms(FALLBACK_TERMS[language]);
|
||||||
setError('');
|
setError("");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -134,102 +122,62 @@ export default function TermsPage() {
|
|||||||
}, [language]);
|
}, [language]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div dir={language === "ar" ? "rtl" : "ltr"} className="min-h-screen bg-gradient-to-b py-12">
|
||||||
dir={language === 'ar' ? 'rtl' : 'ltr'}
|
|
||||||
className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12"
|
|
||||||
>
|
|
||||||
<div className="container mx-auto px-4 max-w-4xl">
|
<div className="container mx-auto px-4 max-w-4xl">
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="text-center mb-12">
|
||||||
initial={{ opacity: 0, y: -20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="text-center mb-12"
|
|
||||||
>
|
|
||||||
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-amber-100">
|
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-amber-100">
|
||||||
<FileText className="w-10 h-10 text-amber-600" />
|
<FileText className="w-10 h-10 text-amber-600" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-center gap-4 mb-4">
|
<div className="flex items-center justify-center gap-4 mb-4">
|
||||||
<h1 className="text-4xl font-bold text-gray-900">
|
<h1 className="text-4xl font-bold text-gray-900">{language === "ar" ? "شروط الاستخدام" : "Terms of Use"}</h1>
|
||||||
{language === 'ar' ? 'شروط الاستخدام' : 'Terms of Use'}
|
|
||||||
</h1>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setLanguage(language === 'ar' ? 'en' : 'ar')}
|
onClick={() => setLanguage(language === "ar" ? "en" : "ar")}
|
||||||
className="inline-flex items-center gap-2 rounded-full border border-amber-200 bg-white px-4 py-2 text-sm font-semibold text-gray-700 shadow-sm transition hover:shadow-md"
|
className="inline-flex items-center gap-2 rounded-full border border-amber-200 bg-white px-4 py-2 text-sm font-semibold text-gray-700 shadow-sm transition hover:shadow-md"
|
||||||
>
|
>
|
||||||
<Languages className="h-4 w-4" />
|
<Languages className="h-4 w-4" />
|
||||||
{language === 'ar' ? 'English' : 'العربية'}
|
{language === "ar" ? "English" : "العربية"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
||||||
{language === 'ar'
|
{language === "ar" ? "يرجى قراءة شروط الاستخدام التالية بعناية قبل استخدام المنصة" : "Please read the following terms of use carefully before using the platform"}
|
||||||
? 'يرجى قراءة شروط الاستخدام التالية بعناية قبل استخدام المنصة'
|
|
||||||
: 'Please read the following terms of use carefully before using the platform'}
|
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="flex items-center justify-center gap-3 mb-8 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-4 text-amber-800">
|
<div className="flex items-center justify-center gap-3 mb-8 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-4 text-amber-800">
|
||||||
<Loader2 className="h-5 w-5 animate-spin" />
|
<Loader2 className="h-5 w-5 animate-spin" />
|
||||||
<span>
|
<span>{language === "ar" ? "جاري تحميل شروط الاستخدام..." : "Loading terms of use..."}</span>
|
||||||
{language === 'ar'
|
|
||||||
? 'جاري تحميل شروط الاستخدام...'
|
|
||||||
: 'Loading terms of use...'}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} className="mb-8 rounded-2xl border border-red-200 bg-red-50 p-4 flex items-center gap-3 text-red-700">
|
||||||
initial={{ opacity: 0, y: -10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="mb-8 rounded-2xl border border-red-200 bg-red-50 p-4 flex items-center gap-3 text-red-700"
|
|
||||||
>
|
|
||||||
<AlertCircle className="h-5 w-5 shrink-0" />
|
<AlertCircle className="h-5 w-5 shrink-0" />
|
||||||
<span>{error}</span>
|
<span>{error}</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && terms.length === 0 && !error && (
|
{!loading && terms.length === 0 && !error && (
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-center py-16 text-gray-500">
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
className="text-center py-16 text-gray-500"
|
|
||||||
>
|
|
||||||
<FileText className="h-16 w-16 mx-auto mb-4 text-gray-300" />
|
<FileText className="h-16 w-16 mx-auto mb-4 text-gray-300" />
|
||||||
<p className="text-xl font-medium">
|
<p className="text-xl font-medium">{language === "ar" ? "لا توجد شروط استخدام متاحة حالياً" : "No terms of use available"}</p>
|
||||||
{language === 'ar'
|
|
||||||
? 'لا توجد شروط استخدام متاحة حالياً'
|
|
||||||
: 'No terms of use available'}
|
|
||||||
</p>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{terms.length > 0 && (
|
{terms.length > 0 && (
|
||||||
<motion.div
|
<motion.div variants={containerVariants} initial="hidden" animate="visible" className="space-y-6">
|
||||||
variants={containerVariants}
|
|
||||||
initial="hidden"
|
|
||||||
animate="visible"
|
|
||||||
className="space-y-6"
|
|
||||||
>
|
|
||||||
{terms.map((term, index) => (
|
{terms.map((term, index) => (
|
||||||
<motion.div
|
<motion.div key={index} variants={itemVariants} className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow">
|
||||||
key={index}
|
|
||||||
variants={itemVariants}
|
|
||||||
className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow"
|
|
||||||
>
|
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<div className="w-10 h-10 bg-amber-100 rounded-xl flex items-center justify-center shrink-0 mt-1">
|
<div className="w-10 h-10 bg-amber-100 rounded-xl flex items-center justify-center shrink-0 mt-1">
|
||||||
<Shield className="w-5 h-5 text-amber-600" />
|
<Shield className="w-5 h-5 text-amber-600" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
{term.title && (
|
{term.title && <h2 className="text-xl font-bold text-gray-900 mb-3">{term.title}</h2>}
|
||||||
<h2 className="text-xl font-bold text-gray-900 mb-3">{term.title}</h2>
|
<p className="text-gray-600 leading-relaxed whitespace-pre-wrap">{term.description}</p>
|
||||||
)}
|
|
||||||
<p className="text-gray-600 leading-relaxed whitespace-pre-wrap">
|
|
||||||
{term.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@ -237,21 +185,14 @@ export default function TermsPage() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.6 }} className="mt-8 bg-amber-50 rounded-2xl border border-amber-200 p-6 flex items-start gap-4">
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
transition={{ delay: 0.6 }}
|
|
||||||
className="mt-8 bg-amber-50 rounded-2xl border border-amber-200 p-6 flex items-start gap-4"
|
|
||||||
>
|
|
||||||
<CheckCircle className="w-6 h-6 text-amber-600 shrink-0 mt-0.5" />
|
<CheckCircle className="w-6 h-6 text-amber-600 shrink-0 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-bold text-amber-800 mb-1">
|
<p className="font-bold text-amber-800 mb-1">{language === "ar" ? "آخر تحديث" : "Last Updated"}</p>
|
||||||
{language === 'ar' ? 'آخر تحديث' : 'Last Updated'}
|
|
||||||
</p>
|
|
||||||
<p className="text-amber-700">
|
<p className="text-amber-700">
|
||||||
{language === 'ar'
|
{language === "ar"
|
||||||
? 'تم آخر تحديث لشروط الاستخدام في 1 مايو 2026. يرجى مراجعة هذه الصفحة بشكل دوري للاطلاع على أي تغييرات.'
|
? "تم آخر تحديث لشروط الاستخدام في 1 مايو 2026. يرجى مراجعة هذه الصفحة بشكل دوري للاطلاع على أي تغييرات."
|
||||||
: 'Last updated on May 1, 2026. Please review this page periodically for any changes.'}
|
: "Last updated on May 1, 2026. Please review this page periodically for any changes."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@ -345,9 +345,10 @@ export async function editSaleProperty(id, data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function addSaleProperty(data) {
|
export async function addSaleProperty(data) {
|
||||||
|
|
||||||
return apiFetch("/SaleProperties/AddSaleProperty", {
|
return apiFetch("/SaleProperties/AddSaleProperty", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: data,
|
body: data ,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -446,7 +447,7 @@ async function multipartAuthFetch(endpoint, formData) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addOwner(data, frontImage = null, backImage = null) {
|
export async function addOwner(data, frontImage = null, backImage = null, licenseImage = null) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
formData.append("FirstName", data.firstName || data.FirstName || "");
|
formData.append("FirstName", data.firstName || data.FirstName || "");
|
||||||
@ -467,6 +468,7 @@ export async function addOwner(data, frontImage = null, backImage = null) {
|
|||||||
|
|
||||||
if (frontImage) formData.append("FrontIdCarImagePath", frontImage);
|
if (frontImage) formData.append("FrontIdCarImagePath", frontImage);
|
||||||
if (backImage) formData.append("RearIdCarImagePath", backImage);
|
if (backImage) formData.append("RearIdCarImagePath", backImage);
|
||||||
|
if (licenseImage) formData.append("RearIdCarImagePath", licenseImage);
|
||||||
return multipartAuthFetch("/Owner/Add", formData);
|
return multipartAuthFetch("/Owner/Add", formData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
54
middleware.js
Normal file
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