Added translation to all site with edit style
All checks were successful
Build frontend / build (push) Successful in 1m40s

This commit is contained in:
Rahaf
2026-07-01 15:43:58 +03:00
parent 3052209df8
commit dab5b62fb7
56 changed files with 5136 additions and 3190 deletions

View File

@ -157,7 +157,7 @@ export default function ClientLayout({ children }) {
<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"></div>
<p className="text-gray-600">جاري التحميل...</p>
<p className="text-gray-600" suppressHydrationWarning>{t("loading")}</p>
</div>
</div>
);
@ -202,7 +202,7 @@ export default function ClientLayout({ children }) {
<path d="M11 2a3 3 0 0 0-3 3v6.5a.5.5 0 0 0 1 0V5a2 2 0 1 1 4 0v6.5a.5.5 0 0 0 1 0V5a3 3 0 0 0-3-3z"/>
<path d="M1.5 12.5A1.5 1.5 0 0 0 3 14h10a1.5 1.5 0 0 0 0-3H3a1.5 1.5 0 0 0-1.5 1.5z"/>
</svg>
<span className="text-sm font-semibold">تحميل التطبيق</span>
<span className="text-sm font-semibold">{t("downloadApp")}</span>
<svg className="w-4 h-4 transition-transform group-hover:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div className="absolute right-0 mt-2 w-64 bg-white rounded-xl shadow-xl border border-gray-200 overflow-hidden z-50 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 translate-y-2 group-hover:translate-y-0">
@ -215,8 +215,8 @@ export default function ClientLayout({ children }) {
</svg>
</div>
<div>
<p className="font-semibold text-gray-900 text-sm">Android</p>
<p className="text-xs text-green-600">تحميل APK</p>
<p className="font-semibold text-gray-900 text-sm">{t("android")}</p>
<p className="text-xs text-green-600">{t("downloadAPK")}</p>
</div>
</a>
<div className="flex items-center gap-3 px-4 py-3 rounded-lg opacity-50 cursor-not-allowed">
@ -226,16 +226,16 @@ export default function ClientLayout({ children }) {
</svg>
</div>
<div>
<p className="font-semibold text-gray-400 text-sm">iOS</p>
<p className="text-xs text-gray-400">قريباً</p>
<p className="font-semibold text-gray-400 text-sm">{t("ios")}</p>
<p className="text-xs text-gray-400">{t("comingSoon")}</p>
</div>
</div>
</div>
</div>
</div>
<NavLink light={isHomePage} href="/">الرئيسية</NavLink>
<NavLink light={isHomePage} href="/properties">عقاراتنا</NavLink>
<NavLink light={isHomePage} href="/">{t("home")}</NavLink>
<NavLink light={isHomePage} href="/properties">{t("ourProperties")}</NavLink>
{isOwner && (
@ -243,25 +243,19 @@ export default function ClientLayout({ children }) {
<NavLink light={isHomePage} href="/owner/properties">
<span className="flex items-center gap-2">
<Building className="w-4 h-4" />
عقاراتي
{t("myProperties")}
</span>
</NavLink>
<NavLink light={isHomePage} href="/owner/reservations">
<span className="flex items-center gap-2">
<Calendar className="w-4 h-4" />
الحجوزات
{t("reservations")}
</span>
</NavLink>
{/* <NavLink light={isHomePage} href="/owner/calendar">
<span className="flex items-center gap-2">
<CalendarDays className="w-4 h-4" />
التقويم
</span>
</NavLink> */}
<NavLink light={isHomePage} href="/owner/profits">
<span className="flex items-center gap-2">
<TrendingUp className="w-4 h-4" />
الأرباح
{t("profits")}
</span>
</NavLink>
</>
@ -272,13 +266,13 @@ export default function ClientLayout({ children }) {
<NavLink light={isHomePage} href="/login">
<span className="flex items-center gap-2">
<LogIn className="w-4 h-4" />
تسجيل الدخول
{t("login")}
</span>
</NavLink>
<NavLink light={isHomePage} href="/auth/choose-role">
<span className="flex items-center gap-2">
<UserPlus className="w-4 h-4" />
إنشاء حساب
{t("createAccount")}
</span>
</NavLink>
</>
@ -322,14 +316,14 @@ export default function ClientLayout({ children }) {
</div>
<div>
<p className="font-bold">
{user?.name || "مستخدم"}
{user?.name || t("userLabel")}
</p>
<p className="text-xs text-amber-100">
{user?.email || ""}
</p>
<p className="text-xs text-amber-100 mt-1">
{UserRoleLabels[user?.role] || 'زائر'}
</p>
<p className="text-xs text-amber-100 mt-1">
{t(UserRoleLabels[user?.role] ? `userRole.${user?.role.toLowerCase()}` : 'visitor')}
</p>
</div>
</div>
</div>
@ -342,9 +336,9 @@ export default function ClientLayout({ children }) {
>
<UserCircle className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">الملف الشخصي</p>
<p className="font-medium">{t("profile")}</p>
<p className="text-xs text-gray-500">
عرض وتعديل معلوماتك
{t("viewAndEditInfo")}
</p>
</div>
</Link>
@ -356,9 +350,9 @@ export default function ClientLayout({ children }) {
>
<Heart className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">المفضلة</p>
<p className="font-medium">{t("favorites")}</p>
<p className="text-xs text-gray-500">
العقارات المحفوظة
{t("savedProperties")}
</p>
</div>
</Link>
@ -370,9 +364,9 @@ export default function ClientLayout({ children }) {
>
<CalendarDays className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">عقاراتي المحجوزة</p>
<p className="font-medium">{t("myBookedProperties")}</p>
<p className="text-xs text-gray-500">
عرض وتقييم العقارات المحجوزة
{t("viewAndRateBooked")}
</p>
</div>
</Link>
@ -384,9 +378,9 @@ export default function ClientLayout({ children }) {
>
<Star className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">تقييماتي</p>
<p className="font-medium">{t("myRatings")}</p>
<p className="text-xs text-gray-500">
التقييمات التي تلقيتها
{t("ratingsReceived")}
</p>
</div>
</Link>
@ -398,9 +392,9 @@ export default function ClientLayout({ children }) {
>
<Settings className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">الإعدادات</p>
<p className="font-medium">{t("settings")}</p>
<p className="text-xs text-gray-500">
إعدادات الحساب والأمان
{t("accountAndSecurity")}
</p>
</div>
</Link>
@ -416,9 +410,9 @@ export default function ClientLayout({ children }) {
>
<Building className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">عقاراتي</p>
<p className="font-medium">{t("myProperties")}</p>
<p className="text-xs text-gray-500">
إدارة عقاراتك
{t("manageYourProperties")}
</p>
</div>
</Link>
@ -430,9 +424,9 @@ export default function ClientLayout({ children }) {
>
<PlusCircle className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">إضافة عقار</p>
<p className="font-medium">{t("addProperty")}</p>
<p className="text-xs text-gray-500">
أضف عقاراً جديداً
{t("addNewProperty")}
</p>
</div>
</Link>
@ -444,9 +438,9 @@ export default function ClientLayout({ children }) {
>
<Calendar className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">الحجوزات</p>
<p className="font-medium">{t("reservations")}</p>
<p className="text-xs text-gray-500">
إدارة حجوزاتك
{t("manageReservations")}
</p>
</div>
</Link>
@ -458,9 +452,9 @@ export default function ClientLayout({ children }) {
>
<CalendarDays className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">التقويم</p>
<p className="font-medium">{t("calendar")}</p>
<p className="text-xs text-gray-500">
جدول توفر العقارات
{t("propertyAvailability")}
</p>
</div>
</Link>
@ -472,9 +466,9 @@ export default function ClientLayout({ children }) {
>
<TrendingUp className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">الأرباح</p>
<p className="font-medium">{t("profits")}</p>
<p className="text-xs text-gray-500">
إحصائيات وأرباح
{t("statisticsAndProfits")}
</p>
</div>
</Link>
@ -486,9 +480,9 @@ export default function ClientLayout({ children }) {
>
<DollarSign className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">دفتر الحسابات</p>
<p className="font-medium">{t("accountBook")}</p>
<p className="text-xs text-gray-500">
سجل المعاملات المالية
{t("financialRecords")}
</p>
</div>
</Link>
@ -506,9 +500,9 @@ export default function ClientLayout({ children }) {
>
<Calendar className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">حجوزاتي</p>
<p className="font-medium">{t("myBookings")}</p>
<p className="text-xs text-gray-500">
عرض حجوزاتك
{t("viewBookings")}
</p>
</div>
</Link>
@ -520,9 +514,9 @@ export default function ClientLayout({ children }) {
>
<CalendarDays className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">عقاراتي المحجوزة</p>
<p className="font-medium">{t("myBookedProperties")}</p>
<p className="text-xs text-gray-500">
عرض وتقييم العقارات المحجوزة
{t("viewAndRateBooked")}
</p>
</div>
</Link>
@ -534,9 +528,9 @@ export default function ClientLayout({ children }) {
>
<Star className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">تقييماتي</p>
<p className="font-medium">{t("myRatings")}</p>
<p className="text-xs text-gray-500">
التقييمات التي تلقيتها
{t("ratingsReceived")}
</p>
</div>
</Link>
@ -548,9 +542,9 @@ export default function ClientLayout({ children }) {
>
<FileText className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">البلاغات</p>
<p className="font-medium">{t("reports")}</p>
<p className="text-xs text-gray-500">
تقديم ومتابعة البلاغات
{t("submitAndTrackReports")}
</p>
</div>
</Link>
@ -565,9 +559,9 @@ export default function ClientLayout({ children }) {
>
<LogOut className="w-5 h-5" />
<div>
<p className="font-medium">تسجيل الخروج</p>
<p className="font-medium">{t("logout")}</p>
<p className="text-xs text-red-400">
إنهاء الجلسة الحالية
{t("endSession")}
</p>
</div>
</button>
@ -642,19 +636,19 @@ export default function ClientLayout({ children }) {
{/* Download App - Mobile */}
<div className={`border-t my-2 ${isHomePage ? "border-white/10" : "border-gray-200"}`}></div>
<p className={`px-3 py-1 text-xs font-medium ${isHomePage ? "text-white/50" : "text-gray-400"}`}>تحميل التطبيق</p>
<p className={`px-3 py-1 text-xs font-medium ${isHomePage ? "text-white/50" : "text-gray-400"}`}>{t("downloadApp")}</p>
<a href="/files/SweetHome.apk" download onClick={closeMobileMenu}
className={`flex items-center gap-2 px-3 py-2 rounded-md transition-colors ${isHomePage ? "text-green-400 hover:bg-white/10" : "text-green-600 hover:bg-green-50"}`}>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
<path d="M2.76 3.061a.5.5 0 0 1 .679.2l1.283 2.352A8.9 8.9 0 0 1 8 5a8.9 8.9 0 0 1 3.278.613l1.283-2.352a.5.5 0 1 1 .878.478l-1.252 2.295C14.475 7.266 16 9.477 16 12H0c0-2.523 1.525-4.734 3.813-5.966L2.56 3.74a.5.5 0 0 1 .2-.678ZM5 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2m6 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/>
</svg>
<span className="text-sm font-medium">Android - تحميل APK</span>
<span className="text-sm font-medium">{t("android")} - {t("downloadAPK")}</span>
</a>
<div className="flex items-center gap-2 px-3 py-2 rounded-md text-gray-400 cursor-not-allowed opacity-50">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M11.182.008C11.148-.03 9.923.023 8.857 1.18c-1.066 1.156-.902 2.482-.878 2.516.024.034 1.52.087 2.475-1.258.955-1.345.762-2.391.728-2.43Zm3.314 11.733c-.048-.096-2.325-1.234-2.113-3.422.212-2.189 1.675-2.789 1.698-2.854.023-.065-.597-.79-1.254-1.157a3.692 3.692 0 0 0-1.563-.434c-.108-.003-.483-.095-1.254.116-.508.139-1.653.589-1.968.607-.316.018-1.256-.522-2.267-.665-.647-.125-1.333.131-1.824.328-.49.196-1.422.754-2.074 2.237-.652 1.482-.311 3.83-.067 4.56.244.729.625 1.924 1.273 2.796.576.984 1.34 1.667 1.659 1.899.319.232 1.219.385 1.843.067.502-.308 1.408-.485 1.766-.472.357.013 1.061.154 1.782.539.571.197 1.111.115 1.652-.105.541-.221 1.324-1.059 2.238-2.758.347-.79.505-1.217.473-1.282Z"/>
</svg>
<span className="text-sm">iOS - قريباً</span>
<span className="text-sm">{t("ios")} - {t("comingSoon")}</span>
</div>
<div className={`border-t my-2 ${isHomePage ? "border-white/10" : "border-gray-200"}`}></div>
@ -665,7 +659,7 @@ export default function ClientLayout({ children }) {
>
<span className="flex items-center gap-2">
<Building className="w-4 h-4" />
عقاراتي
{t("myProperties")}
</span>
</MobileNavLink>
<MobileNavLink light={isHomePage}
@ -674,7 +668,7 @@ export default function ClientLayout({ children }) {
>
<span className="flex items-center gap-2">
<Calendar className="w-4 h-4" />
الحجوزات
{t("reservations")}
</span>
</MobileNavLink>
<MobileNavLink light={isHomePage}
@ -683,7 +677,7 @@ export default function ClientLayout({ children }) {
>
<span className="flex items-center gap-2">
<CalendarDays className="w-4 h-4" />
التقويم
{t("calendar")}
</span>
</MobileNavLink>
<MobileNavLink light={isHomePage}
@ -692,7 +686,7 @@ export default function ClientLayout({ children }) {
>
<span className="flex items-center gap-2">
<TrendingUp className="w-4 h-4" />
الأرباح
{t("profits")}
</span>
</MobileNavLink>
</>
@ -704,7 +698,7 @@ export default function ClientLayout({ children }) {
<MobileNavLink light={isHomePage} href="/login" onClick={closeMobileMenu}>
<span className="flex items-center gap-2">
<LogIn className="w-4 h-4" />
تسجيل الدخول
{t("login")}
</span>
</MobileNavLink>
<MobileNavLink light={isHomePage}
@ -713,7 +707,7 @@ export default function ClientLayout({ children }) {
>
<span className="flex items-center gap-2">
<UserPlus className="w-4 h-4" />
إنشاء حساب
{t("createAccount")}
</span>
</MobileNavLink>
</>
@ -740,18 +734,18 @@ export default function ClientLayout({ children }) {
<main
className={`${!isAuthPage && !isProfilePage && !isAuthenticated ? "pt-20" : ""} flex-1 flex flex-col ${isHomePage ? "bg-transparent" : "bg-gray-50"} ${currentLanguage === "ar" ? "text-right" : "text-left"} ${isHomePage ? "relative" : ""} ${isAuthenticated && !isAuthPage ? "pb-20" : ""}`}
>
{!isAuthPage && (
{!isAuthPage && isAuthenticated && (
<div className={`flex items-center justify-between px-4 py-2 sticky top-0 z-20 bg-transparent backdrop-blur-3xl ${isHomePage ? "" : "border-b border-gray-200/20"}`}>
<div className="flex items-center gap-2">
<span className={`text-sm font-semibold px-3 py-1 rounded-lg ${!isAuthenticated ? (isHomePage ? 'bg-white/20 text-white' : 'bg-gray-200 text-gray-600') : isOwner ? (isHomePage ? 'bg-amber-500/30 text-amber-200' : 'bg-amber-100 text-amber-700') : (isHomePage ? 'bg-blue-500/30 text-blue-200' : 'bg-blue-100 text-blue-700')}`}>
{!isAuthenticated ? 'زائر' : isOwner ? 'مالك' : 'عميل'}
{!isAuthenticated ? t('visitor') : isOwner ? t('owner') : t('customer')}
</span>
</div>
<div className="flex items-center gap-3">
<Link href="/favorites" className={`relative group flex items-center justify-center w-9 h-9 rounded-full transition-colors hover:bg-white/10 ${pathname === '/favorites' ? 'text-amber-400' : 'nav-icon'}`}>
<Heart className="w-5 h-5" />
<div className="absolute -bottom-8 left-1/2 -translate-x-1/2 bg-gray-800 text-white text-xs rounded-lg px-2 py-1 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none shadow-lg">
المفضلة
{t("favorites")}
</div>
</Link>
<Link href="/notifications" className={`relative group flex items-center justify-center w-9 h-9 rounded-full transition-colors hover:bg-white/10 ${pathname === '/notifications' ? 'text-amber-400' : 'nav-icon'}`}>
@ -762,19 +756,25 @@ export default function ClientLayout({ children }) {
</div>
)}
<div className="absolute -bottom-8 left-1/2 -translate-x-1/2 bg-gray-800 text-white text-xs rounded-lg px-2 py-1 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none shadow-lg">
الإشعارات
{t("notifications")}
</div>
</Link>
<Link href="/settings" className={`relative group flex items-center justify-center w-9 h-9 rounded-full transition-colors hover:bg-white/10 ${pathname === '/settings' ? 'text-amber-400' : 'nav-icon'}`}>
<Settings className="w-5 h-5" />
<div className="absolute -bottom-8 left-1/2 -translate-x-1/2 bg-gray-800 text-white text-xs rounded-lg px-2 py-1 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none shadow-lg">
الإعدادات
{t("settings")}
</div>
</Link>
<button onClick={toggleTheme} className="relative group flex items-center justify-center w-9 h-9 rounded-full transition-colors hover:bg-white/10 nav-icon">
{theme === 'dark' ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
<div className="absolute -bottom-8 left-1/2 -translate-x-1/2 bg-gray-800 text-white text-xs rounded-lg px-2 py-1 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none shadow-lg">
{theme === 'dark' ? 'وضع نهاري' : 'وضع ليلي'}
{theme === 'dark' ? t('lightMode') : t('darkMode')}
</div>
</button>
<button onClick={() => changeLanguage(currentLanguage === 'ar' ? 'en' : 'ar')} className="relative group flex items-center justify-center w-9 h-9 rounded-full transition-colors hover:bg-white/10 nav-icon">
<Globe className="w-5 h-5" />
<div className="absolute -bottom-8 left-1/2 -translate-x-1/2 bg-gray-800 text-white text-xs rounded-lg px-2 py-1 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none shadow-lg">
{currentLanguage === 'ar' ? t('switchToEnglish') : t('switchToArabic')}
</div>
</button>
</div>

View File

@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { motion } from 'framer-motion';
import { useTranslation } from 'react-i18next';
import {
Mail,
Phone,
@ -21,6 +22,7 @@ import { sendEmailOTP, sendPhoneOTP, verifyEmail, verifyPhone } from '../utils/a
export default function AccountVerificationPage() {
const router = useRouter();
const { t } = useTranslation();
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
@ -49,10 +51,10 @@ export default function AccountVerificationPage() {
setSendingEmailOTP(true);
try {
await sendEmailOTP();
toast.success('تم إرسال رمز التحقق إلى بريدك الإلكتروني');
toast.success(t('accountVerification.emailOtpSent'));
setShowEmailInput(true);
} catch (err) {
toast.error(err.message || 'فشل إرسال رمز التحقق');
toast.error(err.message || t('accountVerification.otpSendFailed'));
} finally {
setSendingEmailOTP(false);
}
@ -60,7 +62,7 @@ export default function AccountVerificationPage() {
const handleVerifyEmail = async () => {
if (!emailCode.trim()) {
toast.error('الرجاء إدخال رمز التحقق');
toast.error(t('accountVerification.otpRequired'));
return;
}
setVerifyingEmail(true);
@ -68,9 +70,9 @@ export default function AccountVerificationPage() {
await verifyEmail(emailCode.trim());
setEmailVerified(true);
setShowEmailInput(false);
toast.success('تم التحقق من البريد الإلكتروني بنجاح');
toast.success(t('accountVerification.emailVerifiedSuccess'));
} catch (err) {
toast.error(err.message || 'رمز التحقق غير صحيح');
toast.error(err.message || t('accountVerification.otpInvalid'));
} finally {
setVerifyingEmail(false);
}
@ -80,10 +82,10 @@ export default function AccountVerificationPage() {
setSendingPhoneOTP(true);
try {
await sendPhoneOTP();
toast.success('تم إرسال رمز التحقق إلى هاتفك');
toast.success(t('accountVerification.phoneOtpSent'));
setShowPhoneInput(true);
} catch (err) {
toast.error(err.message || 'فشل إرسال رمز التحقق');
toast.error(err.message || t('accountVerification.otpSendFailed'));
} finally {
setSendingPhoneOTP(false);
}
@ -91,7 +93,7 @@ export default function AccountVerificationPage() {
const handleVerifyPhone = async () => {
if (!phoneCode.trim()) {
toast.error('الرجاء إدخال رمز التحقق');
toast.error(t('accountVerification.otpRequired'));
return;
}
setVerifyingPhone(true);
@ -99,9 +101,9 @@ export default function AccountVerificationPage() {
await verifyPhone(phoneCode.trim());
setPhoneVerified(true);
setShowPhoneInput(false);
toast.success('تم التحقق من رقم الهاتف بنجاح');
toast.success(t('accountVerification.phoneVerifiedSuccess'));
} catch (err) {
toast.error(err.message || 'رمز التحقق غير صحيح');
toast.error(err.message || t('accountVerification.otpInvalid'));
} finally {
setVerifyingPhone(false);
}
@ -144,7 +146,7 @@ export default function AccountVerificationPage() {
>
<ChevronLeft className="w-5 h-5" />
</Link>
<h1 className="text-2xl font-bold text-gray-900">التحقق من الحساب</h1>
<h1 className="text-2xl font-bold text-gray-900">{t('accountVerification.title')}</h1>
</motion.div>
<motion.div
@ -156,22 +158,22 @@ export default function AccountVerificationPage() {
<motion.div variants={itemVariants} className="bg-white rounded-2xl shadow-sm overflow-hidden">
<div className="px-6 py-4 border-b border-gray-100 flex items-center gap-3">
<Mail className="w-5 h-5 text-amber-600" />
<h2 className="text-lg font-semibold text-gray-800">البريد الإلكتروني</h2>
<h2 className="text-lg font-semibold text-gray-800">{t('accountVerification.emailSection')}</h2>
{emailVerified && (
<span className="flex items-center gap-1 text-xs text-green-600 bg-green-50 px-2 py-0.5 rounded-full">
<CheckCircle className="w-3 h-3" />
موثق
{t('accountVerification.verified')}
</span>
)}
</div>
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<div>
<p className="text-sm text-gray-600">{user?.email || 'بريد إلكتروني غير مسجل'}</p>
<p className="text-sm text-gray-600">{user?.email || t('accountVerification.emailNotRegistered')}</p>
<p className="text-xs text-gray-400 mt-1">
{emailVerified
? 'بريدك الإلكتروني موثق ويمكنك استخدامه لتسجيل الدخول'
: 'يرجى توثيق بريدك الإلكتروني لحماية حسابك'}
? t('accountVerification.emailVerifiedDesc')
: t('accountVerification.emailVerifyDesc')}
</p>
</div>
{!emailVerified && !showEmailInput && (
@ -185,7 +187,7 @@ export default function AccountVerificationPage() {
) : (
<Send className="w-4 h-4" />
)}
{sendingEmailOTP ? 'جاري الإرسال...' : 'إرسال رمز التحقق'}
{sendingEmailOTP ? t('accountVerification.sending') : t('accountVerification.sendOtp')}
</button>
)}
</div>
@ -196,13 +198,13 @@ export default function AccountVerificationPage() {
animate={{ opacity: 1, height: 'auto' }}
className="space-y-3"
>
<p className="text-xs text-gray-500">أدخل الرمز المكون من 6 أرقام الذي تم إرساله إلى بريدك الإلكتروني</p>
<p className="text-xs text-gray-500">{t('accountVerification.enter6DigitCode')}</p>
<div className="flex gap-2">
<input
type="text"
value={emailCode}
onChange={(e) => setEmailCode(e.target.value)}
placeholder="رمز التحقق"
placeholder={t('accountVerification.otpPlaceholder')}
maxLength={6}
className="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none text-center text-lg tracking-widest"
/>
@ -216,7 +218,7 @@ export default function AccountVerificationPage() {
) : (
<Key className="w-4 h-4" />
)}
تحقق
{t('accountVerification.verify')}
</button>
</div>
<button
@ -224,7 +226,7 @@ export default function AccountVerificationPage() {
disabled={sendingEmailOTP}
className="text-xs text-amber-600 hover:text-amber-700"
>
إعادة إرسال الرمز
{t('accountVerification.resendCode')}
</button>
</motion.div>
)}
@ -232,7 +234,7 @@ export default function AccountVerificationPage() {
{emailVerified && (
<div className="flex items-center gap-2 text-green-600 bg-green-50 px-4 py-3 rounded-xl">
<CheckCircle className="w-5 h-5" />
<span className="text-sm font-medium">تم توثيق البريد الإلكتروني بنجاح</span>
<span className="text-sm font-medium">{t('accountVerification.emailVerifiedSuccess')}</span>
</div>
)}
</div>
@ -241,22 +243,22 @@ export default function AccountVerificationPage() {
<motion.div variants={itemVariants} className="bg-white rounded-2xl shadow-sm overflow-hidden">
<div className="px-6 py-4 border-b border-gray-100 flex items-center gap-3">
<Phone className="w-5 h-5 text-amber-600" />
<h2 className="text-lg font-semibold text-gray-800">رقم الهاتف</h2>
<h2 className="text-lg font-semibold text-gray-800">{t('accountVerification.phoneSection')}</h2>
{phoneVerified && (
<span className="flex items-center gap-1 text-xs text-green-600 bg-green-50 px-2 py-0.5 rounded-full">
<CheckCircle className="w-3 h-3" />
موثق
{t('accountVerification.verified')}
</span>
)}
</div>
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<div>
<p className="text-sm text-gray-600">{user?.phone || 'رقم هاتف غير مسجل'}</p>
<p className="text-sm text-gray-600">{user?.phone || t('accountVerification.phoneNotRegistered')}</p>
<p className="text-xs text-gray-400 mt-1">
{phoneVerified
? 'رقم هاتفك موثق ويمكنك استخدامه لتسجيل الدخول'
: 'يرجى توثيق رقم هاتفك لحماية حسابك'}
? t('accountVerification.phoneVerifiedDesc')
: t('accountVerification.phoneVerifyDesc')}
</p>
</div>
{!phoneVerified && !showPhoneInput && (
@ -270,7 +272,7 @@ export default function AccountVerificationPage() {
) : (
<Send className="w-4 h-4" />
)}
{sendingPhoneOTP ? 'جاري الإرسال...' : 'إرسال رمز التحقق'}
{sendingPhoneOTP ? t('accountVerification.sending') : t('accountVerification.sendOtp')}
</button>
)}
</div>
@ -281,13 +283,13 @@ export default function AccountVerificationPage() {
animate={{ opacity: 1, height: 'auto' }}
className="space-y-3"
>
<p className="text-xs text-gray-500">أدخل الرمز المكون من 6 أرقام الذي تم إرساله إلى هاتفك</p>
<p className="text-xs text-gray-500">{t('accountVerification.enter6DigitCodePhone')}</p>
<div className="flex gap-2">
<input
type="text"
value={phoneCode}
onChange={(e) => setPhoneCode(e.target.value)}
placeholder="رمز التحقق"
placeholder={t('accountVerification.otpPlaceholder')}
maxLength={6}
className="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none text-center text-lg tracking-widest"
/>
@ -301,7 +303,7 @@ export default function AccountVerificationPage() {
) : (
<Key className="w-4 h-4" />
)}
تحقق
{t('accountVerification.verify')}
</button>
</div>
<button
@ -309,7 +311,7 @@ export default function AccountVerificationPage() {
disabled={sendingPhoneOTP}
className="text-xs text-amber-600 hover:text-amber-700"
>
إعادة إرسال الرمز
{t('accountVerification.resendCode')}
</button>
</motion.div>
)}
@ -317,7 +319,7 @@ export default function AccountVerificationPage() {
{phoneVerified && (
<div className="flex items-center gap-2 text-green-600 bg-green-50 px-4 py-3 rounded-xl">
<CheckCircle className="w-5 h-5" />
<span className="text-sm font-medium">تم توثيق رقم الهاتف بنجاح</span>
<span className="text-sm font-medium">{t('accountVerification.phoneVerifiedSuccess')}</span>
</div>
)}
</div>
@ -326,20 +328,20 @@ export default function AccountVerificationPage() {
<motion.div variants={itemVariants} className="bg-gradient-to-br from-amber-500 to-amber-600 rounded-2xl p-6 text-white">
<div className="flex items-center gap-3 mb-3">
<Shield className="w-6 h-6" />
<h3 className="text-lg font-semibold">لماذا يجب توثيق حسابك؟</h3>
<h3 className="text-lg font-semibold">{t('accountVerification.whyVerify')}</h3>
</div>
<ul className="space-y-2 text-sm text-amber-50">
<li className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 flex-shrink-0" />
حماية حسابك من الوصول غير المصرح به
{t('accountVerification.reason1')}
</li>
<li className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 flex-shrink-0" />
استعادة كلمة المرور بسهولة في حال فقدانها
{t('accountVerification.reason2')}
</li>
<li className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 flex-shrink-0" />
زيادة الثقة مع مالكي العقارات والمستأجرين
{t('accountVerification.reason3')}
</li>
</ul>
</motion.div>

View File

@ -3,8 +3,10 @@
import { motion } from 'framer-motion';
import Link from 'next/link';
import { Home, Building, Briefcase, ArrowLeft } from 'lucide-react';
import { useTranslation } from 'react-i18next';
export default function ChooseRolePage() {
const { t, i18n } = useTranslation();
const containerVariants = {
hidden: { opacity: 0 },
visible: {
@ -35,7 +37,7 @@ export default function ChooseRolePage() {
};
return (
<div 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 bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center p-4 relative overflow-hidden">
<div className="absolute inset-0 overflow-hidden">
{[...Array(20)].map((_, i) => (
<motion.div
@ -77,7 +79,7 @@ export default function ChooseRolePage() {
<motion.div whileHover={{ x: -5 }}>
<ArrowLeft className="w-4 h-4" />
</motion.div>
<span>العودة للرئيسية</span>
<span>{t('backToHome')}</span>
</Link>
</motion.div>
@ -89,10 +91,10 @@ export default function ChooseRolePage() {
className="text-4xl md:text-5xl font-bold text-white mb-4"
animate={floatingAnimation}
>
مرحباً بك في SweetHome
{t('chooseRole.welcomeToSweetHome')}
</motion.h1>
<p className="text-xl text-gray-400">
اختر نوع الحساب المناسب لك
{t('chooseRole.subtitle')}
</p>
</motion.div>
@ -123,23 +125,23 @@ export default function ChooseRolePage() {
<Building className="w-10 h-10 text-white" />
</motion.div>
<h2 className="text-3xl font-bold mb-3">صاحب عقار</h2>
<h2 className="text-3xl font-bold mb-3">{t('chooseRole.owner')}</h2>
<p className="text-amber-100 mb-6">
تريد عرض عقار للإيجار أو البيع؟ انضم كمالك عقار وابدأ الآن
{t('chooseRole.ownerDesc')}
</p>
<ul className="space-y-2 text-sm text-amber-100">
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" />
إضافة عقارات غير محدودة
{t('chooseRole.ownerFeature1')}
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" />
إدارة الحجوزات بسهولة
{t('chooseRole.ownerFeature2')}
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" />
تواصل مباشر مع المستأجرين
{t('chooseRole.ownerFeature3')}
</li>
</ul>
@ -148,7 +150,7 @@ export default function ChooseRolePage() {
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
إنشاء حساب مالك
{t('chooseRole.createOwnerAccount')}
</motion.button>
</div>
</div>
@ -181,23 +183,23 @@ export default function ChooseRolePage() {
<Home className="w-10 h-10 text-white" />
</motion.div>
<h2 className="text-3xl font-bold mb-3">مستأجر / مشتري</h2>
<h2 className="text-3xl font-bold mb-3">{t('chooseRole.tenant')}</h2>
<p className="text-blue-100 mb-6">
تبحث عن عقار للإيجار أو الشراء؟ انضم كعميل وابحث عن منزل أحلامك
{t('chooseRole.tenantDesc')}
</p>
<ul className="space-y-2 text-sm text-blue-100">
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" />
آلاف العقارات المتاحة
{t('chooseRole.tenantFeature1')}
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" />
بحث متقدم بفلاتر ذكية
{t('chooseRole.tenantFeature2')}
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" />
حجز وإستئجار فوري
{t('chooseRole.tenantFeature3')}
</li>
</ul>
@ -206,7 +208,7 @@ export default function ChooseRolePage() {
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
إنشاء حساب مستأجر
{t('chooseRole.createTenantAccount')}
</motion.button>
</div>
</div>
@ -239,23 +241,23 @@ export default function ChooseRolePage() {
<Briefcase className="w-10 h-10 text-white" />
</motion.div>
<h2 className="text-3xl font-bold mb-3">وكيل عقاري</h2>
<h2 className="text-3xl font-bold mb-3">{t('chooseRole.agent')}</h2>
<p className="text-purple-100 mb-6">
تدير عقارات للغير؟ انضم كوسيط عقاري واحصل على فرص مميزة
{t('chooseRole.agentDesc')}
</p>
<ul className="space-y-2 text-sm text-purple-100">
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" />
إدارة عقارات العملاء
{t('chooseRole.agentFeature1')}
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" />
عمولات وأرباح مضمونة
{t('chooseRole.agentFeature2')}
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" />
لوحة تحكم متكاملة
{t('chooseRole.agentFeature3')}
</li>
</ul>
@ -264,7 +266,7 @@ export default function ChooseRolePage() {
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
إنشاء حساب وكيل
{t('chooseRole.createAgentAccount')}
</motion.button>
</div>
</div>
@ -276,12 +278,12 @@ export default function ChooseRolePage() {
variants={itemVariants}
className="text-center text-gray-400 mt-8"
>
لديك حساب بالفعل؟{' '}
{t('chooseRole.alreadyHaveAccount')}{' '}
<Link
href="/login"
className="text-amber-400 hover:text-amber-300 font-medium transition-colors"
>
تسجيل الدخول
{t('login')}
</Link>
</motion.p>
</motion.div>

View File

@ -3,12 +3,14 @@
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { motion } from 'framer-motion';
import { useTranslation } from 'react-i18next';
import { ShieldAlert, LogOut, MessageSquare, Send, Loader2 } from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
import AuthService from '../services/AuthService';
import { sendGeneralReport } from '../utils/api';
export default function BlockedPage() {
const { t } = useTranslation();
const router = useRouter();
const [form, setForm] = useState({ subject: '', body: '' });
const [isSubmitting, setIsSubmitting] = useState(false);
@ -28,7 +30,7 @@ export default function BlockedPage() {
event.preventDefault();
if (!form.subject.trim() || !form.body.trim()) {
toast.error('يرجى تعبئة الموضوع والرسالة');
toast.error(t('blocked.fillAllFields'));
return;
}
@ -38,9 +40,9 @@ export default function BlockedPage() {
await sendGeneralReport(form.subject.trim(), form.body.trim());
setIsSent(true);
setForm({ subject: '', body: '' });
toast.success('تم إرسال طلب الدعم بنجاح');
toast.success(t('blocked.reportSent'));
} catch (error) {
toast.error('حدث خطأ أثناء إرسال طلب الدعم. حاول مرة أخرى');
toast.error(t('blocked.sendError'));
} finally {
setIsSubmitting(false);
}
@ -63,9 +65,9 @@ export default function BlockedPage() {
>
<ShieldAlert className="w-12 h-12 text-red-600" />
</motion.div>
<h1 className="text-4xl font-bold text-gray-900 mb-3">الحساب محظور</h1>
<h1 className="text-4xl font-bold text-gray-900 mb-3">{t('blocked.title')}</h1>
<p className="text-gray-600 text-lg max-w-2xl mx-auto">
تم تقييد وصولك إلى التطبيق. يمكنك تسجيل الخروج أو مراسلة دعم العملاء للمساعدة في حل المشكلة.
{t('blocked.description')}
</p>
</div>
@ -80,9 +82,9 @@ export default function BlockedPage() {
<div className="w-14 h-14 bg-red-50 rounded-2xl flex items-center justify-center mb-6">
<LogOut className="w-7 h-7 text-red-600" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-3">تسجيل الخروج</h2>
<h2 className="text-2xl font-bold text-gray-900 mb-3">{t('blocked.logoutTitle')}</h2>
<p className="text-gray-600 leading-7">
إنهاء الجلسة الحالية وإزالة بيانات الدخول من هذا الجهاز.
{t('blocked.logoutDesc')}
</p>
</div>
@ -92,13 +94,13 @@ export default function BlockedPage() {
className="mt-8 w-full bg-red-600 hover:bg-red-700 text-white rounded-2xl py-4 font-bold transition-colors flex items-center justify-center gap-3"
>
<LogOut className="w-5 h-5" />
تسجيل الخروج
{t('blocked.logoutButton')}
</button>
</motion.div>
<motion.div
initial={{ opacity: 0, x: 24 }}
animate={{ opacity: 1, x: 0 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="bg-white rounded-3xl shadow-sm border border-gray-200 p-8"
>
@ -107,30 +109,30 @@ export default function BlockedPage() {
<MessageSquare className="w-7 h-7 text-amber-600" />
</div>
<div>
<h2 className="text-2xl font-bold text-gray-900">مراسلة دعم العملاء</h2>
<p className="text-gray-600 mt-1">أرسل تفاصيل المشكلة وسنقوم بمراجعتها.</p>
<h2 className="text-2xl font-bold text-gray-900">{t('blocked.contactSupportTitle')}</h2>
<p className="text-gray-600 mt-1">{t('blocked.contactSupportDesc')}</p>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-5">
<div>
<label className="block text-sm font-bold text-gray-700 mb-2">الموضوع</label>
<label className="block text-sm font-bold text-gray-700 mb-2">{t('subject')}</label>
<input
type="text"
value={form.subject}
onChange={(event) => updateField('subject', event.target.value)}
placeholder="اكتب موضوع الرسالة"
placeholder={t('blocked.subjectPlaceholder')}
className="w-full px-4 py-3 bg-white border border-gray-200 rounded-2xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400"
/>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-2">الرسالة</label>
<label className="block text-sm font-bold text-gray-700 mb-2">{t('blocked.messageLabel')}</label>
<textarea
value={form.body}
onChange={(event) => updateField('body', event.target.value)}
rows={6}
placeholder="اشرح المشكلة بالتفصيل"
placeholder={t('blocked.messagePlaceholder')}
className="w-full px-4 py-3 bg-white border border-gray-200 rounded-2xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400 resize-none"
/>
</div>
@ -143,17 +145,17 @@ export default function BlockedPage() {
{isSubmitting ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
جاري الإرسال...
{t('blocked.sending')}
</>
) : isSent ? (
<>
<Send className="w-5 h-5" />
تم الإرسال
{t('blocked.sentButton')}
</>
) : (
<>
<Send className="w-5 h-5" />
إرسال الرسالة
{t('blocked.sendButton')}
</>
)}
</button>
@ -163,4 +165,4 @@ export default function BlockedPage() {
</motion.div>
</div>
);
}
}

View File

@ -2,6 +2,7 @@
import { useState, useEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useTranslation } from 'react-i18next';
import {
Home, Star, MapPin, Calendar, Clock, Check, X, Loader2,
User, MessageCircle, ChevronDown, Image as ImageIcon,
@ -14,27 +15,29 @@ import PropertyRatingForm from '../components/ratings/PropertyRatingForm';
const STATUS_MAP = ['pending', 'ownerConfirmed', 'depositPaid', 'depositConfirmed', 'completed', 'cancelled'];
const STATUS_CONFIG = {
pending: { label: 'قيد الانتظار', color: 'bg-yellow-100 text-yellow-800 border-yellow-300' },
ownerConfirmed: { label: 'مؤكد من المالك', color: 'bg-blue-100 text-blue-800 border-blue-300' },
depositPaid: { label: 'تم دفع السلفة', color: 'bg-orange-100 text-orange-800 border-orange-300' },
depositConfirmed: { label: 'مؤكد', color: 'bg-green-100 text-green-800 border-green-300' },
completed: { label: 'منتهي', color: 'bg-teal-100 text-teal-800 border-teal-300' },
cancelled: { label: 'ملغي', color: 'bg-red-100 text-red-800 border-red-300' },
pending: { color: 'bg-yellow-100 text-yellow-800 border-yellow-300' },
ownerConfirmed: { color: 'bg-blue-100 text-blue-800 border-blue-300' },
depositPaid: { color: 'bg-orange-100 text-orange-800 border-orange-300' },
depositConfirmed: { color: 'bg-green-100 text-green-800 border-green-300' },
completed: { color: 'bg-teal-100 text-teal-800 border-teal-300' },
cancelled: { color: 'bg-red-100 text-red-800 border-red-300' },
};
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api';
function StatusBadge({ code }) {
const { t } = useTranslation();
const key = STATUS_MAP[code] || 'pending';
const cfg = STATUS_CONFIG[key];
return (
<span className={`inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium border ${cfg.color}`}>
<Clock className="w-3 h-3" /> {cfg.label}
<Clock className="w-3 h-3" /> {t(`bookingStatus.${key}`)}
</span>
);
}
function ReservationCard({ reservation: r, onRate }) {
const { t } = useTranslation();
const isCompleted = STATUS_MAP[r.status] === 'completed';
const imgSrc = r.propertyImage || r._prop?.images?.[0] || (r.propertyInfo?.images?.[0]);
const imageUrl = imgSrc ? `${API_BASE}${imgSrc}` : null;
@ -49,7 +52,7 @@ function ReservationCard({ reservation: r, onRate }) {
<div className="flex justify-between items-start">
<div className="flex items-center gap-2">
<Home className="w-5 h-5 text-amber-500" />
<span className="font-semibold text-gray-900">عقار #{r.propertyId || r.id}</span>
<span className="font-semibold text-gray-900">{t('propertyId', { id: r.propertyId || r.id })}</span>
</div>
<StatusBadge code={r.status} />
</div>
@ -73,38 +76,38 @@ function ReservationCard({ reservation: r, onRate }) {
{(beds > 0 || baths > 0) && (
<div className="flex gap-3 text-sm text-gray-600">
{beds > 0 && <span>{beds} غرف</span>}
{baths > 0 && <span>{baths} حمامات</span>}
{beds > 0 && <span>{beds} {t('rooms')}</span>}
{baths > 0 && <span>{baths} {t('bathrooms')}</span>}
</div>
)}
<div className="grid grid-cols-2 gap-3">
<div className="bg-gray-50 p-3 rounded-xl text-center">
<Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1" />
<div className="text-xs text-gray-500">من</div>
<div className="text-xs text-gray-500">{t('from')}</div>
<div className="text-sm font-medium">{new Date(r.startDate).toLocaleDateString('ar')}</div>
</div>
<div className="bg-gray-50 p-3 rounded-xl text-center">
<Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1" />
<div className="text-xs text-gray-500">إلى</div>
<div className="text-xs text-gray-500">{t('to')}</div>
<div className="text-sm font-medium">{new Date(r.endDate).toLocaleDateString('ar')}</div>
</div>
</div>
<div className="bg-amber-50 p-3 rounded-xl text-center">
<div className="text-lg font-bold text-amber-600">{r.totalPrice?.toLocaleString() ?? '—'}</div>
<div className="text-xs text-gray-500">السعر الإجمالي</div>
<div className="text-xs text-gray-500">{t('totalPrice')}</div>
</div>
<div className="flex items-center gap-2 text-xs text-gray-400">
<User className="w-3 h-3" />
<span>رقم الحجز: #{r.id}</span>
<span>{t('reservationNumber', { id: r.id })}</span>
</div>
{isCompleted && (
<button onClick={onRate}
className="w-full bg-amber-500 hover:bg-amber-600 text-white py-2.5 rounded-xl text-sm font-medium transition flex items-center justify-center gap-2">
<Star className="w-4 h-4" /> تقييم العقار
<Star className="w-4 h-4" /> {t('rateProperty')}
</button>
)}
</div>
@ -113,6 +116,7 @@ function ReservationCard({ reservation: r, onRate }) {
}
export default function BookedPropertiesPage() {
const { t } = useTranslation();
const [reservations, setReservations] = useState([]);
const [loading, setLoading] = useState(true);
const [ratingReservation, setRatingReservation] = useState(null);
@ -120,7 +124,7 @@ export default function BookedPropertiesPage() {
useEffect(() => {
if (!AuthService.getToken()) {
toast.error('يرجى تسجيل الدخول أولاً');
toast.error(t('loginRequired'));
setLoading(false);
return;
}
@ -133,7 +137,7 @@ export default function BookedPropertiesPage() {
setReservations(Array.isArray(data) ? data : []);
} catch (err) {
console.error(err);
toast.error('فشل تحميل الحجوزات');
toast.error(t('failedToLoadBookings'));
setReservations([]);
} finally {
setLoading(false);
@ -155,14 +159,14 @@ export default function BookedPropertiesPage() {
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="mb-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">العقارات المحجوزة</h1>
<p className="text-gray-600">لديك {reservations.length} حجز</p>
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t('bookedProperties')}</h1>
<p className="text-gray-600">{t('youHaveBookings', { count: reservations.length })}</p>
</div>
{reservations.length > 0 && (
<button onClick={() => setExpandedId(expandedId ? null : 'all')}
className="flex items-center gap-1 text-sm text-amber-600 hover:text-amber-700 transition">
<ChevronDown className={`w-4 h-4 transition-transform ${expandedId ? 'rotate-180' : ''}`} />
{expandedId ? 'طي الكل' : 'عرض الكل'}
{expandedId ? t('collapseAll') : t('showAll')}
</button>
)}
</div>
@ -172,8 +176,8 @@ export default function BookedPropertiesPage() {
<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">
<Home className="w-16 h-16 text-amber-500 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-900 mb-2">لا توجد حجوزات</h3>
<p className="text-gray-500">لم تقم بحجز أي عقار حتى الآن</p>
<h3 className="text-xl font-bold text-gray-900 mb-2">{t('noBookings')}</h3>
<p className="text-gray-500">{t('noBookingsDesc')}</p>
</motion.div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
@ -188,8 +192,8 @@ export default function BookedPropertiesPage() {
className="mt-8 bg-amber-50 border border-amber-200 rounded-xl p-4 flex items-start gap-3">
<MessageCircle className="w-5 h-5 text-amber-600 flex-shrink-0 mt-0.5" />
<div>
<p className="text-amber-800 font-medium">تقييم العقارات</p>
<p className="text-amber-600 text-sm">يمكنك تقييم العقارات المنتهية حجزها لمساعدة المستأجرين الآخرين</p>
<p className="text-amber-800 font-medium">{t('rateProperties')}</p>
<p className="text-amber-600 text-sm">{t('ratePropertiesDesc')}</p>
</div>
</motion.div>
)}
@ -209,7 +213,7 @@ export default function BookedPropertiesPage() {
</button>
<PropertyRatingForm
reservationId={ratingReservation.id}
onSuccess={() => { setRatingReservation(null); toast.success('تم إرسال التقييم بنجاح!'); }}
onSuccess={() => { setRatingReservation(null); toast.success(t('ratingSubmitted')); }}
onCancel={() => setRatingReservation(null)}
/>
</div>

View File

@ -5,11 +5,13 @@ import { motion } from 'framer-motion';
import { useRouter } from 'next/navigation';
import toast, { Toaster } from 'react-hot-toast';
import { Lock, Eye, EyeOff, ArrowLeft, Shield } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { changePassword } from '../utils/api';
import AuthService from '../services/AuthService';
export default function ChangePasswordPage() {
const router = useRouter();
const { t } = useTranslation();
const [form, setForm] = useState({ oldPassword: '', newPassword: '', confirmPassword: '' });
const [show, setShow] = useState({ old: false, new: false, confirm: false });
const [isLoading, setIsLoading] = useState(false);
@ -22,28 +24,28 @@ export default function ChangePasswordPage() {
e.preventDefault();
if (!AuthService.isAuthenticated()) {
toast.error('يرجى تسجيل الدخول أولاً');
toast.error(t('changePassword.loginRequired'));
router.push('/login');
return;
}
if (form.newPassword !== form.confirmPassword) {
toast.error('كلمة المرور الجديدة وتأكيدها غير متطابقتين');
toast.error(t('changePassword.passwordsDoNotMatch'));
return;
}
if (form.newPassword.length < 6) {
toast.error('كلمة المرور الجديدة يجب أن تكون 6 أحرف على الأقل');
toast.error(t('validation.passwordMinLength'));
return;
}
setIsLoading(true);
try {
await changePassword(form.oldPassword, form.newPassword);
toast.success('تم تغيير كلمة المرور بنجاح');
toast.success(t('changePassword.changeSuccess'));
setTimeout(() => router.push('/profile'), 1200);
} catch (err) {
toast.error(err?.message || 'فشل تغيير كلمة المرور');
toast.error(err?.message || t('changePassword.changeFailed'));
} finally {
setIsLoading(false);
}
@ -71,7 +73,7 @@ export default function ChangePasswordPage() {
className="flex items-center gap-2 text-gray-600 hover:text-amber-600 transition-colors"
>
<ArrowLeft className="w-5 h-5" />
<span>العودة للملف الشخصي</span>
<span>{t('changePassword.backToProfile')}</span>
</button>
</motion.div>
@ -90,7 +92,7 @@ export default function ChangePasswordPage() {
animate={{ y: 0, opacity: 1 }}
className="text-3xl font-bold text-white mb-2"
>
تغيير كلمة المرور
{t('changePassword.title')}
</motion.h1>
<motion.p
initial={{ y: 20, opacity: 0 }}
@ -98,14 +100,14 @@ export default function ChangePasswordPage() {
transition={{ delay: 0.1 }}
className="text-amber-100"
>
أدخل كلمة المرور الحالية والجديدة
{t('changePassword.subtitle')}
</motion.p>
</div>
<form onSubmit={handleSubmit} className="p-8 space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
كلمة المرور الحالية
{t('changePassword.currentPasswordLabel')}
</label>
<div className="relative">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
@ -116,7 +118,7 @@ export default function ChangePasswordPage() {
value={form.oldPassword}
onChange={handleChange('oldPassword')}
className={inputClass}
placeholder="أدخل كلمة المرور الحالية"
placeholder={t('changePassword.currentPasswordPlaceholder')}
required
/>
<button
@ -131,7 +133,7 @@ export default function ChangePasswordPage() {
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
كلمة المرور الجديدة
{t('changePassword.newPasswordLabel')}
</label>
<div className="relative">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
@ -142,7 +144,7 @@ export default function ChangePasswordPage() {
value={form.newPassword}
onChange={handleChange('newPassword')}
className={inputClass}
placeholder="أدخل كلمة المرور الجديدة"
placeholder={t('changePassword.newPasswordPlaceholder')}
required
minLength={6}
/>
@ -158,7 +160,7 @@ export default function ChangePasswordPage() {
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
تأكيد كلمة المرور الجديدة
{t('changePassword.confirmNewPasswordLabel')}
</label>
<div className="relative">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
@ -169,7 +171,7 @@ export default function ChangePasswordPage() {
value={form.confirmPassword}
onChange={handleChange('confirmPassword')}
className={inputClass}
placeholder="أعد إدخال كلمة المرور الجديدة"
placeholder={t('changePassword.confirmPasswordPlaceholder')}
required
minLength={6}
/>
@ -193,10 +195,10 @@ export default function ChangePasswordPage() {
{isLoading ? (
<div className="flex items-center justify-center gap-2">
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
<span>جاري الحفظ...</span>
<span>{t('changePassword.saving')}</span>
</div>
) : (
'تغيير كلمة المرور'
t('changePassword.changePassword')
)}
</motion.button>
</form>

View File

@ -4,8 +4,10 @@ import Link from "next/link";
import { usePathname } from "next/navigation";
import { Home, Building, Calendar, CreditCard, Briefcase, BookOpen } from "lucide-react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
export default function BottomNav({ isOwner, isOwnerOrAgent }) {
const { t } = useTranslation();
const pathname = usePathname();
const [isMounted, setIsMounted] = useState(false);
const bookingsHref = isOwner ? "/owner/reservations" : "/reservations";
@ -15,12 +17,12 @@ export default function BottomNav({ isOwner, isOwnerOrAgent }) {
}, []);
const items = [
{ href: "/", label: "الرئيسية", icon: Home },
{ href: "/properties", label: "عقاراتنا", icon: Building },
...(isOwnerOrAgent ? [{ href: "/owner/properties", label: "عقاراتي", icon: Briefcase }] : []),
{ href: bookingsHref, label: "الحجوزات", icon: Calendar },
{ href: "/payments", label: "المدفوعات", icon: CreditCard },
...(isOwnerOrAgent ? [{ href: "/owner/account-book", label: "دفتر الحسابات", icon: BookOpen }] : []),
{ href: "/", label: t("home"), icon: Home },
{ href: "/properties", label: t("ourProperties"), icon: Building },
...(isOwnerOrAgent ? [{ href: "/owner/properties", label: t("myProperties"), icon: Briefcase }] : []),
{ href: bookingsHref, label: t("reservations"), icon: Calendar },
{ href: "/payments", label: t("payments"), icon: CreditCard },
...(isOwnerOrAgent ? [{ href: "/owner/account-book", label: t("accountBook"), icon: BookOpen }] : []),
];
const isActive = (href) => {

View File

@ -4,11 +4,13 @@ import { useState } from 'react';
import { motion } from 'framer-motion';
import Link from 'next/link';
import { Heart, Bell, CreditCard, Settings } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useFavorites } from '@/app/contexts/FavoritesContext';
import { useNotifications } from '@/app/contexts/NotificationsContext';
import AuthService from '@/app/services/AuthService';
export default function FloatingSidebar({ isRTL }) {
const { t } = useTranslation();
const { favorites } = useFavorites();
const { unreadCount } = useNotifications();
const [tooltip, setTooltip] = useState(null);
@ -88,7 +90,7 @@ export default function FloatingSidebar({ isRTL }) {
)}
</div>
</Link>
{renderTooltip('favorites', 'المفضلة')}
{renderTooltip('favorites', t('favorites'))}
</motion.div>
<motion.div
className="relative group"
@ -116,7 +118,7 @@ export default function FloatingSidebar({ isRTL }) {
)}
</div>
</Link>
{renderTooltip('notifications', 'الإشعارات')}
{renderTooltip('notifications', t('notifications'))}
</motion.div>
<motion.div
className="relative group"
@ -133,7 +135,7 @@ export default function FloatingSidebar({ isRTL }) {
>
<CreditCard className="w-7 h-7 text-gray-600 transition-colors group-hover:text-amber-600" />
</Link>
{renderTooltip('payments', 'المدفوعات')}
{renderTooltip('payments', t('payments'))}
</motion.div>
<motion.div
className="relative group"
@ -150,7 +152,7 @@ export default function FloatingSidebar({ isRTL }) {
>
<Settings className="w-7 h-7 text-gray-600 transition-colors group-hover:text-amber-600" />
</Link>
{renderTooltip('settings', 'الإعدادات')}
{renderTooltip('settings', t('settings'))}
</motion.div>
</div>
</motion.div>

View File

@ -21,46 +21,46 @@ export default function HeroSearch({ onSearch, isAuthenticated }) {
const [showLoginDialog, setShowLoginDialog] = useState(false);
const cities = [
{ id: 'all', label: 'جميع المدن' },
{ id: 'دمشق', label: 'دمشق' },
{ id: 'حلب', label: 'حلب' },
{ id: 'حمص', label: 'حمص' },
{ id: 'اللاذقية', label: 'اللاذقية' },
{ id: 'درعا', label: 'درعا' }
{ id: 'all', label: t('allCities') },
{ id: 'دمشق', label: t('damascus') },
{ id: 'حلب', label: t('aleppo') },
{ id: 'حمص', label: t('homs') },
{ id: 'اللاذقية', label: t('latakia') },
{ id: 'درعا', label: t('daraa') }
];
const propertyTypes = [
{ id: 'all', label: 'الكل' },
{ id: 'apartment', label: 'شقق سكنية' },
{ id: 'studio', label: 'استوديو' },
{ id: 'commercial', label: 'عقار تجاري' },
{ id: 'villa', label: 'فيلا / مزرعة' }
{ id: 'all', label: t('all') },
{ id: 'apartment', label: t('residentialApartments') },
{ id: 'studio', label: t('studio') },
{ id: 'commercial', label: t('commercialProperty') },
{ id: 'villa', label: t('villaFarm') }
];
const priceRanges = [
{ id: 'all', label: 'جميع الأسعار' },
{ id: '0-500', label: 'أقل من 50$' },
{ id: '500-1000', label: '50$ - 100$' },
{ id: '1000-2000', label: '100$ - 200$' },
{ id: '2000-3000', label: '200$ - 300$' },
{ id: '3000+', label: 'أكثر من 300$' }
{ id: 'all', label: t('allPrices') },
{ id: '0-500', label: t('priceRange1') },
{ id: '500-1000', label: t('priceRange2') },
{ id: '1000-2000', label: t('priceRange3') },
{ id: '2000-3000', label: t('priceRange4') },
{ id: '3000+', label: t('priceRange5') }
];
const identityTypes = [
{ id: 'syrian', label: 'هوية سورية' },
{ id: 'passport', label: 'جواز سفر' }
{ id: 'syrian', label: t('syrianId') },
{ id: 'passport', label: t('passport') }
];
const ownerSources = [
{ id: 'all', label: 'الكل' },
{ id: 'owner', label: 'من المالك' },
{ id: 'agency', label: 'من مكتب عقاري' }
{ id: 'all', label: t('all') },
{ id: 'owner', label: t('fromOwner') },
{ id: 'agency', label: t('fromAgency') }
];
const rentPeriods = [
{ id: 'all', label: 'الكل' },
{ id: 'daily', label: 'إيجار يومي' },
{ id: 'monthly', label: 'إيجار شهري' }
{ id: 'all', label: t('all') },
{ id: 'daily', label: t('dailyRent') },
{ id: 'monthly', label: t('monthlyRent') }
];
const handleTabClick = (tab) => {
@ -190,7 +190,7 @@ export default function HeroSearch({ onSearch, isAuthenticated }) {
<div>
<label className="block text-sm font-medium text-white mb-2">
<div className="flex items-center gap-1">
نوع الوثيقة
{t("identityTypeLabel")}
</div>
</label>
<select
@ -213,7 +213,7 @@ export default function HeroSearch({ onSearch, isAuthenticated }) {
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mt-4">
<div>
<label className="block text-sm font-medium text-white mb-2">مصدر العرض</label>
<label className="block text-sm font-medium text-white mb-2">{t("ownerSourceLabel")}</label>
<select
value={filters.ownerSource}
onChange={(e) => setFilters({ ...filters, ownerSource: e.target.value })}
@ -236,7 +236,7 @@ export default function HeroSearch({ onSearch, isAuthenticated }) {
{activeTab === 'rent' && (
<div>
<label className="block text-sm font-medium text-white mb-2">نوع الإيجار</label>
<label className="block text-sm font-medium text-white mb-2">{t("rentTypeLabel")}</label>
<select
value={filters.rentPeriod}
onChange={(e) => setFilters({ ...filters, rentPeriod: e.target.value })}
@ -266,7 +266,7 @@ export default function HeroSearch({ onSearch, isAuthenticated }) {
onChange={(e) => setFilters({ ...filters, availableToday: e.target.checked })}
className="w-5 h-5 text-amber-500 rounded border-gray-300 bg-white"
/>
<span className="font-medium">عرض فقط العقارات المتاحة من اليوم</span>
<span className="font-medium">{t("availableToday")}</span>
</label>
</div>
</div>
@ -289,13 +289,13 @@ export default function HeroSearch({ onSearch, isAuthenticated }) {
<div className="flex items-center gap-3 mb-5">
<ShieldCheck className="w-7 h-7 text-amber-500" />
<div>
<h3 className="text-lg font-semibold text-gray-900">يرجى تسجيل الدخول</h3>
<p className="text-sm text-gray-600">للوصول إلى خيارات التأجير والبيع، يجب أن تكون مسجلاً.</p>
<h3 className="text-lg font-semibold text-gray-900">{t("loginRequired")}</h3>
<p className="text-sm text-gray-600">{t("loginRequiredDesc")}</p>
</div>
</div>
<div className="space-y-4">
<div className="rounded-2xl bg-gray-50 p-4">
<p className="text-sm text-gray-700">اضغط على تسجيل الدخول لاستكمال البحث أو إدارة عقاراتك.</p>
<p className="text-sm text-gray-700">{t("loginPrompt")}</p>
</div>
<div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
<button
@ -303,13 +303,13 @@ export default function HeroSearch({ onSearch, isAuthenticated }) {
onClick={() => setShowLoginDialog(false)}
className="w-full sm:w-auto px-5 py-3 rounded-xl border border-gray-300 text-gray-700 hover:bg-gray-100 transition-colors"
>
إغلاق
{t("close")}
</button>
<Link
href="/login"
className="w-full sm:w-auto px-5 py-3 rounded-xl bg-amber-500 text-white font-semibold text-center hover:bg-amber-600 transition-colors"
>
تسجيل الدخول
{t("login")}
</Link>
</div>
</div>
@ -318,4 +318,4 @@ export default function HeroSearch({ onSearch, isAuthenticated }) {
)}
</>
);
}
}

View File

@ -2,6 +2,7 @@
import { useState } from 'react';
import { X, Loader2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import toast from 'react-hot-toast';
import StarRating from './StarRating';
import { addCustomerRating } from '../../utils/ratings';
@ -10,11 +11,12 @@ const RatingField = ({ label, value, onChange }) => (
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">{label} <span className="text-red-500">*</span></label>
<StarRating rating={value} onRatingChange={onChange} size={28} />
{value === 0 && <p className="text-xs text-red-500">مطلوب</p>}
{value === 0 && <p className="text-xs text-red-500">{/* required text handled in parent */}</p>}
</div>
);
export default function CustomerRatingForm({ reservationId, onSuccess, onCancel }) {
const { t } = useTranslation();
const [furnitureIntegrityRating, setFurnitureIntegrityRating] = useState(0);
const [termsComplianceRating, setTermsComplianceRating] = useState(0);
const [renterBehaviorRating, setRenterBehaviorRating] = useState(0);
@ -22,9 +24,9 @@ export default function CustomerRatingForm({ reservationId, onSuccess, onCancel
const [loading, setLoading] = useState(false);
const validate = () => {
if (furnitureIntegrityRating === 0) return 'الحفاظ على الأثاث';
if (termsComplianceRating === 0) return 'الالتزام بالشروط';
if (renterBehaviorRating === 0) return 'سلوك المستأجر';
if (furnitureIntegrityRating === 0) return t('ratings.furnitureIntegrity');
if (termsComplianceRating === 0) return t('ratings.termsCompliance');
if (renterBehaviorRating === 0) return t('ratings.renterBehavior');
return null;
};
@ -32,7 +34,7 @@ export default function CustomerRatingForm({ reservationId, onSuccess, onCancel
e.preventDefault();
const missing = validate();
if (missing) {
toast.error(`يرجى تقييم: ${missing}`);
toast.error(t('ratings.pleaseRate') + ': ' + missing);
return;
}
@ -45,10 +47,10 @@ export default function CustomerRatingForm({ reservationId, onSuccess, onCancel
renterBehaviorRating,
comment: comment.trim() || null,
});
toast.success('تم إرسال تقييم المستأجر بنجاح!');
toast.success(t('ratings.customerRatingSubmitted'));
onSuccess?.();
} catch (err) {
toast.error('حدث خطأ، حاول مرة أخرى');
toast.error(t('ratings.submitError'));
} finally {
setLoading(false);
}
@ -57,7 +59,7 @@ export default function CustomerRatingForm({ reservationId, onSuccess, onCancel
return (
<div className="bg-white rounded-2xl shadow-lg border border-gray-200 p-6 max-w-lg mx-auto">
<div className="flex justify-between items-center mb-4">
<h3 className="text-xl font-bold text-gray-900">تقييم المستأجر</h3>
<h3 className="text-xl font-bold text-gray-900">{t('ratings.customerRating')}</h3>
{onCancel && (
<button onClick={onCancel} className="text-gray-400 hover:text-gray-600">
<X size={20} />
@ -65,17 +67,17 @@ export default function CustomerRatingForm({ reservationId, onSuccess, onCancel
)}
</div>
<form onSubmit={handleSubmit} className="space-y-5">
<RatingField label="الحفاظ على الأثاث" value={furnitureIntegrityRating} onChange={setFurnitureIntegrityRating} />
<RatingField label="الالتزام بالشروط" value={termsComplianceRating} onChange={setTermsComplianceRating} />
<RatingField label="سلوك المستأجر" value={renterBehaviorRating} onChange={setRenterBehaviorRating} />
<RatingField label={t('ratings.furnitureIntegrity')} value={furnitureIntegrityRating} onChange={setFurnitureIntegrityRating} />
<RatingField label={t('ratings.termsCompliance')} value={termsComplianceRating} onChange={setTermsComplianceRating} />
<RatingField label={t('ratings.renterBehavior')} value={renterBehaviorRating} onChange={setRenterBehaviorRating} />
<div>
<label className="block text-sm font-medium text-gray-700">تعليق (اختياري)</label>
<label className="block text-sm font-medium text-gray-700">{t('ratings.commentLabel')}</label>
<textarea
rows={3}
value={comment}
onChange={(e) => setComment(e.target.value)}
className="w-full mt-1 px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
placeholder="شارك تجربتك مع المستأجر..."
placeholder={t('ratings.commentPlaceholder')}
/>
</div>
<button
@ -84,7 +86,7 @@ export default function CustomerRatingForm({ reservationId, onSuccess, onCancel
className="w-full bg-amber-500 hover:bg-amber-600 text-white font-bold py-3 rounded-xl transition flex items-center justify-center gap-2 disabled:opacity-50"
>
{loading && <Loader2 className="w-5 h-5 animate-spin" />}
{loading ? 'جاري الإرسال...' : 'إرسال التقييم'}
{loading ? t('ratings.sending') : t('ratings.submitRating')}
</button>
</form>
</div>

View File

@ -26,4 +26,13 @@ const BookingStatusColors = Object.freeze({
[BookingStatus.cancelled]: 'red',
});
export { BookingStatus, BookingStatusLabels, BookingStatusColors };
const BookingStatusTranslationKeys = Object.freeze({
[BookingStatus.PENDING]: 'bookingStatus.pending',
[BookingStatus.ownerConfirmed]: 'bookingStatus.ownerConfirmed',
[BookingStatus.depositPaid]: 'bookingStatus.depositPaid',
[BookingStatus.depositConfirmed]: 'bookingStatus.depositConfirmed',
[BookingStatus.completed]: 'bookingStatus.completed',
[BookingStatus.cancelled]: 'bookingStatus.cancelled',
});
export { BookingStatus, BookingStatusLabels, BookingStatusColors, BookingStatusTranslationKeys };

View File

@ -56,4 +56,17 @@ const BuildingTypeByKey = Object.freeze({
warehouse: BuildingType.WAREHOUSE,
});
export { BuildingType, BuildingTypeLabels, BuildingTypeKeys, BuildingTypeByKey };
const BuildingTypeTranslationKeys = Object.freeze({
[BuildingType.APARTMENT]: 'buildingType.apartment',
[BuildingType.VILLA]: 'buildingType.villa',
[BuildingType.HOUSE]: 'buildingType.house',
[BuildingType.SWEET]: 'buildingType.sweet',
[BuildingType.ROOM]: 'buildingType.room',
[BuildingType.STUDIO]: 'buildingType.studio',
[BuildingType.OFFICE]: 'buildingType.office',
[BuildingType.FARMS]: 'buildingType.farms',
[BuildingType.SHOP]: 'buildingType.shop',
[BuildingType.WAREHOUSE]: 'buildingType.warehouse',
});
export { BuildingType, BuildingTypeLabels, BuildingTypeKeys, BuildingTypeByKey, BuildingTypeTranslationKeys };

View File

@ -10,4 +10,10 @@ const CancellationReasonLabels = Object.freeze({
[CancellationReason.OWNER_CANCELED]: 'إلغاء من المالك',
});
export { CancellationReason, CancellationReasonLabels };
const CancellationReasonTranslationKeys = Object.freeze({
[CancellationReason.NONE]: 'cancellationReason.none',
[CancellationReason.DEPOSIT_NOT_PAID]: 'cancellationReason.noDeposit',
[CancellationReason.OWNER_CANCELED]: 'cancellationReason.ownerCancellation',
});
export { CancellationReason, CancellationReasonLabels, CancellationReasonTranslationKeys };

View File

@ -35,4 +35,20 @@ function extractCity(address) {
return '';
}
export { City, CitiesList, extractCity };
const CityTranslationKeys = Object.freeze({
[City.DAMASCUS]: 'city.damascus',
[City.ALEPPO]: 'city.aleppo',
[City.HOMS]: 'city.homs',
[City.LATAKIA]: 'city.latakia',
[City.DARAA]: 'city.daraa',
[City.TARTOUS]: 'city.tartous',
[City.SUWEIDA]: 'city.suweida',
[City.DEIR_EZZOR]: 'city.deirEzzor',
[City.RAQQA]: 'city.raqqa',
[City.IDLIB]: 'city.idlib',
[City.HASAKAH]: 'city.hasakah',
[City.QAMISHLI]: 'city.qamishli',
[City.RURAL_DAMASCUS]: 'city.ruralDamascus',
});
export { City, CitiesList, extractCity, CityTranslationKeys };

View File

@ -16,4 +16,10 @@ const CommissionTypeLabels = Object.freeze({
[CommissionType.FROM_BOTH]: 'من الاثنين',
});
export { CommissionType, CommissionTypeLabels };
const CommissionTypeTranslationKeys = Object.freeze({
[CommissionType.FROM_OWNER]: 'commissionType.fromOwner',
[CommissionType.FROM_TENANT]: 'commissionType.fromTenant',
[CommissionType.FROM_BOTH]: 'commissionType.fromBoth',
});
export { CommissionType, CommissionTypeLabels, CommissionTypeTranslationKeys };

View File

@ -23,4 +23,11 @@ const CurrencySymbols = Object.freeze({
[Currency.TRY]: 'TRY',
});
export { Currency, CurrencyLabels, CurrencySymbols };
const CurrencyTranslationKeys = Object.freeze({
[Currency.SYP]: 'currency.syp',
[Currency.USD]: 'currency.usd',
[Currency.EUR]: 'currency.eur',
[Currency.TRY]: 'currency.try',
});
export { Currency, CurrencyLabels, CurrencySymbols, CurrencyTranslationKeys };

View File

@ -14,4 +14,9 @@ const CustomerTypeLabels = Object.freeze({
[CustomerType.FAMILY]: 'عائلي',
});
export { CustomerType, CustomerTypeLabels };
const CustomerTypeTranslationKeys = Object.freeze({
[CustomerType.PERSONAL]: 'customerType.personal',
[CustomerType.FAMILY]: 'customerType.family',
});
export { CustomerType, CustomerTypeLabels, CustomerTypeTranslationKeys };

View File

@ -20,4 +20,9 @@ const IdentityTypeFlags = Object.freeze({
[IdentityType.PASSPORT]: '🛂',
});
export { IdentityType, IdentityTypeLabels, IdentityTypeFlags };
const IdentityTypeTranslationKeys = Object.freeze({
[IdentityType.SYRIAN]: 'identityType.syrian',
[IdentityType.PASSPORT]: 'identityType.passport',
});
export { IdentityType, IdentityTypeLabels, IdentityTypeFlags, IdentityTypeTranslationKeys };

View File

@ -14,4 +14,9 @@ const OwnerTypeLabels = Object.freeze({
[OwnerType.REAL_ESTATE_AGENCY]: 'وكالة عقارية',
});
export { OwnerType, OwnerTypeLabels };
const OwnerTypeTranslationKeys = Object.freeze({
[OwnerType.PERSON]: 'ownerType.person',
[OwnerType.REAL_ESTATE_AGENCY]: 'ownerType.agency',
});
export { OwnerType, OwnerTypeLabels, OwnerTypeTranslationKeys };

View File

@ -38,4 +38,20 @@ const PropertyServiceLabels = Object.freeze({
// All services as array
const PropertyServicesList = Object.freeze(Object.values(PropertyService));
export { PropertyService, PropertyServiceLabels, PropertyServicesList };
const PropertyServiceTranslationKeys = Object.freeze({
[PropertyService.ELECTRICITY]: 'propertyService.electricity',
[PropertyService.INTERNET]: 'propertyService.internet',
[PropertyService.HEATING]: 'propertyService.heating',
[PropertyService.WATER]: 'propertyService.water',
[PropertyService.POOL]: 'propertyService.pool',
[PropertyService.PRIVATE_GARDEN]: 'propertyService.privateGarden',
[PropertyService.PARKING]: 'propertyService.parking',
[PropertyService.SECURITY_247]: 'propertyService.security247',
[PropertyService.CENTRAL_HEATING]: 'propertyService.centralHeating',
[PropertyService.CENTRAL_AIR_CONDITIONING]: 'propertyService.centralAirConditioning',
[PropertyService.EQUIPPED_KITCHEN]: 'propertyService.equippedKitchen',
[PropertyService.MAIDS_ROOM]: 'propertyService.maidsRoom',
[PropertyService.ELEVATOR]: 'propertyService.elevator',
});
export { PropertyService, PropertyServiceLabels, PropertyServicesList, PropertyServiceTranslationKeys };

View File

@ -27,4 +27,10 @@ const PropertyStatusByKey = Object.freeze({
booked: PropertyStatus.BOOKED,
});
export { PropertyStatus, PropertyStatusLabels, PropertyStatusKeys, PropertyStatusByKey };
const PropertyStatusTranslationKeys = Object.freeze({
[PropertyStatus.AVAILABLE]: 'propertyStatus.available',
[PropertyStatus.NOT_AVAILABLE]: 'propertyStatus.notAvailable',
[PropertyStatus.BOOKED]: 'propertyStatus.booked',
});
export { PropertyStatus, PropertyStatusLabels, PropertyStatusKeys, PropertyStatusByKey, PropertyStatusTranslationKeys };

View File

@ -18,4 +18,10 @@ const PropertyTermLabels = Object.freeze({
// All terms as array
const PropertyTermsList = Object.freeze(Object.values(PropertyTerm));
export { PropertyTerm, PropertyTermLabels, PropertyTermsList };
const PropertyTermTranslationKeys = Object.freeze({
[PropertyTerm.NO_SMOKING]: 'propertyTerm.noSmoking',
[PropertyTerm.NO_ANIMALS]: 'propertyTerm.noAnimals',
[PropertyTerm.NO_PARTIES]: 'propertyTerm.noParties',
});
export { PropertyTerm, PropertyTermLabels, PropertyTermsList, PropertyTermTranslationKeys };

View File

@ -13,4 +13,9 @@ const RentPropertyConditionLabels = Object.freeze({
[RentPropertyCondition.WITHOUT_FURNITURE]: 'غير مفروش',
});
export { RentPropertyCondition, RentPropertyConditionLabels };
const RentPropertyConditionTranslationKeys = Object.freeze({
[RentPropertyCondition.WITH_FURNITURE]: 'rentCondition.furnished',
[RentPropertyCondition.WITHOUT_FURNITURE]: 'rentCondition.unfurnished',
});
export { RentPropertyCondition, RentPropertyConditionLabels, RentPropertyConditionTranslationKeys };

View File

@ -14,4 +14,10 @@ const RentPropertyTypeLabels = Object.freeze({
[RentPropertyType.SEMI_FURNISHED]: 'مفروش جزئياً',
});
export { RentPropertyType, RentPropertyTypeLabels };
const RentPropertyTypeTranslationKeys = Object.freeze({
[RentPropertyType.FURNISHED]: 'rentPropertyType.fullyFurnished',
[RentPropertyType.UNFURNISHED]: 'rentPropertyType.unfurnished',
[RentPropertyType.SEMI_FURNISHED]: 'rentPropertyType.partiallyFurnished',
});
export { RentPropertyType, RentPropertyTypeLabels, RentPropertyTypeTranslationKeys };

View File

@ -13,4 +13,9 @@ const RentTypeLabels = Object.freeze({
[RentType.DAILY]: 'يومي',
});
export { RentType, RentTypeLabels };
const RentTypeTranslationKeys = Object.freeze({
[RentType.MONTHLY]: 'rentType.monthly',
[RentType.DAILY]: 'rentType.daily',
});
export { RentType, RentTypeLabels, RentTypeTranslationKeys };

View File

@ -8,4 +8,9 @@ const SalePropertiesStatusLabels = Object.freeze({
[SalePropertiesStatus.CONFIRMED]: 'مؤكد',
});
export { SalePropertiesStatus, SalePropertiesStatusLabels };
const SalePropertiesStatusTranslationKeys = Object.freeze({
[SalePropertiesStatus.PENDING]: 'saleStatus.pending',
[SalePropertiesStatus.CONFIRMED]: 'saleStatus.confirmed',
});
export { SalePropertiesStatus, SalePropertiesStatusLabels, SalePropertiesStatusTranslationKeys };

View File

@ -8,4 +8,9 @@ const TransactionTypeLabels = Object.freeze({
[TransactionType.MANUAL]: 'يدوي',
});
export { TransactionType, TransactionTypeLabels };
const TransactionTypeTranslationKeys = Object.freeze({
[TransactionType.ONLINE]: 'transactionType.online',
[TransactionType.MANUAL]: 'transactionType.manual',
});
export { TransactionType, TransactionTypeLabels, TransactionTypeTranslationKeys };

View File

@ -24,4 +24,11 @@ const UserRoleColors = Object.freeze({
[UserRole.AGENT]: 'purple',
});
export { UserRole, UserRoleLabels, UserRoleColors };
const UserRoleTranslationKeys = Object.freeze({
[UserRole.GUEST]: 'userRole.guest',
[UserRole.CUSTOMER]: 'userRole.customer',
[UserRole.OWNER]: 'userRole.owner',
[UserRole.AGENT]: 'userRole.agent',
});
export { UserRole, UserRoleLabels, UserRoleColors, UserRoleTranslationKeys };

View File

@ -2,47 +2,25 @@
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useTranslation } from 'react-i18next';
import { HelpCircle, ChevronDown, Search } from 'lucide-react';
const faqs = [
{
question: 'كيفية البحث عن عقار؟',
answer: 'يمكنك البحث عن عقار من خلال استخدام شريط البحث في الصفحة الرئيسية، حيث يمكنك تحديد المدينة ونوع العقار والسعر. كما يمكنك استخدام الفلاتر المتقدمة لتحديد عدد الغرف والحمامات والمساحة وطرق الدفع. يمكنك أيضاً تصفح العقارات المميزة والتوصيات المقدمة خصيصاً لك.',
},
{
question: 'كيف أحجز عقاراً؟',
answer: 'بعد العثور على العقار المناسب، يمكنك النقر على "حجز" واختيار تواريخ الإقامة. سيطلب منك تسجيل الدخول أو إنشاء حساب إذا لم يكن لديك حساب. بعد ذلك، قم بتأكيد الحجز وسيتم إخطار المالك بطلبك. يمكنك متابعة حالة حجزك من صفحة الحجوزات.',
},
{
question: 'كيف أضيف عقاراً؟',
answer: 'لتسجيل عقارك على المنصة، يجب عليك أولاً إنشاء حساب مالك. بعد تسجيل الدخول، انتقل إلى لوحة التحكم واختر "إضافة عقار". قم بتعبئة جميع المعلومات المطلوبة مثل الموقع، المساحة، عدد الغرف، الصور، السعر، وغيرها من التفاصيل. بعد المراجعة، سيتم نشر عقارك على المنصة.',
},
{
question: 'كيف أتواصل مع المالك؟',
answer: 'بعد تسجيل الدخول، يمكنك التواصل مع المالك من خلال صفحة العقار بالضغط على زر "تواصل مع المالك". ستظهر لك معلومات الاتصال بالمالك بما في ذلك رقم الهاتف والبريد الإلكتروني. يمكنك أيضاً مراسلة المالك مباشرة من خلال نظام المراسلة الداخلي في المنصة.',
},
{
question: 'ما هي سياسة الإلغاء؟',
answer: 'سياسة الإلغاء تختلف حسب كل عقار. بشكل عام، يمكن إلغاء الحجز قبل 48 ساعة من موعد الوصول للحصول على استرداد كامل. للإلغاء خلال أقل من 48 ساعة، قد يتم تطبيق رسوم إلغاء. ننصحك بمراجعة سياسة الإلغاء المدونة في صفحة العقار قبل تأكيد الحجز.',
},
{
question: 'كيف أقيم العقار؟',
answer: 'بعد انتهاء إقامتك، يمكنك تقييم العقار من خلال صفحة الحجوزات. اختر الحجز المناسب ثم اضغط على "تقييم". يمكنك إعطاء تقييم بالنجوم وكتابة تعليق يصف تجربتك. تقييمات المستخدمين تساعد الآخرين في اتخاذ القرار المناسب.',
},
{
question: 'هل يمكنني تغيير كلمة المرور؟',
answer: 'نعم، يمكنك تغيير كلمة المرور من خلال صفحة الملف الشخصي. اختر "تغيير كلمة المرور" وأدخل كلمة المرور الحالية ثم كلمة المرور الجديدة. يجب أن تتكون كلمة المرور الجديدة من 8 أحرف على الأقل وتحتوي على حرف كبير وحرف صغير ورقم.',
},
{
question: 'كيف أحذف حسابي؟',
answer: 'لحذف حسابك نهائياً، يرجى التواصل مع فريق الدعم الفني من خلال صفحة الدعم. سنقوم بمعالجة طلبك خلال 24 ساعة. يرجى العلم أنه لا يمكن استعادة الحساب بعد حذفه، وسيتم فقدان جميع البيانات والحجوزات المرتبطة به.',
},
];
export default function FAQPage() {
const { t } = useTranslation();
const [openIndex, setOpenIndex] = useState(null);
const [searchTerm, setSearchTerm] = useState('');
const faqs = [
{ question: t('faq.q1'), answer: t('faq.a1') },
{ question: t('faq.q2'), answer: t('faq.a2') },
{ question: t('faq.q3'), answer: t('faq.a3') },
{ question: t('faq.q4'), answer: t('faq.a4') },
{ question: t('faq.q5'), answer: t('faq.a5') },
{ question: t('faq.q6'), answer: t('faq.a6') },
{ question: t('faq.q7'), answer: t('faq.a7') },
{ question: t('faq.q8'), answer: t('faq.a8') },
];
const filteredFaqs = faqs.filter(
(faq) =>
faq.question.includes(searchTerm) ||
@ -64,9 +42,9 @@ export default function FAQPage() {
<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">
<HelpCircle className="w-10 h-10 text-amber-600" />
</div>
<h1 className="text-4xl font-bold text-gray-900 mb-4">الأسئلة الشائعة</h1>
<h1 className="text-4xl font-bold text-gray-900 mb-4">{t('faq.pageTitle')}</h1>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
إجابات على أكثر الأسئلة شيوعاً حول استخدام المنصة
{t('faq.pageDescription')}
</p>
</motion.div>
@ -79,7 +57,7 @@ export default function FAQPage() {
<Search className="absolute right-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
type="text"
placeholder="ابحث في الأسئلة الشائعة..."
placeholder={t('faq.searchPlaceholder')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pr-12 pl-4 py-4 bg-white border border-gray-200 rounded-2xl shadow-sm focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400"
@ -134,7 +112,7 @@ export default function FAQPage() {
animate={{ opacity: 1 }}
className="text-center py-12"
>
<p className="text-gray-500 text-lg">لا توجد نتائج للبحث</p>
<p className="text-gray-500 text-lg">{t('faq.noResults')}</p>
</motion.div>
)}
</div>

View File

@ -1,6 +1,7 @@
'use client';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useRouter } from 'next/navigation';
import { motion } from 'framer-motion';
import Link from 'next/link';
@ -10,6 +11,7 @@ import { useFavorites } from '@/app/contexts/FavoritesContext';
import AuthService from '@/app/services/AuthService';
export default function FavoritesPage() {
const { t } = useTranslation();
const router = useRouter();
const { favorites, isLoading: favoritesLoading, removeFavorite } = useFavorites();
@ -23,7 +25,7 @@ export default function FavoritesPage() {
}, [router]);
const formatCurrency = (amount) => {
return amount?.toLocaleString() + ' ل.س';
return amount?.toLocaleString() + ' ' + t('currency-syp-suffix');
};
if (favoritesLoading && favorites.length === 0) {
@ -31,7 +33,7 @@ export default function FavoritesPage() {
<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">جاري التحميل...</p>
<p className="text-gray-600">{t('loading')}</p>
</div>
</div>
);
@ -41,20 +43,20 @@ export default function FavoritesPage() {
<div className="min-h-screen bg-gray-50 py-8">
<div className="container mx-auto px-4 max-w-6xl">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">المفضلة</h1>
<p className="text-gray-600">العقارات التي قمت بحفظها</p>
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t('favorites')}</h1>
<p className="text-gray-600">{t('saved-properties')}</p>
</div>
{favorites.length === 0 ? (
<div className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
<Heart className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد عقارات في المفضلة</h3>
<p className="text-gray-500 mb-6">يمكنك إضافة العقارات التي تعجبك بالنقر على أيقونة القلب</p>
<h3 className="text-xl font-bold text-gray-700 mb-2">{t('no-favorites')}</h3>
<p className="text-gray-500 mb-6">{t('add-favorites-hint')}</p>
<Link
href="/properties"
className="inline-flex items-center gap-2 bg-amber-500 text-white px-6 py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors"
>
استعرض العقارات
{t('browse-properties')}
</Link>
</div>
) : (
@ -93,7 +95,7 @@ export default function FavoritesPage() {
<div>
<div className="flex items-center gap-2 mb-2">
<span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-lg text-xs font-medium">
{property.type === 'apartment' ? 'شقة' : property.type === 'villa' ? 'فيلا' : 'بيت'}
{property.type === 'apartment' ? t('buildingType.apartment') : property.type === 'villa' ? t('buildingType.villa') : t('buildingType.house')}
</span>
</div>
<h3 className="font-bold text-gray-900 mb-1 line-clamp-1">{property.title}</h3>
@ -106,7 +108,7 @@ export default function FavoritesPage() {
</div>
<div className="text-left">
<div className="text-xl font-bold text-gray-900">{formatCurrency(property.price)}</div>
<div className="text-xs text-gray-500">/{property.priceUnit === 'daily' ? 'يوم' : 'شهر'}</div>
<div className="text-xs text-gray-500">/{property.priceUnit === 'daily' ? t('day') : t('month')}</div>
</div>
</div>
@ -131,7 +133,7 @@ export default function FavoritesPage() {
href={`/property/${property.id}`}
className="block w-full bg-amber-500 text-white py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors text-center"
>
عرض التفاصيل
{t('viewDetails')}
</Link>
</div>
</motion.div>

View File

@ -6,10 +6,12 @@ import { useRouter } from 'next/navigation';
import Link from 'next/link';
import toast, { Toaster } from 'react-hot-toast';
import { Mail, Key, Lock, CheckCircle, ArrowLeft, RefreshCw } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { requestForgetPasswordOtp, verifyForgetPasswordOtp } from '../utils/api';
export default function ForgotPasswordPage() {
const router = useRouter();
const { t } = useTranslation();
const [step, setStep] = useState(1);
const [email, setEmail] = useState('');
const [code, setCode] = useState('');
@ -20,17 +22,17 @@ export default function ForgotPasswordPage() {
e.preventDefault();
if (!email.trim()) {
toast.error('يرجى إدخال البريد الإلكتروني');
toast.error(t('forgotPassword.emailRequired'));
return;
}
setIsLoading(true);
try {
await requestForgetPasswordOtp(email);
toast.success('تم إرسال رمز التحقق إلى بريدك الإلكتروني');
toast.success(t('forgotPassword.otpSent'));
setStep(2);
} catch (err) {
toast.error(err?.message || 'فشل إرسال رمز التحقق');
toast.error(err?.message || t('forgotPassword.otpSendFailed'));
} finally {
setIsLoading(false);
}
@ -40,22 +42,22 @@ export default function ForgotPasswordPage() {
e.preventDefault();
if (!code.trim()) {
toast.error('يرجى إدخال رمز التحقق');
toast.error(t('forgotPassword.otpRequired'));
return;
}
if (newPassword.length < 6) {
toast.error('كلمة المرور الجديدة يجب أن تكون 6 أحرف على الأقل');
toast.error(t('validation.passwordMinLength'));
return;
}
setIsLoading(true);
try {
await verifyForgetPasswordOtp(email, code, newPassword);
toast.success('تم إعادة تعيين كلمة المرور بنجاح');
toast.success(t('forgotPassword.resetSuccess'));
setTimeout(() => router.push('/login'), 1200);
} catch (err) {
toast.error(err?.message || 'فشل التحقق من الرمز');
toast.error(err?.message || t('forgotPassword.otpVerifyFailed'));
} finally {
setIsLoading(false);
}
@ -84,7 +86,7 @@ export default function ForgotPasswordPage() {
>
<Link href="/login" className="flex items-center gap-2 text-gray-600 hover:text-amber-600 transition-colors group">
<ArrowLeft className="w-5 h-5 group-hover:-translate-x-1 transition-transform" />
<span>العودة لتسجيل الدخول</span>
<span>{t('forgotPassword.backToLogin')}</span>
</Link>
</motion.div>
@ -95,7 +97,7 @@ export default function ForgotPasswordPage() {
animate={{ y: 0, opacity: 1 }}
className="text-3xl font-bold text-white mb-2"
>
استعادة كلمة المرور
{t('forgotPassword.title')}
</motion.h1>
<motion.p
initial={{ y: 20, opacity: 0 }}
@ -103,7 +105,7 @@ export default function ForgotPasswordPage() {
transition={{ delay: 0.1 }}
className="text-amber-100"
>
{step === 1 ? 'أدخل بريدك الإلكتروني لاستلام رمز التحقق' : 'أدخل رمز التحقق وكلمة المرور الجديدة'}
{step === 1 ? t('forgotPassword.step1Instruction') : t('forgotPassword.step2Instruction')}
</motion.p>
</div>
@ -120,7 +122,7 @@ export default function ForgotPasswordPage() {
>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
البريد الإلكتروني
{t('forgotPassword.emailLabel')}
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
@ -131,7 +133,7 @@ export default function ForgotPasswordPage() {
value={email}
onChange={(e) => setEmail(e.target.value)}
className={inputClass}
placeholder="أدخل بريدك الإلكتروني"
placeholder={t('forgotPassword.emailPlaceholder')}
dir="ltr"
required
/>
@ -148,10 +150,10 @@ export default function ForgotPasswordPage() {
{isLoading ? (
<div className="flex items-center justify-center gap-2">
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
<span>جاري الإرسال...</span>
<span>{t('forgotPassword.sending')}</span>
</div>
) : (
'إرسال رمز التحقق'
t('forgotPassword.sendOtp')
)}
</motion.button>
</motion.form>
@ -168,7 +170,7 @@ export default function ForgotPasswordPage() {
>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
رمز التحقق
{t('forgotPassword.otpLabel')}
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
@ -179,7 +181,7 @@ export default function ForgotPasswordPage() {
value={code}
onChange={(e) => setCode(e.target.value)}
className={inputClass}
placeholder="أدخل رمز التحقق"
placeholder={t('forgotPassword.otpPlaceholder')}
dir="ltr"
required
/>
@ -188,7 +190,7 @@ export default function ForgotPasswordPage() {
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
كلمة المرور الجديدة
{t('forgotPassword.newPasswordLabel')}
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
@ -199,7 +201,7 @@ export default function ForgotPasswordPage() {
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className={inputClass}
placeholder="أدخل كلمة المرور الجديدة"
placeholder={t('forgotPassword.newPasswordPlaceholder')}
required
minLength={6}
/>
@ -216,10 +218,10 @@ export default function ForgotPasswordPage() {
{isLoading ? (
<div className="flex items-center justify-center gap-2">
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
<span>جاري التحقق...</span>
<span>{t('forgotPassword.verifying')}</span>
</div>
) : (
'إعادة تعيين كلمة المرور'
t('forgotPassword.resetPassword')
)}
</motion.button>
@ -230,7 +232,7 @@ export default function ForgotPasswordPage() {
className="text-sm text-amber-600 hover:text-amber-700 transition-colors flex items-center justify-center gap-1 mx-auto"
>
<RefreshCw className="w-4 h-4" />
<span>تغيير البريد الإلكتروني</span>
<span>{t('forgotPassword.changeEmail')}</span>
</button>
</div>
</motion.form>

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { motion, AnimatePresence } from "framer-motion";
import toast, { Toaster } from "react-hot-toast";
import Link from "next/link";
@ -35,6 +36,7 @@ import AuthService from "../services/AuthService";
export default function LoginPage() {
const router = useRouter();
const { t } = useTranslation();
// Step: 'login' | 'otp'
const [step, setStep] = useState("login");
@ -59,16 +61,16 @@ export default function LoginPage() {
if (!formData.credential) {
newErrors.credential =
loginMethod === "email"
? "البريد الإلكتروني مطلوب"
: "رقم الهاتف مطلوب";
? t("emailRequired")
: t("phoneRequired");
// } else if (loginMethod === 'email' && !isEmail(formData.credential)) {
// newErrors.credential = 'البريد الإلكتروني غير صالح';
// } else if (loginMethod === 'phone' && !isPhoneNumber(formData.credential)) {
newErrors.credential = "رقم الهاتف غير صالح";
newErrors.credential = t("phoneInvalid");
}
if (!formData.password) {
newErrors.password = "كلمة المرور مطلوبة";
newErrors.password = t("passwordRequired");
}
setErrors(newErrors);
@ -119,7 +121,7 @@ export default function LoginPage() {
const userRole = AuthService.isOwner() ? "owner" : "customer";
setIsSuccess(true);
toast.success("تم تسجيل الدخول بنجاح!", {
toast.success(t("loginSuccess"), {
style: { background: "#dcfce7", color: "#166534" },
});
@ -134,7 +136,7 @@ export default function LoginPage() {
if (tempToken) {
AuthService.addToken(tempToken);
}
toast("يرجى إدخال رمز التحقق", {
toast(t("otpRequired"), {
icon: "🔐",
style: { background: "#fef3c7", color: "#92400e" },
});
@ -155,7 +157,7 @@ export default function LoginPage() {
// Other error
console.error("[Login] Unexpected status:", result.status, result.data);
toast.error(
result.data?.message || result.data || "بيانات الدخول غير صحيحة",
result.data?.message || result.data || t("invalidCredentials"),
{
style: { background: "#fee2e2", color: "#991b1b" },
},
@ -163,7 +165,7 @@ export default function LoginPage() {
}
} catch (err) {
console.error("[Login] Error:", err);
toast.error(err.message || "حدث خطأ في الاتصال", {
toast.error(err.message || t("connectionError"), {
style: { background: "#fee2e2", color: "#991b1b" },
});
} finally {
@ -174,7 +176,7 @@ export default function LoginPage() {
const handleVerifyOTP = async (e) => {
e.preventDefault();
if (!otpCode || otpCode.length < 4) {
setOtpError("يرجى إدخال رمز التحقق");
setOtpError(t("otpRequired"));
return;
}
@ -196,7 +198,7 @@ export default function LoginPage() {
}
setIsSuccess(true);
toast.success("تم التحقق بنجاح!", {
toast.success(t("verifySuccess"), {
style: { background: "#dcfce7", color: "#166534" },
});
@ -205,11 +207,11 @@ export default function LoginPage() {
}, 1500);
} else {
console.error("[OTP] Verification failed:", result.data);
setOtpError(result.data?.message || "رمز التحقق غير صحيح");
setOtpError(result.data?.message || t("wrongOTP"));
}
} catch (err) {
console.error("[OTP] Error:", err);
setOtpError(err.message || "حدث خطأ في التحقق");
setOtpError(err.message || t("verifyError"));
} finally {
setIsLoading(false);
}
@ -222,12 +224,12 @@ export default function LoginPage() {
} else {
await sendPhoneOTP();
}
toast.success("تم إرسال رمز التحقق مجدداً", {
toast.success(t("resendSuccess"), {
style: { background: "#dcfce7", color: "#166534" },
});
} catch (err) {
console.error("[OTP] Resend failed:", err);
toast.error("فشل إرسال الرمز");
toast.error(t("resendFailed"));
}
};
@ -331,7 +333,7 @@ export default function LoginPage() {
>
<ArrowLeft className="w-4 h-4" />
</motion.div>
<span>العودة للرئيسية</span>
<span>{t("backToHome")}</span>
</Link>
</motion.div>
@ -373,7 +375,7 @@ export default function LoginPage() {
</motion.div>
<h1 className="text-3xl font-bold text-white mb-2">SweetHome</h1>
<p className="text-amber-100">
{step === "otp" ? "أدخل رمز التحقق" : "مرحباً بعودتك!"}
{step === "otp" ? t("enterOTP") : t("welcomeBack")}
</p>
</motion.div>
</div>
@ -404,7 +406,7 @@ export default function LoginPage() {
}`}
>
<Mail className="w-4 h-4" />
بريد إلكتروني
{t("emailMethod")}
</button>
<button
type="button"
@ -419,7 +421,7 @@ export default function LoginPage() {
}`}
>
<Phone className="w-4 h-4" />
رقم الهاتف
{t("phoneMethod")}
</button>
</div>
@ -427,8 +429,8 @@ export default function LoginPage() {
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
{loginMethod === "email"
? "البريد الإلكتروني"
: "رقم الهاتف"}
? t("emailLabel")
: t("phoneLabel")}
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
@ -454,8 +456,8 @@ export default function LoginPage() {
}`}
placeholder={
loginMethod === "email"
? "example@email.com"
: "+963XXXXXXXXX"
? t("emailPlaceholder")
: t("phonePlaceholder")
}
dir="ltr"
/>
@ -474,7 +476,7 @@ export default function LoginPage() {
{/* Password */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
كلمة المرور
{t("password")}
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
@ -496,7 +498,7 @@ export default function LoginPage() {
className={`w-full pr-12 pl-12 py-4 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.password ? "border-red-500" : "border-gray-700"
}`}
placeholder="أدخل كلمة المرور"
placeholder={t("passwordPlaceholder")}
/>
<button
type="button"
@ -536,14 +538,14 @@ export default function LoginPage() {
className="w-4 h-4 rounded border-gray-600 bg-white/5 text-amber-500 focus:ring-amber-500 focus:ring-offset-0"
/>
<span className="text-sm text-gray-400 group-hover:text-white transition-colors">
تذكرني
{t("rememberMe")}
</span>
</label>
<Link
href="/forgot-password"
className="text-sm text-amber-400 hover:text-amber-300 transition-colors"
>
نسيت كلمة المرور؟
{t("forgotPassword")}
</Link>
</div>
@ -559,17 +561,17 @@ export default function LoginPage() {
{isLoading ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
جاري تسجيل الدخول...
{t("loggingIn")}
</>
) : isSuccess ? (
<>
<CheckCircle className="w-5 h-5" />
تم بنجاح!
{t("success")}
</>
) : (
<>
<LogIn className="w-5 h-5" />
تسجيل الدخول
{t("login")}
</>
)}
</span>
@ -588,7 +590,7 @@ export default function LoginPage() {
<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>
@ -597,7 +599,7 @@ export default function LoginPage() {
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
رمز التحقق
{t("otpLabel")}
</label>
<input
type="text"
@ -635,17 +637,17 @@ export default function LoginPage() {
{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>
@ -661,14 +663,14 @@ export default function LoginPage() {
}}
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>
@ -679,12 +681,12 @@ export default function LoginPage() {
variants={itemVariants}
className="text-center text-gray-400 mt-6"
>
ليس لديك حساب؟{" "}
{t("noAccount")}{" "}
<Link
href="/auth/choose-role"
className="text-amber-400 hover:text-amber-300 font-medium transition-colors"
>
إنشاء حساب جديد
{t("createNewAccount")}
</Link>
</motion.p>
</div>
@ -694,19 +696,19 @@ export default function LoginPage() {
variants={itemVariants}
className="text-center text-gray-500 text-xs mt-4"
>
بتسجيل الدخول، أنت توافق على{" "}
{t("agreeTerms")}{" "}
<Link
href="/terms"
className="text-amber-400 hover:text-amber-300 transition-colors"
>
شروط الاستخدام
{t("termsOfUse")}
</Link>{" "}
و{" "}
{t("and")}{" "}
<Link
href="/privacy"
className="text-amber-400 hover:text-amber-300 transition-colors"
>
سياسة الخصوصية
{t("privacyPolicy")}
</Link>
</motion.p>
</motion.div>

View File

@ -1,6 +1,6 @@
'use client';
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { motion } from 'framer-motion';
import { Star, User, Calendar, MessageSquare, ThumbsUp, Loader2 } from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
@ -9,10 +9,10 @@ import AuthService from '../services/AuthService';
import StarRating from '../components/ratings/StarRating';
const RATING_FIELDS = [
{ key: 'cleanRating', label: 'النظافة' },
{ key: 'servicesRating', label: 'الخدمات' },
{ key: 'ownerBehaviorRating', label: 'سلوك المالك' },
{ key: 'experienceRating', label: 'التجربة العامة' },
{ key: 'cleanRating', label: 'ratings.cleanliness' },
{ key: 'servicesRating', label: 'ratings.services' },
{ key: 'ownerBehaviorRating', label: 'ratings.ownerBehavior' },
{ key: 'experienceRating', label: 'ratings.experience' },
];
function calcOverall(ratings) {
@ -24,7 +24,7 @@ function calcOverall(ratings) {
return total / ratings.length;
}
function RatingCard({ rating }) {
function RatingCard({ rating, t }) {
const dateStr = rating.createdAt
? new Date(rating.createdAt).toLocaleDateString('ar')
: null;
@ -49,7 +49,7 @@ function RatingCard({ rating }) {
<div className="space-y-3 mb-4">
{RATING_FIELDS.map(({ key, label }) => (
<div key={key} className="flex items-center justify-between">
<span className="text-sm text-gray-600">{label}</span>
<span className="text-sm text-gray-600">{t(label)}</span>
<div className="flex items-center gap-2">
<StarRating rating={Number(rating[key]) || 0} size={18} readOnly />
<span className="text-sm font-medium text-gray-700 w-6 text-left">
@ -71,18 +71,19 @@ function RatingCard({ rating }) {
}
export default function MyRatesPage() {
const { t } = useTranslation();
const [ratings, setRatings] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const user = AuthService.getUser();
if (!user || !user.id) {
toast.error('يرجى تسجيل الدخول أولاً');
toast.error(t('auth.loginRequired'));
setLoading(false);
return;
}
loadRatings(user.id);
}, []);
}, [t]);
async function loadRatings(userId) {
try {
@ -91,7 +92,7 @@ export default function MyRatesPage() {
setRatings(list);
} catch (err) {
console.error(err);
toast.error('فشل تحميل التقييمات');
toast.error(t('ratings.loadError'));
setRatings([]);
} finally {
setLoading(false);
@ -113,8 +114,8 @@ export default function MyRatesPage() {
<Toaster position="top-center" reverseOrder={false} />
<div className="container mx-auto px-4">
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">تقييماتي</h1>
<p className="text-gray-600">التقييمات التي تلقيتها من مالكي العقارات</p>
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t('ratings.myRatings')}</h1>
<p className="text-gray-600">{t('ratings.receivedFromOwners')}</p>
</motion.div>
{ratings.length > 0 && (
@ -125,9 +126,9 @@ export default function MyRatesPage() {
<StarRating rating={Math.round(overall)} size={24} readOnly />
</div>
<div className="text-4xl font-bold">{overall.toFixed(1)}</div>
<div className="text-amber-100 text-sm mt-1">المعدل العام</div>
<div className="text-amber-100 text-sm mt-1">{t('ratings.overallAverage')}</div>
<div className="text-amber-100 text-xs mt-2">
بناءً على {ratings.length} {ratings.length === 1 ? 'تقييم' : 'تقييمات'}
{t('ratings.basedOn')} {ratings.length} {ratings.length === 1 ? t('ratings.review') : t('ratings.reviews')}
</div>
</motion.div>
)}
@ -136,13 +137,13 @@ export default function MyRatesPage() {
<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">
<Star className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-900 mb-2">لا توجد تقييمات</h3>
<p className="text-gray-500">لم تقم باستئجار أي عقار بعد لتتلقى تقييمات</p>
<h3 className="text-xl font-bold text-gray-900 mb-2">{t('ratings.noRatings')}</h3>
<p className="text-gray-500">{t('ratings.noRatingsDescription')}</p>
</motion.div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{ratings.map((r, i) => (
<RatingCard key={r.id || i} rating={r} />
<RatingCard key={r.id || i} rating={r} t={t} />
))}
</div>
)}

View File

@ -1,6 +1,7 @@
'use client';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useRouter } from 'next/navigation';
import { motion, AnimatePresence } from 'framer-motion';
import { Bell, CheckCircle, XCircle, Calendar, MessageCircle, CheckCheck, Loader2 } from 'lucide-react';
@ -8,6 +9,7 @@ import AuthService from '@/app/services/AuthService';
import { getUserNotifications } from '@/app/utils/api';
export default function NotificationsPage() {
const { t } = useTranslation();
const router = useRouter();
const [notifications, setNotifications] = useState([]);
const [unreadCount, setUnreadCount] = useState(0);
@ -32,7 +34,7 @@ export default function NotificationsPage() {
setUnreadCount(items.length);
} catch (err) {
console.error('Error fetching notifications:', err);
setError(err.message || 'فشل تحميل الإشعارات');
setError(err.message || t('fetch-notifications-failed'));
} finally {
setIsLoading(false);
}
@ -59,7 +61,7 @@ export default function NotificationsPage() {
className="text-center"
>
<Loader2 className="w-12 h-12 text-amber-500 mx-auto mb-4 animate-spin" />
<p className="text-gray-600">جاري التحميل...</p>
<p className="text-gray-600">{t('loading')}</p>
</motion.div>
</div>
);
@ -74,7 +76,7 @@ export default function NotificationsPage() {
className="text-center"
>
<XCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-700 mb-2">خطأ في التحميل</h3>
<h3 className="text-xl font-bold text-gray-700 mb-2">{t('loading-error')}</h3>
<p className="text-gray-500 mb-4">{error}</p>
<motion.button
whileHover={{ scale: 1.05 }}
@ -82,7 +84,7 @@ export default function NotificationsPage() {
onClick={fetchNotifications}
className="px-6 py-2 bg-amber-500 text-white rounded-xl font-medium hover:bg-amber-600 transition-colors"
>
إعادة المحاولة
{t('retry')}
</motion.button>
</motion.div>
</div>
@ -98,9 +100,9 @@ export default function NotificationsPage() {
className="flex justify-between items-center mb-8"
>
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">الإشعارات</h1>
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t('notifications')}</h1>
<p className="text-gray-600">
{unreadCount > 0 ? `لديك ${unreadCount} إشعار غير مقروء` : 'جميع الإشعارات مقروءة'}
{unreadCount > 0 ? t('unread-notifications', { count: unreadCount }) : t('all-notifications-read')}
</p>
</div>
{unreadCount > 0 && (
@ -111,7 +113,7 @@ export default function NotificationsPage() {
className="flex items-center gap-2 px-4 py-2 bg-white border border-gray-200 rounded-xl text-sm font-medium text-gray-700 hover:bg-gray-50 transition-all shadow-sm"
>
<CheckCheck className="w-4 h-4 text-amber-500" />
تحديد الكل كمقروء
{t('mark-all-read')}
</motion.button>
)}
</motion.div>
@ -123,8 +125,8 @@ export default function NotificationsPage() {
className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300"
>
<Bell className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد إشعارات</h3>
<p className="text-gray-500">ستظهر هنا الإشعارات المتعلقة بحجوزاتك ومدفوعاتك</p>
<h3 className="text-xl font-bold text-gray-700 mb-2">{t('no-notifications')}</h3>
<p className="text-gray-500">{t('notifications-empty-hint')}</p>
</motion.div>
) : (
<div className="space-y-4">

View File

@ -5,33 +5,35 @@ import { motion, AnimatePresence } from 'framer-motion';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { Home, Search, Rocket, ArrowLeft, ArrowRight } from 'lucide-react';
import { useTranslation } from 'react-i18next';
const STORAGE_KEY = 'sweethome_onboarding_completed';
const steps = [
{
icon: Home,
title: 'مرحباً بك في SweetHome',
description: 'نحن هنا لمساعدتك في إيجاد العقار المثالي الذي تحلم به. اكتشف آلاف العقارات المتاحة للإيجار والبيع في جميع أنحاء سوريا.',
gradient: 'from-amber-500 to-orange-600',
},
{
icon: Search,
title: 'ابحث عن منزل أحلامك',
description: 'استخدم محرك البحث الذكي لدينا للعثور على العقار المناسب. تصفح حسب الموقع، السعر، النوع، والمزيد من الفلاتر المتقدمة.',
gradient: 'from-blue-500 to-indigo-600',
},
{
icon: Rocket,
title: 'انطلق الآن',
description: 'انشئ حسابك مجاناً وابدأ رحلة البحث عن منزلك الجديد. يمكنك حفظ العقارات المفضلة والتواصل مع المالكين مباشرة.',
gradient: 'from-emerald-500 to-teal-600',
},
];
export default function OnboardingPage() {
const { t, i18n } = useTranslation();
const router = useRouter();
const [currentStep, setCurrentStep] = useState(0);
const steps = [
{
icon: Home,
title: t('onboarding.step1.title'),
description: t('onboarding.step1.description'),
gradient: 'from-amber-500 to-orange-600',
},
{
icon: Search,
title: t('onboarding.step2.title'),
description: t('onboarding.step2.description'),
gradient: 'from-blue-500 to-indigo-600',
},
{
icon: Rocket,
title: t('onboarding.step3.title'),
description: t('onboarding.step3.description'),
gradient: 'from-emerald-500 to-teal-600',
},
];
const [direction, setDirection] = useState(0);
useEffect(() => {
@ -74,7 +76,7 @@ export default function OnboardingPage() {
};
return (
<div 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 bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center p-4 relative overflow-hidden">
<div className="absolute inset-0 overflow-hidden">
{[...Array(30)].map((_, i) => (
<motion.div
@ -199,7 +201,7 @@ export default function OnboardingPage() {
className="flex items-center gap-2 px-5 py-3 rounded-xl text-gray-300 hover:text-white hover:bg-white/10 transition-all border border-white/10"
>
<ArrowRight className="w-5 h-5" />
<span>السابق</span>
<span>{t('onboarding.prev')}</span>
</motion.button>
) : (
<div />
@ -212,7 +214,7 @@ export default function OnboardingPage() {
onClick={nextStep}
className={`flex items-center gap-2 px-6 py-3 rounded-xl text-white font-medium bg-gradient-to-r ${step.gradient} hover:shadow-lg transition-all`}
>
<span>التالي</span>
<span>{t('onboarding.next')}</span>
<ArrowLeft className="w-5 h-5" />
</motion.button>
) : (
@ -223,7 +225,7 @@ export default function OnboardingPage() {
whileTap={{ scale: 0.95 }}
className="px-6 py-3 rounded-xl bg-white text-gray-900 font-bold hover:bg-gray-100 transition-all"
>
تسجيل جديد
{t('onboarding.register')}
</motion.button>
</Link>
<Link href="/login" onClick={handleFinish}>
@ -232,7 +234,7 @@ export default function OnboardingPage() {
whileTap={{ scale: 0.95 }}
className="px-6 py-3 rounded-xl bg-gradient-to-r from-emerald-500 to-teal-600 text-white font-bold hover:shadow-lg transition-all"
>
تسجيل دخول
{t('onboarding.login')}
</motion.button>
</Link>
</div>

View File

@ -2,14 +2,12 @@
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { useRouter } from 'next/navigation';
import { useTranslation } from 'react-i18next';
import {
DollarSign,
TrendingUp,
Calendar,
Building,
Loader2,
ArrowLeft,
Star,
} from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
@ -34,7 +32,7 @@ const StatCard = ({ title, value, icon: Icon, color, subtitle, isNA }) => (
);
export default function OwnerAccountBookPage() {
const router = useRouter();
const { t, i18n } = useTranslation();
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [stats, setStats] = useState({
@ -53,11 +51,9 @@ export default function OwnerAccountBookPage() {
useEffect(() => {
if (AuthService.isGuest()) {
router.push('/auth/choose-role');
return;
}
if (!AuthService.isOwner()) {
router.push('/');
return;
}
@ -93,18 +89,18 @@ export default function OwnerAccountBookPage() {
});
}
} catch {
toast.error('تعذر تحميل إحصائيات الحساب');
toast.error(t('accountBook.loadError'));
} finally {
setIsLoading(false);
}
}
fetchData();
}, [router]);
}, []);
const formatCurrency = (amount) => {
const num = Number(amount);
if (isNaN(num)) return 'N/A';
return num.toLocaleString() + ' ل.س';
return num.toLocaleString() + ' ' + t('currency-syp-suffix');
};
const formatNumber = (val) => {
@ -126,32 +122,23 @@ export default function OwnerAccountBookPage() {
<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">جاري تحميل بيانات الحساب...</p>
<p className="text-gray-600">{t('accountBook.loading')}</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">
<div className="min-h-screen bg-gray-50 py-8" dir={i18n.language === 'ar' ? 'rtl' : 'ltr'}>
<Toaster position="top-center" reverseOrder={false} />
<div className="container mx-auto px-4 max-w-7xl">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4"
className="mb-8"
>
<div className="flex items-center gap-4">
<button
onClick={() => router.back()}
className="p-2 hover:bg-gray-200 rounded-xl transition-colors"
>
<ArrowLeft className="w-5 h-5 text-gray-600" />
</button>
<div>
<h1 className="text-3xl font-bold text-gray-900">دفتر الحسابات</h1>
</div>
</div>
<h1 className="text-3xl font-bold text-gray-900">{t('accountBook')}</h1>
<p className="text-gray-600">{t('financialRecords')}</p>
</motion.div>
<motion.div
@ -159,24 +146,24 @@ export default function OwnerAccountBookPage() {
animate={{ opacity: 1, y: 0 }}
className="mb-8"
>
<h2 className="text-xl font-bold text-gray-900 mb-4">نظرة مالية للمالك</h2>
<h2 className="text-xl font-bold text-gray-900 mb-4">{t('accountBook.ownerFinancialOverview')}</h2>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
<StatCard
title="الإيرادات"
title={t('accountBook.revenue')}
value={formatCurrency(stats.financialRevenue)}
icon={TrendingUp}
color="bg-emerald-500"
isNA={isNA(stats.financialRevenue)}
/>
<StatCard
title="العمولة"
title={t('accountBook.commission')}
value={formatCurrency(stats.financialCommission)}
icon={DollarSign}
color="bg-orange-500"
isNA={isNA(stats.financialCommission)}
/>
<StatCard
title="الرصيد المتبقي"
title={t('accountBook.remainingBalance')}
value={formatCurrency(stats.financialBalance)}
icon={DollarSign}
color="bg-blue-500"
@ -191,38 +178,38 @@ export default function OwnerAccountBookPage() {
transition={{ delay: 0.1 }}
className="mb-8"
>
<h2 className="text-xl font-bold text-gray-900 mb-4">إحصائيات العقارات المباشرة</h2>
<h2 className="text-xl font-bold text-gray-900 mb-4">{t('accountBook.directPropertyStats')}</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-6">
<StatCard
title="الإيرادات"
title={t('accountBook.revenue')}
value={formatCurrency(stats.directRevenue)}
icon={TrendingUp}
color="bg-emerald-500"
isNA={isNA(stats.directRevenue)}
/>
<StatCard
title="العمولة"
title={t('accountBook.commission')}
value={formatCurrency(stats.directCommission)}
icon={DollarSign}
color="bg-orange-500"
isNA={isNA(stats.directCommission)}
/>
<StatCard
title="الرصيد المتبقي"
title={t('accountBook.remainingBalance')}
value={formatCurrency(stats.directBalance)}
icon={DollarSign}
color="bg-blue-500"
isNA={isNA(stats.directBalance)}
/>
<StatCard
title="التقييم العام"
title={t('accountBook.overallRating')}
value={formatRating(stats.directRating)}
icon={Star}
color="bg-purple-500"
isNA={isNA(stats.directRating)}
/>
<StatCard
title="مرات التأجيل"
title={t('accountBook.postponements')}
value={formatNumber(stats.directPostponements)}
icon={Calendar}
color="bg-red-500"

View File

@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { useTranslation } from 'react-i18next';
import {
Calendar,
Home,
@ -37,6 +38,7 @@ import AuthService from '../../services/AuthService';
import Image from 'next/image';
const OwnerBookingCalendar = ({ property, onDateSelect, selectedDates }) => {
const { t } = useTranslation();
const [currentMonth, setCurrentMonth] = useState(new Date());
const [hoverDate, setHoverDate] = useState(null);
@ -53,8 +55,13 @@ const OwnerBookingCalendar = ({ property, onDateSelect, selectedDates }) => {
).getDay();
const monthNames = [
'يناير', 'فبراير', 'مارس', 'إبريل', 'مايو', 'يونيو',
'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'
t('january'), t('february'), t('march'), t('april'), t('may'), t('june'),
t('july'), t('august'), t('september'), t('october'), t('november'), t('december')
];
const dayNames = [
t('dayFull.sunday'), t('dayFull.monday'), t('dayFull.tuesday'), t('dayFull.wednesday'),
t('dayFull.thursday'), t('dayFull.friday'), t('dayFull.saturday')
];
const isDateBooked = (date) => {
@ -134,7 +141,6 @@ const OwnerBookingCalendar = ({ property, onDateSelect, selectedDates }) => {
return (
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
{/* رأس التقويم */}
<div className="flex items-center justify-between mb-6">
<button
onClick={() => setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1))}
@ -156,15 +162,10 @@ const OwnerBookingCalendar = ({ property, onDateSelect, selectedDates }) => {
</button>
</div>
{/* أيام الأسبوع */}
<div className="grid grid-cols-7 gap-1 mb-3 text-center text-sm font-medium text-gray-500">
<div>أحد</div>
<div>إثنين</div>
<div>ثلاثاء</div>
<div>أربعاء</div>
<div>خميس</div>
<div>جمعة</div>
<div>سبت</div>
{dayNames.map((name, i) => (
<div key={i}>{name}</div>
))}
</div>
<div className="grid grid-cols-7 gap-1">
@ -174,19 +175,19 @@ const OwnerBookingCalendar = ({ property, onDateSelect, selectedDates }) => {
<div className="flex flex-wrap gap-4 mt-6 pt-4 border-t border-gray-200 text-xs">
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-red-100 rounded" />
<span className="text-gray-600">محجوز</span>
<span className="text-gray-600">{t('booked')}</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-amber-500 rounded" />
<span className="text-gray-600">محدد</span>
<span className="text-gray-600">{t('selected')}</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-amber-100 rounded" />
<span className="text-gray-600">ضمن الفترة</span>
<span className="text-gray-600">{t('inRange')}</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 border-2 border-amber-500 rounded" />
<span className="text-gray-600">اليوم</span>
<span className="text-gray-600">{t('today')}</span>
</div>
</div>
</div>
@ -194,16 +195,18 @@ const OwnerBookingCalendar = ({ property, onDateSelect, selectedDates }) => {
};
const BookingCard = ({ booking, onViewDetails, onContact }) => {
const { t } = useTranslation();
const formatCurrency = (amount) => {
return amount?.toLocaleString() + ' ل.س';
return amount?.toLocaleString() + ' ' + t('syp');
};
const getStatusBadge = (status) => {
const statusConfig = {
pending: { label: 'قيد الانتظار', color: 'bg-yellow-100 text-yellow-800', icon: Clock },
confirmed: { label: 'مؤكد', color: 'bg-green-100 text-green-800', icon: CheckCircle },
cancelled: { label: 'ملغي', color: 'bg-red-100 text-red-800', icon: XCircle },
completed: { label: 'منتهي', color: 'bg-gray-100 text-gray-800', icon: CheckCircle }
pending: { label: t('pending'), color: 'bg-yellow-100 text-yellow-800', icon: Clock },
confirmed: { label: t('confirmed'), color: 'bg-green-100 text-green-800', icon: CheckCircle },
cancelled: { label: t('cancelled'), color: 'bg-red-100 text-red-800', icon: XCircle },
completed: { label: t('completed'), color: 'bg-gray-100 text-gray-800', icon: CheckCircle }
};
const config = statusConfig[status] || statusConfig.pending;
@ -237,7 +240,7 @@ const BookingCard = ({ booking, onViewDetails, onContact }) => {
</div>
<div className="text-left">
<div className="text-lg font-bold text-amber-600">{formatCurrency(booking.totalAmount)}</div>
<div className="text-xs text-gray-500">إجمالي المبلغ</div>
<div className="text-xs text-gray-500">{t('totalAmount')}</div>
</div>
</div>
@ -261,18 +264,18 @@ const BookingCard = ({ booking, onViewDetails, onContact }) => {
<div className="grid grid-cols-3 gap-3 mb-4 text-center">
<div className="bg-gray-50 p-2 rounded-lg">
<Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1" />
<div className="text-xs text-gray-500">من</div>
<div className="text-xs text-gray-500">{t('from')}</div>
<div className="text-sm font-medium">{booking.startDate}</div>
</div>
<div className="bg-gray-50 p-2 rounded-lg">
<Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1" />
<div className="text-xs text-gray-500">إلى</div>
<div className="text-xs text-gray-500">{t('to')}</div>
<div className="text-sm font-medium">{booking.endDate}</div>
</div>
<div className="bg-gray-50 p-2 rounded-lg">
<Clock className="w-4 h-4 text-amber-500 mx-auto mb-1" />
<div className="text-xs text-gray-500">المدة</div>
<div className="text-sm font-medium">{booking.days} يوم</div>
<div className="text-xs text-gray-500">{t('duration')}</div>
<div className="text-sm font-medium">{booking.days} {t('days')}</div>
</div>
</div>
@ -282,15 +285,8 @@ const BookingCard = ({ booking, onViewDetails, onContact }) => {
className="flex-1 bg-gray-100 text-gray-700 py-2 rounded-xl text-sm font-medium hover:bg-gray-200 transition-colors flex items-center justify-center gap-2"
>
<Eye className="w-4 h-4" />
التفاصيل
{t('details')}
</button>
{/* <button
onClick={() => onContact(booking)}
className="flex-1 bg-amber-500 text-white py-2 rounded-xl text-sm font-medium hover:bg-amber-600 transition-colors flex items-center justify-center gap-2"
>
<MessageCircle className="w-4 h-4" />
تواصل
</button> */}
</div>
</div>
</motion.div>
@ -298,10 +294,11 @@ const BookingCard = ({ booking, onViewDetails, onContact }) => {
};
const BookingDetailsModal = ({ booking, isOpen, onClose }) => {
const { t } = useTranslation();
if (!isOpen || !booking) return null;
const formatCurrency = (amount) => {
return amount?.toLocaleString() + ' ل.س';
return amount?.toLocaleString() + ' ' + t('syp');
};
return (
@ -321,80 +318,80 @@ const BookingDetailsModal = ({ booking, isOpen, onClose }) => {
>
<div className="sticky top-0 bg-gradient-to-r from-amber-500 to-amber-600 p-6 text-white">
<div className="flex justify-between items-center">
<h2 className="text-xl font-bold">تفاصيل الحجز</h2>
<h2 className="text-xl font-bold">{t('bookingDetails')}</h2>
<button onClick={onClose} className="p-1 hover:bg-white/20 rounded-full">
<XCircle className="w-6 h-6" />
</button>
</div>
<p className="text-amber-100 text-sm mt-1">رقم الحجز: #{booking.id}</p>
<p className="text-amber-100 text-sm mt-1">{t('bookingId', { id: booking.id })}</p>
</div>
<div className="p-6 space-y-6">
<div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-3">معلومات العقار</h3>
<h3 className="font-bold text-gray-900 mb-3">{t('propertyInfo')}</h3>
<div className="space-y-2">
<p><span className="text-gray-500">العقار:</span> {booking.propertyTitle}</p>
<p><span className="text-gray-500">الموقع:</span> {booking.location}</p>
<p><span className="text-gray-500">{t('property')}:</span> {booking.propertyTitle}</p>
<p><span className="text-gray-500">{t('location')}:</span> {booking.location}</p>
{booking.propertyDetails && (
<div className="flex gap-3 mt-2">
<span className="text-sm bg-white px-2 py-1 rounded-lg">{booking.propertyDetails.bedrooms} غرف</span>
<span className="text-sm bg-white px-2 py-1 rounded-lg">{booking.propertyDetails.bathrooms} حمامات</span>
<span className="text-sm bg-white px-2 py-1 rounded-lg">{booking.propertyDetails.area} م²</span>
<span className="text-sm bg-white px-2 py-1 rounded-lg">{booking.propertyDetails.bedrooms} {t('rooms')}</span>
<span className="text-sm bg-white px-2 py-1 rounded-lg">{booking.propertyDetails.bathrooms} {t('bathrooms')}</span>
<span className="text-sm bg-white px-2 py-1 rounded-lg">{booking.propertyDetails.area} {t('sqm')}</span>
</div>
)}
</div>
</div>
<div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-3">معلومات المستأجر</h3>
<h3 className="font-bold text-gray-900 mb-3">{t('tenantInfo')}</h3>
<div className="space-y-2">
<p><span className="text-gray-500">الاسم:</span> {booking.tenantName}</p>
<p><span className="text-gray-500">البريد الإلكتروني:</span> {booking.tenantEmail}</p>
<p><span className="text-gray-500">رقم الهاتف:</span> {booking.tenantPhone}</p>
<p><span className="text-gray-500">{t('name')}</span> {booking.tenantName}</p>
<p><span className="text-gray-500">{t('email')}</span> {booking.tenantEmail}</p>
<p><span className="text-gray-500">{t('phone')}</span> {booking.tenantPhone}</p>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-3">تفاصيل الحجز</h3>
<h3 className="font-bold text-gray-900 mb-3">{t('bookingDetails')}</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-gray-500">تاريخ البداية</p>
<p className="text-gray-500">{t('startDate')}</p>
<p className="font-medium">{booking.startDate}</p>
</div>
<div>
<p className="text-gray-500">تاريخ النهاية</p>
<p className="text-gray-500">{t('endDate')}</p>
<p className="font-medium">{booking.endDate}</p>
</div>
<div>
<p className="text-gray-500">عدد الأيام</p>
<p className="font-medium">{booking.days} يوم</p>
<p className="text-gray-500">{t('durationDays', { days: booking.days })}</p>
<p className="font-medium">{booking.days} {t('days')}</p>
</div>
<div>
<p className="text-gray-500">حالة الحجز</p>
<p className="font-medium">{booking.status === 'pending' ? 'قيد الانتظار' :
booking.status === 'confirmed' ? 'مؤكد' :
booking.status === 'cancelled' ? 'ملغي' : 'منتهي'}</p>
<p className="text-gray-500">{t('bookingStatus')}</p>
<p className="font-medium">{booking.status === 'pending' ? t('pending') :
booking.status === 'confirmed' ? t('confirmed') :
booking.status === 'cancelled' ? t('cancelled') : t('completed')}</p>
</div>
</div>
</div>
<div className="bg-amber-50 p-4 rounded-xl">
<h3 className="font-bold text-amber-700 mb-3">المعلومات المالية</h3>
<h3 className="font-bold text-amber-700 mb-3">{t('financialInfo')}</h3>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-gray-600">السعر اليومي</span>
<span className="text-gray-600">{t('dailyPrice')}</span>
<span className="font-medium">{formatCurrency(booking.dailyPrice)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">المدة ({booking.days} أيام)</span>
<span className="text-gray-600">{t('durationDays', { days: booking.days })}</span>
<span className="font-medium">{formatCurrency(booking.dailyPrice * booking.days)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">سلفة الضمان</span>
<span className="text-gray-600">{t('securityDeposit')}</span>
<span className="font-medium">{formatCurrency(booking.securityDeposit || 0)}</span>
</div>
<div className="flex justify-between pt-2 border-t border-amber-200 font-bold">
<span className="text-gray-900">الإجمالي</span>
<span className="text-gray-900">{t('total')}</span>
<span className="text-amber-600 text-lg">{formatCurrency(booking.totalAmount)}</span>
</div>
</div>
@ -402,7 +399,7 @@ const BookingDetailsModal = ({ booking, isOpen, onClose }) => {
{booking.notes && (
<div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-2">ملاحظات</h3>
<h3 className="font-bold text-gray-900 mb-2">{t('notes')}</h3>
<p className="text-gray-600">{booking.notes}</p>
</div>
)}
@ -413,6 +410,7 @@ const BookingDetailsModal = ({ booking, isOpen, onClose }) => {
};
export default function OwnerBookingsPage() {
const { t, i18n } = useTranslation();
const router = useRouter();
const [user, setUser] = useState(null);
const [bookings, setBookings] = useState([]);
@ -449,10 +447,10 @@ export default function OwnerBookingsPage() {
{
id: 'BK001',
propertyId: 1,
propertyTitle: 'فيلا فاخرة في المزة',
location: 'دمشق، المزة',
propertyTitle: t('ownerBookings.mockB1Title'),
location: t('ownerBookings.mockB1Location'),
propertyDetails: { bedrooms: 5, bathrooms: 4, area: 450 },
tenantName: 'أحمد محمد',
tenantName: t('ownerBookings.mockB1Tenant'),
tenantEmail: 'ahmed@example.com',
tenantPhone: '0933111222',
startDate: '2024-03-10',
@ -463,15 +461,15 @@ export default function OwnerBookingsPage() {
securityDeposit: 500000,
status: 'confirmed',
createdAt: '2024-02-25',
notes: 'طلب الحجز من خلال الموقع'
notes: t('ownerBookings.mockB1Notes')
},
{
id: 'BK002',
propertyId: 2,
propertyTitle: 'شقة حديثة في الشهباء',
location: 'حلب، الشهباء',
propertyTitle: t('ownerBookings.mockB2Title'),
location: t('ownerBookings.mockB2Location'),
propertyDetails: { bedrooms: 3, bathrooms: 2, area: 180 },
tenantName: 'سارة أحمد',
tenantName: t('ownerBookings.mockB2Tenant'),
tenantEmail: 'sara@example.com',
tenantPhone: '0945123789',
startDate: '2024-03-05',
@ -482,15 +480,15 @@ export default function OwnerBookingsPage() {
securityDeposit: 250000,
status: 'pending',
createdAt: '2024-02-24',
notes: 'تحتاج إلى تأكيد'
notes: t('ownerBookings.mockB2Notes')
},
{
id: 'BK003',
propertyId: 3,
propertyTitle: 'بيت عائلي في بابا عمرو',
location: 'حمص، بابا عمرو',
propertyTitle: t('ownerBookings.mockB3Title'),
location: t('ownerBookings.mockB3Location'),
propertyDetails: { bedrooms: 4, bathrooms: 3, area: 300 },
tenantName: 'محمد الحلبي',
tenantName: t('ownerBookings.mockB3Tenant'),
tenantEmail: 'mohammed@example.com',
tenantPhone: '0956123456',
startDate: '2024-02-20',
@ -501,7 +499,7 @@ export default function OwnerBookingsPage() {
securityDeposit: 500000,
status: 'completed',
createdAt: '2024-02-15',
notes: 'تم إنهاء الإيجار بنجاح'
notes: t('ownerBookings.mockB3Notes')
}
];
setBookings(mockBookings);
@ -518,7 +516,7 @@ export default function OwnerBookingsPage() {
};
const handleContact = (booking) => {
toast.success(`جاري فتح محادثة مع ${booking.tenantName}`, {
toast.success(t('openingChat', { name: booking.tenantName }), {
icon: '💬',
style: { background: '#dcfce7', color: '#166534' }
});
@ -531,7 +529,7 @@ export default function OwnerBookingsPage() {
setBookings(updatedBookings);
setFilteredBookings(updatedBookings);
localStorage.setItem('ownerBookings', JSON.stringify(updatedBookings));
toast.success(`تم تحديث حالة الحجز بنجاح`);
toast.success(t('statusUpdated'));
};
const statusCounts = {
@ -547,14 +545,14 @@ export default function OwnerBookingsPage() {
<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">جاري تحميل الحجوزات...</p>
<p className="text-gray-600">{t('loading')}</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="min-h-screen bg-gray-50 py-8" dir={i18n.language === 'ar' ? 'rtl' : 'ltr'}>
<Toaster position="top-center" reverseOrder={false} />
<BookingDetailsModal
@ -570,8 +568,8 @@ export default function OwnerBookingsPage() {
className="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4"
>
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">حجوزاتي</h1>
<p className="text-gray-600">مرحباً {user?.name}، لديك {bookings.length} حجز</p>
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t('myBookings')}</h1>
<p className="text-gray-600">{t('welcomeBookings', { name: user?.name, count: bookings.length })}</p>
</div>
<div className="flex gap-3">
@ -580,12 +578,8 @@ export default function OwnerBookingsPage() {
className="px-4 py-2 bg-white border border-gray-300 rounded-xl text-gray-700 hover:bg-gray-50 transition-colors flex items-center gap-2"
>
<Calendar className="w-5 h-5" />
{showCalendar ? 'إخفاء التقويم' : 'عرض التقويم'}
{showCalendar ? t('hideCalendar') : t('showCalendar')}
</button>
{/* <button className="px-4 py-2 bg-green-600 text-white rounded-xl hover:bg-green-700 transition-colors flex items-center gap-2">
<Download className="w-5 h-5" />
تصدير التقرير
</button> */}
</div>
</motion.div>
@ -598,7 +592,7 @@ export default function OwnerBookingsPage() {
onClick={() => setFilterStatus('all')}
>
<div className="text-2xl font-bold text-gray-900">{statusCounts.all}</div>
<div className="text-sm text-gray-600">جميع الحجوزات</div>
<div className="text-sm text-gray-600">{t('allBookings')}</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
@ -610,7 +604,7 @@ export default function OwnerBookingsPage() {
onClick={() => setFilterStatus('pending')}
>
<div className="text-2xl font-bold text-yellow-600">{statusCounts.pending}</div>
<div className="text-sm text-gray-600">قيد الانتظار</div>
<div className="text-sm text-gray-600">{t('pending')}</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
@ -622,7 +616,7 @@ export default function OwnerBookingsPage() {
onClick={() => setFilterStatus('confirmed')}
>
<div className="text-2xl font-bold text-green-600">{statusCounts.confirmed}</div>
<div className="text-sm text-gray-600">مؤكدة</div>
<div className="text-sm text-gray-600">{t('confirmed')}</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
@ -634,7 +628,7 @@ export default function OwnerBookingsPage() {
onClick={() => setFilterStatus('completed')}
>
<div className="text-2xl font-bold text-gray-600">{statusCounts.completed}</div>
<div className="text-sm text-gray-600">منتهية</div>
<div className="text-sm text-gray-600">{t('completed')}</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
@ -646,7 +640,7 @@ export default function OwnerBookingsPage() {
onClick={() => setFilterStatus('cancelled')}
>
<div className="text-2xl font-bold text-red-600">{statusCounts.cancelled}</div>
<div className="text-sm text-gray-600">ملغية</div>
<div className="text-sm text-gray-600">{t('cancelled')}</div>
</motion.div>
</div>
@ -655,7 +649,7 @@ export default function OwnerBookingsPage() {
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
type="text"
placeholder="ابحث باسم العقار أو المستأجر.."
placeholder={t('searchBookings')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
@ -667,21 +661,21 @@ export default function OwnerBookingsPage() {
value={dateRange.start}
onChange={(e) => setDateRange({...dateRange, start: e.target.value})}
className="px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
placeholder="من تاريخ"
placeholder={t('filterDateFrom')}
/>
<input
type="date"
value={dateRange.end}
onChange={(e) => setDateRange({...dateRange, end: e.target.value})}
className="px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
placeholder="إلى تاريخ"
placeholder={t('filterDateTo')}
/>
{(dateRange.start || dateRange.end) && (
<button
onClick={() => setDateRange({ start: '', end: '' })}
className="px-4 py-3 bg-gray-100 text-gray-700 rounded-xl hover:bg-gray-200 transition-colors"
>
مسح
{t('clear')}
</button>
)}
</div>
@ -708,16 +702,16 @@ export default function OwnerBookingsPage() {
<div className="w-24 h-24 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Calendar className="w-12 h-12 text-amber-600" />
</div>
<h3 className="text-xl font-bold text-gray-900 mb-2">لا توجد حجوزات</h3>
<h3 className="text-xl font-bold text-gray-900 mb-2">{t('noBookings')}</h3>
<p className="text-gray-600 mb-4">
{filterStatus !== 'all' ? 'لا توجد حجوزات في هذه الفئة' : 'لم يتم استلام أي حجوزات بعد'}
{filterStatus !== 'all' ? t('noBookingsInCategory') : t('noBookingsReceived')}
</p>
{filterStatus !== 'all' && (
<button
onClick={() => setFilterStatus('all')}
className="inline-flex items-center gap-2 bg-amber-500 text-white px-6 py-3 rounded-xl font-medium hover:bg-amber-600"
>
عرض جميع الحجوزات
{t('viewAllBookings')}
</button>
)}
</motion.div>
@ -736,4 +730,4 @@ export default function OwnerBookingsPage() {
</div>
</div>
);
}
}

View File

@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { useTranslation } from 'react-i18next';
import {
Calendar,
ChevronLeft,
@ -39,13 +40,14 @@ import toast, { Toaster } from 'react-hot-toast';
import AuthService from '../../services/AuthService';
const MonthlyCalendar = ({ properties, selectedPropertyId, onDateClick, onPropertySelect }) => {
const { t } = useTranslation();
const [currentMonth, setCurrentMonth] = useState(new Date());
const [selectedDate, setSelectedDate] = useState(null);
const [viewType, setViewType] = useState('grid');
const monthNames = [
'يناير', 'فبراير', 'مارس', 'إبريل', 'مايو', 'يونيو',
'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'
t('calendar.month1'), t('calendar.month2'), t('calendar.month3'), t('calendar.month4'), t('calendar.month5'), t('calendar.month6'),
t('calendar.month7'), t('calendar.month8'), t('calendar.month9'), t('calendar.month10'), t('calendar.month11'), t('calendar.month12')
];
const daysInMonth = new Date(
@ -76,17 +78,17 @@ const MonthlyCalendar = ({ properties, selectedPropertyId, onDateClick, onProper
const totalProperties = properties.length;
const bookedCount = properties.filter(p => isDateBookedForProperty(date, p)).length;
if (bookedCount === 0) return { status: 'all_available', label: 'جميع العقارات متاحة', color: 'bg-green-100 text-green-800' };
if (bookedCount === totalProperties) return { status: 'all_booked', label: 'جميع العقارات محجوزة', color: 'bg-red-100 text-red-800' };
return { status: 'partial', label: `${bookedCount}/${totalProperties} محجوز`, color: 'bg-yellow-100 text-yellow-800' };
if (bookedCount === 0) return { status: 'all_available', label: t('calendar.allAvailable'), color: 'bg-green-100 text-green-800' };
if (bookedCount === totalProperties) return { status: 'all_booked', label: t('calendar.allBooked'), color: 'bg-red-100 text-red-800' };
return { status: 'partial', label: t('calendar.bookedCount', { count: bookedCount, total: totalProperties }), color: 'bg-yellow-100 text-yellow-800' };
} else {
const property = properties.find(p => p.id === selectedPropertyId);
if (!property) return { status: 'no_property', label: 'غير متاح', color: 'bg-gray-100 text-gray-500' };
if (!property) return { status: 'no_property', label: t('calendar.notAvailable'), color: 'bg-gray-100 text-gray-500' };
const isBooked = isDateBookedForProperty(date, property);
return {
status: isBooked ? 'booked' : 'available',
label: isBooked ? 'محجوز' : 'متاح',
label: isBooked ? t('calendar.booked') : t('calendar.available'),
color: isBooked ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800'
};
}
@ -171,7 +173,7 @@ const MonthlyCalendar = ({ properties, selectedPropertyId, onDateClick, onProper
onClick={() => setCurrentMonth(new Date())}
className="px-4 py-2 bg-amber-500 text-white rounded-xl text-sm hover:bg-amber-600 transition-colors"
>
اليوم
{t('calendar.today')}
</button>
<div className="flex border border-gray-200 rounded-xl overflow-hidden">
<button
@ -192,7 +194,7 @@ const MonthlyCalendar = ({ properties, selectedPropertyId, onDateClick, onProper
</div>
<div className="grid grid-cols-7 gap-1 p-4 bg-gray-50 border-b border-gray-200">
{['أحد', 'إثنين', 'ثلاثاء', 'أربعاء', 'خميس', 'جمعة', 'سبت'].map((day, index) => (
{[t('calendar.day1'), t('calendar.day2'), t('calendar.day3'), t('calendar.day4'), t('calendar.day5'), t('calendar.day6'), t('calendar.day7')].map((day, index) => (
<div key={index} className="text-center text-sm font-medium text-gray-600 py-2">
{day}
</div>
@ -209,23 +211,23 @@ const MonthlyCalendar = ({ properties, selectedPropertyId, onDateClick, onProper
<div className="flex flex-wrap gap-4 justify-center text-xs">
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-green-100 rounded" />
<span className="text-gray-600">متاح</span>
<span className="text-gray-600">{t('calendar.available')}</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-red-100 rounded" />
<span className="text-gray-600">محجوز</span>
<span className="text-gray-600">{t('calendar.booked')}</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-yellow-100 rounded" />
<span className="text-gray-600">محجوز جزئياً</span>
<span className="text-gray-600">{t('calendar.partiallyBooked')}</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 ring-2 ring-amber-500 rounded" />
<span className="text-gray-600">اليوم</span>
<span className="text-gray-600">{t('calendar.today')}</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 ring-2 ring-blue-500 rounded" />
<span className="text-gray-600">محدد</span>
<span className="text-gray-600">{t('calendar.selected')}</span>
</div>
</div>
</div>
@ -234,8 +236,9 @@ const MonthlyCalendar = ({ properties, selectedPropertyId, onDateClick, onProper
};
const PropertyCalendarList = ({ properties, selectedDate, onPropertyClick }) => {
const { t } = useTranslation();
const formatCurrency = (amount) => {
return amount?.toLocaleString() + ' ل.س';
return amount?.toLocaleString() + ' ' + t('calendar.syp');
};
const isDateBooked = (property, date) => {
@ -264,8 +267,8 @@ const PropertyCalendarList = ({ properties, selectedDate, onPropertyClick }) =>
return (
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-12 text-center">
<CalendarDays className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-lg font-bold text-gray-700 mb-2">اختر تاريخاً</h3>
<p className="text-gray-500">اضغط على أي يوم في التقويم لعرض حالة العقارات في ذلك التاريخ</p>
<h3 className="text-lg font-bold text-gray-700 mb-2">{t('calendar.selectDate')}</h3>
<p className="text-gray-500">{t('calendar.clickDayHint')}</p>
</div>
);
}
@ -282,7 +285,7 @@ const PropertyCalendarList = ({ properties, selectedDate, onPropertyClick }) =>
<div className="p-4 md:p-6 border-b border-gray-200 bg-gradient-to-r from-amber-50 to-amber-100">
<h3 className="text-lg font-bold text-gray-900 flex items-center gap-2">
<CalendarDays className="w-5 h-5 text-amber-500" />
حالة العقارات في تاريخ: {formattedDate}
{t('calendar.statusForDate')} {formattedDate}
</h3>
</div>
@ -306,7 +309,7 @@ const PropertyCalendarList = ({ properties, selectedDate, onPropertyClick }) =>
<span className={`px-2 py-1 rounded-lg text-xs font-medium ${
isBooked ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800'
}`}>
{isBooked ? 'محجوز' : 'متاح'}
{isBooked ? t('calendar.booked') : t('calendar.available')}
</span>
</div>
<div className="flex items-center gap-1 text-gray-500 text-sm mb-2">
@ -316,26 +319,26 @@ const PropertyCalendarList = ({ properties, selectedDate, onPropertyClick }) =>
<div className="flex flex-wrap gap-3 text-sm text-gray-600">
<div className="flex items-center gap-1">
<Bed className="w-4 h-4" />
<span>{property.bedrooms} غرف</span>
<span>{property.bedrooms} {t('calendar.rooms')}</span>
</div>
<div className="flex items-center gap-1">
<Bath className="w-4 h-4" />
<span>{property.bathrooms} حمامات</span>
<span>{property.bathrooms} {t('calendar.bathrooms')}</span>
</div>
<div className="flex items-center gap-1">
<Square className="w-4 h-4" />
<span>{property.area} م²</span>
<span>{property.area} {t('calendar.sqm')}</span>
</div>
</div>
</div>
<div className="text-right">
<div className="text-lg font-bold text-amber-600">{formatCurrency(property.price)}</div>
<div className="text-xs text-gray-500">/يوم</div>
<div className="text-xs text-gray-500">{t('calendar.perDay')}</div>
{isBooked && booking && (
<div className="mt-2 text-xs text-gray-500">
<div>مستأجر: {booking.tenantName || 'غير معروف'}</div>
<div>من: {booking.startDate} إلى {booking.endDate}</div>
<div>{t('calendar.tenant')}: {booking.tenantName || t('calendar.unknown')}</div>
<div>{t('calendar.from')} {booking.startDate} {t('calendar.to')} {booking.endDate}</div>
</div>
)}
</div>
@ -349,10 +352,11 @@ const PropertyCalendarList = ({ properties, selectedDate, onPropertyClick }) =>
};
const PropertyDetailsModal = ({ property, isOpen, onClose }) => {
const { t } = useTranslation();
if (!isOpen || !property) return null;
const formatCurrency = (amount) => {
return amount?.toLocaleString() + ' ل.س';
return amount?.toLocaleString() + ' ' + t('calendar.syp');
};
return (
@ -385,7 +389,7 @@ const PropertyDetailsModal = ({ property, isOpen, onClose }) => {
<div className="p-6 space-y-6">
{property.images && property.images.length > 0 && (
<div>
<h3 className="font-bold text-gray-900 mb-3">صور العقار</h3>
<h3 className="font-bold text-gray-900 mb-3">{t('calendar.propertyImages')}</h3>
<div className="grid grid-cols-2 gap-3">
{property.images.slice(0, 4).map((image, index) => (
<div key={index} className="relative h-32 rounded-lg overflow-hidden bg-gray-100">
@ -400,28 +404,28 @@ const PropertyDetailsModal = ({ property, isOpen, onClose }) => {
<div className="bg-gray-50 p-3 rounded-xl text-center">
<Bed className="w-5 h-5 text-amber-500 mx-auto mb-1" />
<div className="text-sm font-bold">{property.bedrooms}</div>
<div className="text-xs text-gray-500">غرف نوم</div>
<div className="text-xs text-gray-500">{t('calendar.bedrooms')}</div>
</div>
<div className="bg-gray-50 p-3 rounded-xl text-center">
<Bath className="w-5 h-5 text-amber-500 mx-auto mb-1" />
<div className="text-sm font-bold">{property.bathrooms}</div>
<div className="text-xs text-gray-500">حمامات</div>
<div className="text-xs text-gray-500">{t('calendar.bathrooms')}</div>
</div>
<div className="bg-gray-50 p-3 rounded-xl text-center">
<Square className="w-5 h-5 text-amber-500 mx-auto mb-1" />
<div className="text-sm font-bold">{property.area}</div>
<div className="text-xs text-gray-500">م²</div>
<div className="text-xs text-gray-500">{t('calendar.sqm')}</div>
</div>
<div className="bg-gray-50 p-3 rounded-xl text-center">
<DollarSign className="w-5 h-5 text-amber-500 mx-auto mb-1" />
<div className="text-sm font-bold">{formatCurrency(property.price)}</div>
<div className="text-xs text-gray-500">/يوم</div>
<div className="text-xs text-gray-500">{t('calendar.perDay')}</div>
</div>
</div>
{property.features && property.features.length > 0 && (
<div>
<h3 className="font-bold text-gray-900 mb-3">المميزات</h3>
<h3 className="font-bold text-gray-900 mb-3">{t('calendar.features')}</h3>
<div className="flex flex-wrap gap-2">
{property.features.map((feature, index) => (
<span key={index} className="px-2 py-1 bg-gray-100 text-gray-700 rounded-lg text-xs">
@ -436,14 +440,14 @@ const PropertyDetailsModal = ({ property, isOpen, onClose }) => {
<div>
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2">
<Clock className="w-4 h-4 text-amber-500" />
الحجوزات القادمة
{t('calendar.upcomingBookings')}
</h3>
<div className="space-y-2">
{property.bookings.slice(0, 3).map((booking, index) => (
<div key={index} className="bg-gray-50 p-3 rounded-lg flex justify-between items-center">
<div>
<p className="font-medium text-gray-900">{booking.startDate} - {booking.endDate}</p>
<p className="text-xs text-gray-500">مستأجر: {booking.tenantName || 'غير معروف'}</p>
<p className="text-xs text-gray-500">{t('calendar.tenant')}: {booking.tenantName || t('calendar.unknown')}</p>
</div>
<span className="text-sm font-bold text-amber-600">{formatCurrency(booking.totalAmount)}</span>
</div>
@ -458,13 +462,13 @@ const PropertyDetailsModal = ({ property, isOpen, onClose }) => {
href={`/owner/properties/edit?id=${property.id}`}
className="flex-1 bg-amber-500 text-white py-3 rounded-xl text-center font-medium hover:bg-amber-600 transition-colors"
>
تعديل العقار
{t('calendar.editProperty')}
</Link>
<button
onClick={() => window.location.href = `/owner/bookings?property=${property.id}`}
className="flex-1 bg-gray-100 text-gray-700 py-3 rounded-xl text-center font-medium hover:bg-gray-200 transition-colors"
>
عرض الحجوزات
{t('calendar.viewBookings')}
</button>
</div>
</motion.div>
@ -473,6 +477,7 @@ const PropertyDetailsModal = ({ property, isOpen, onClose }) => {
};
export default function OwnerCalendarPage() {
const { t, i18n } = useTranslation();
const router = useRouter();
const [user, setUser] = useState(null);
const [properties, setProperties] = useState([]);
@ -509,44 +514,44 @@ export default function OwnerCalendarPage() {
const mockProperties = [
{
id: 1,
title: 'فيلا فاخرة في المزة',
location: 'دمشق، المزة',
title: t('calendar.mockVillaTitle'),
location: t('calendar.mockVillaLocation'),
bedrooms: 5,
bathrooms: 4,
area: 450,
price: 500000,
features: ['مسبح', 'حديقة خاصة', 'موقف سيارات', 'أمن 24/7'],
features: [t('swimmingPool'), t('privateGarden'), t('parking'), t('propertyService.security247')],
images: ['/villa1.jpg'],
status: 'available',
bookings: [
{ startDate: '2024-03-10', endDate: '2024-03-15', totalAmount: 2500000, tenantName: 'أحمد محمد' },
{ startDate: '2024-03-20', endDate: '2024-03-25', totalAmount: 2500000, tenantName: 'سارة أحمد' }
{ startDate: '2024-03-10', endDate: '2024-03-15', totalAmount: 2500000, tenantName: t('calendar.mockTenant1') },
{ startDate: '2024-03-20', endDate: '2024-03-25', totalAmount: 2500000, tenantName: t('calendar.mockTenant2') }
]
},
{
id: 2,
title: 'شقة حديثة في الشهباء',
location: 'حلب، الشهباء',
title: t('calendar.mockApartmentTitle'),
location: t('calendar.mockApartmentLocation'),
bedrooms: 3,
bathrooms: 2,
area: 180,
price: 250000,
features: ['مطبخ مجهز', 'بلكونة', 'موقف سيارات', 'مصعد'],
features: [t('equippedKitchen'), t('balcony'), t('parking'), t('propertyService.elevator')],
images: ['/apartment1.jpg'],
status: 'available',
bookings: [
{ startDate: '2024-03-05', endDate: '2024-03-08', totalAmount: 750000, tenantName: 'محمد علي' }
{ startDate: '2024-03-05', endDate: '2024-03-08', totalAmount: 750000, tenantName: t('calendar.mockTenant3') }
]
},
{
id: 3,
title: 'بيت عائلي في بابا عمرو',
location: 'حمص، بابا عمرو',
title: t('calendar.mockHouseTitle'),
location: t('calendar.mockHouseLocation'),
bedrooms: 4,
bathrooms: 3,
area: 300,
price: 350000,
features: ['حديقة كبيرة', 'موقف سيارات', 'مدفأة', 'كراج'],
features: [t('largeGarden'), t('parking'), t('fireplace'), t('garage')],
images: ['/house1.jpg'],
status: 'booked',
bookings: []
@ -586,14 +591,14 @@ export default function OwnerCalendarPage() {
<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">جاري تحميل التقويم...</p>
<p className="text-gray-600">{t('calendar.loading')}</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="min-h-screen bg-gray-50 py-8" dir={i18n.language === 'ar' ? 'rtl' : 'ltr'}>
<Toaster position="top-center" reverseOrder={false} />
<PropertyDetailsModal
@ -609,8 +614,8 @@ export default function OwnerCalendarPage() {
className="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4"
>
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">تقويم العقارات</h1>
<p className="text-gray-600">مرحباً {user?.name}، تتبع حالة عقاراتك عبر التقويم</p>
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t('calendar.pageTitle')}</h1>
<p className="text-gray-600">{t('calendar.welcomeMessage', { name: user?.name })}</p>
</div>
<div className="flex gap-3">
@ -619,7 +624,7 @@ export default function OwnerCalendarPage() {
className="px-4 py-2 bg-white border border-gray-300 rounded-xl text-gray-700 hover:bg-gray-50 transition-colors flex items-center gap-2"
>
<Filter className="w-5 h-5" />
فلترة العقارات
{t('calendar.filterProperties')}
<ChevronDown className={`w-4 h-4 transition-transform ${showFilters ? 'rotate-180' : ''}`} />
</button>
{/* <button className="px-4 py-2 bg-green-600 text-white rounded-xl hover:bg-green-700 transition-colors flex items-center gap-2">
@ -638,7 +643,7 @@ export default function OwnerCalendarPage() {
>
<Building className="w-6 h-6 text-amber-500 mx-auto mb-2" />
<div className="text-2xl font-bold text-gray-900">{calendarStats.totalProperties}</div>
<div className="text-sm text-gray-600">إجمالي العقارات</div>
<div className="text-sm text-gray-600">{t('calendar.totalProperties')}</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
@ -648,7 +653,7 @@ export default function OwnerCalendarPage() {
>
<CheckCircle className="w-6 h-6 text-green-500 mx-auto mb-2" />
<div className="text-2xl font-bold text-green-600">{calendarStats.availableToday}</div>
<div className="text-sm text-gray-600">متاح اليوم</div>
<div className="text-sm text-gray-600">{t('calendar.availableToday')}</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
@ -658,7 +663,7 @@ export default function OwnerCalendarPage() {
>
<XCircle className="w-6 h-6 text-red-500 mx-auto mb-2" />
<div className="text-2xl font-bold text-red-600">{calendarStats.bookedToday}</div>
<div className="text-sm text-gray-600">محجوز اليوم</div>
<div className="text-sm text-gray-600">{t('calendar.bookedToday')}</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
@ -668,7 +673,7 @@ export default function OwnerCalendarPage() {
>
<CalendarDays className="w-6 h-6 text-blue-500 mx-auto mb-2" />
<div className="text-2xl font-bold text-blue-600">{calendarStats.upcomingBookings}</div>
<div className="text-sm text-gray-600">حجوزات قادمة</div>
<div className="text-sm text-gray-600">{t('calendar.upcomingBookings')}</div>
</motion.div>
</div>
@ -682,13 +687,13 @@ export default function OwnerCalendarPage() {
>
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-4">
<div className="flex flex-wrap gap-3 items-center">
<label className="text-sm font-medium text-gray-700">اختر عقاراً:</label>
<label className="text-sm font-medium text-gray-700">{t('calendar.selectProperty')}</label>
<select
value={selectedPropertyId}
onChange={(e) => setSelectedPropertyId(e.target.value)}
className="px-4 py-2 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
>
<option value="all">جميع العقارات</option>
<option value="all">{t('calendar.allProperties')}</option>
{properties.map((property) => (
<option key={property.id} value={property.id}>{property.title}</option>
))}
@ -697,7 +702,7 @@ export default function OwnerCalendarPage() {
onClick={() => setSelectedPropertyId('all')}
className="px-3 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
>
إعادة تعيين
{t('calendar.reset')}
</button>
</div>
</div>
@ -729,7 +734,7 @@ export default function OwnerCalendarPage() {
className="mt-6 text-center text-sm text-gray-500"
>
<AlertCircle className="w-4 h-4 inline ml-1" />
اضغط على أي عقار لعرض التفاصيل الكاملة
{t('calendar.clickPropertyHint')}
</motion.div>
)}
</div>

View File

@ -298,9 +298,11 @@ import { useRouter } from 'next/navigation';
import { Download, Loader2 } from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
import * as XLSX from 'xlsx';
import { useTranslation } from 'react-i18next';
import AuthService from '@/app/services/AuthService';
export default function OwnerProfitsPage() {
const { t, i18n } = useTranslation();
const router = useRouter();
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
@ -404,29 +406,29 @@ export default function OwnerProfitsPage() {
const handleExportReport = () => {
try {
const exportData = tableData.map((row) => ({
'العقار': row.property,
'رقم الحجز': row.bookingNumber,
'من تاريخ': row.fromDate,
'حتى تاريخ': row.toDate,
'العروض المستلم': row.amountReceived,
'عمولة المنصة': row.platformCommission,
'ربح المنصة (5%)': row.platformProfit,
'المستحق للمالك': row.ownerDue,
'تم التحويل للمالك': row.transferredToOwner,
'رقم وصل التحويل': row.transferReceipt,
[t('profits.colProperty')]: row.property,
[t('profits.colBookingNumber')]: row.bookingNumber,
[t('profits.colFromDate')]: row.fromDate,
[t('profits.colToDate')]: row.toDate,
[t('profits.colAmountReceived')]: row.amountReceived,
[t('profits.colPlatformCommission')]: row.platformCommission,
[t('profits.colPlatformProfit')]: row.platformProfit,
[t('profits.colOwnerDue')]: row.ownerDue,
[t('profits.colTransferred')]: row.transferredToOwner,
[t('profits.colTransferReceipt')]: row.transferReceipt,
}));
exportData.push({
'العقار': 'الإجمالي العام',
'رقم الحجز': '',
'من تاريخ': '',
'حتى تاريخ': '',
'العروض المستلم': totals.totalAmountReceived,
'عمولة المنصة': totals.totalCommission,
'ربح المنصة (5%)': totals.totalPlatformProfit,
'المستحق للمالك': totals.totalOwnerDue,
'تم التحويل للمالك': totals.totalTransferred,
'رقم وصل التحويل': '—',
[t('profits.colProperty')]: t('profits.totalGeneral'),
[t('profits.colBookingNumber')]: '',
[t('profits.colFromDate')]: '',
[t('profits.colToDate')]: '',
[t('profits.colAmountReceived')]: totals.totalAmountReceived,
[t('profits.colPlatformCommission')]: totals.totalCommission,
[t('profits.colPlatformProfit')]: totals.totalPlatformProfit,
[t('profits.colOwnerDue')]: totals.totalOwnerDue,
[t('profits.colTransferred')]: totals.totalTransferred,
[t('profits.colTransferReceipt')]: '—',
});
const worksheet = XLSX.utils.json_to_sheet(exportData);
@ -445,14 +447,14 @@ export default function OwnerProfitsPage() {
worksheet['!cols'] = colWidths;
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'أرباح المالك');
XLSX.utils.book_append_sheet(workbook, worksheet, t('profits.sheetName'));
XLSX.writeFile(workbook, `تقرير_الأرباح_${new Date().toISOString().slice(0,19).replace(/:/g, '-')}.xlsx`);
XLSX.writeFile(workbook, `${t('profits.fileName')}${new Date().toISOString().slice(0,19).replace(/:/g, '-')}.xlsx`);
toast.success('تم تصدير التقرير بنجاح!');
toast.success(t('profits.exportSuccess'));
} catch (error) {
console.error('خطأ في التصدير:', error);
toast.error('حدث خطأ أثناء تصدير التقرير');
console.error(t('profits.exportErrorLog'), error);
toast.error(t('profits.exportError'));
}
};
@ -461,14 +463,14 @@ export default function OwnerProfitsPage() {
<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">جاري تحميل بيانات الأرباح...</p>
<p className="text-gray-600">{t('profits.loading')}</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">
<div className="min-h-screen bg-gray-50 py-8" dir={i18n.language === 'ar' ? 'rtl' : 'ltr'}>
<Toaster position="top-center" reverseOrder={false} />
<div className="container mx-auto px-4 max-w-7xl">
<motion.div
@ -477,9 +479,9 @@ export default function OwnerProfitsPage() {
className="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4"
>
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">أرباح المالك</h1>
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t('profits.pageTitle')}</h1>
<p className="text-gray-600">
مرحباً {user?.name}
{t('profits.greeting')} {user?.name}
</p>
</div>
<button
@ -487,7 +489,7 @@ export default function OwnerProfitsPage() {
className="px-5 py-2.5 bg-amber-500 text-white rounded-xl font-medium hover:bg-amber-600 transition-colors flex items-center gap-2 shadow-sm"
>
<Download className="w-5 h-5" />
تصدير التقرير
{t('profits.exportReport')}
</button>
</motion.div>
@ -500,18 +502,18 @@ export default function OwnerProfitsPage() {
<table className="min-w-full divide-y divide-gray-200 text-sm">
<thead className="bg-gray-800 text-gray-100">
<tr>
<th className="px-4 py-4 text-center text-xs font-semibold uppercase tracking-wider">العقار</th>
<th className="px-4 py-4 text-center text-xs font-semibold uppercase tracking-wider">رقم الحجز</th>
<th className="px-4 py-4 text-center text-xs font-semibold uppercase tracking-wider">من تاريخ</th>
<th className="px-4 py-4 text-center text-xs font-semibold uppercase tracking-wider">حتى تاريخ</th>
<th className="px-4 py-4 text-center text-xs font-semibold uppercase tracking-wider">العروض المستلم</th>
<th className="px-4 py-4 text-center text-xs font-semibold uppercase tracking-wider">عمولة المنصة</th>
<th className="px-4 py-4 text-center text-xs font-semibold uppercase tracking-wider">{t('profits.colProperty')}</th>
<th className="px-4 py-4 text-center text-xs font-semibold uppercase tracking-wider">{t('profits.colBookingNumber')}</th>
<th className="px-4 py-4 text-center text-xs font-semibold uppercase tracking-wider">{t('profits.colFromDate')}</th>
<th className="px-4 py-4 text-center text-xs font-semibold uppercase tracking-wider">{t('profits.colToDate')}</th>
<th className="px-4 py-4 text-center text-xs font-semibold uppercase tracking-wider">{t('profits.colAmountReceived')}</th>
<th className="px-4 py-4 text-center text-xs font-semibold uppercase tracking-wider">{t('profits.colPlatformCommission')}</th>
<th className="px-4 py-4 text-center text-xs font-semibold uppercase tracking-wider bg-amber-50 text-amber-800">
ربح المنصة <span className="font-normal text-[11px] block">(5% من العربون)</span>
{t('profits.colPlatformProfit')} <span className="font-normal text-[11px] block">{t('profits.colPlatformProfitSub')}</span>
</th>
<th className="px-4 py-4 text-center text-xs font-semibold uppercase tracking-wider">المستحق للمالك</th>
<th className="px-4 py-4 text-center text-xs font-semibold uppercase tracking-wider">تم التحويل للمالك</th>
<th className="px-4 py-4 text-center text-xs font-semibold uppercase tracking-wider">رقم وصل التحويل</th>
<th className="px-4 py-4 text-center text-xs font-semibold uppercase tracking-wider">{t('profits.colOwnerDue')}</th>
<th className="px-4 py-4 text-center text-xs font-semibold uppercase tracking-wider">{t('profits.colTransferred')}</th>
<th className="px-4 py-4 text-center text-xs font-semibold uppercase tracking-wider">{t('profits.colTransferReceipt')}</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-100">
@ -558,7 +560,7 @@ export default function OwnerProfitsPage() {
<tfoot className="bg-gray-100 border-t-2 border-gray-300">
<tr>
<td colSpan="4" className="px-4 py-4 text-right font-bold text-gray-800">
الإجمالي العام
{t('profits.totalGeneral')}
</td>
<td className="px-4 py-4 text-center font-bold font-mono text-gray-800">
{totals.totalAmountReceived}
@ -582,8 +584,8 @@ export default function OwnerProfitsPage() {
</div>
<div className="bg-gray-50 px-6 py-3 text-xs text-gray-500 border-t border-gray-200">
<span className="inline-flex items-center gap-1"></span> ملاحظة:
<strong> ربح المنصة </strong> يُحتسب تلقائياً بنسبة <strong className="text-amber-600">5%</strong> من قيمة «العروض المستلم».
<span className="inline-flex items-center gap-1"></span> {t('profits.note')}
<strong> {t('profits.colPlatformProfit')} </strong>{t('profits.noteMiddle')}<strong className="text-amber-600">5%</strong>{t('profits.noteEnd', { colName: t('profits.colAmountReceived') })}
</div>
</motion.div>
</div>

View File

@ -6,6 +6,7 @@ import { useRouter, useSearchParams } from 'next/navigation';
import Link from 'next/link';
import Image from 'next/image';
import 'leaflet/dist/leaflet.css';
import { useTranslation } from 'react-i18next';
import {
ArrowLeft,
MapPin,
@ -74,6 +75,7 @@ export default function AddPropertyPage() {
const router = useRouter();
const searchParams = useSearchParams();
const purpose = searchParams.get('purpose') || 'rent';
const { t } = useTranslation();
const [step, setStep] = useState(1);
const totalSteps = purpose === 'sale' ? 4 : 4;
@ -171,15 +173,15 @@ export default function AddPropertyPage() {
const fileInputRef = useRef(null);
const propertyTypes = [
{ id: 'apartment', label: 'شقة', icon: Building },
{ id: 'villa', label: 'فيلا', icon: Home },
{ id: 'sweet', label: 'سويت', icon: Sofa },
{ id: 'room', label: 'غرفة ضمن شقة (سكن مشترك)', icon: DoorOpen },
{ id: 'studio', label: 'استوديو', icon: Sofa },
{ id: 'office', label: 'مكتب', icon: Building },
{ id: 'farms', label: 'مزرعة', icon: Trees },
{ id: 'shop', label: 'متجر', icon: Warehouse },
{ id: 'warehouse', label: 'مستودع', icon: Warehouse },
{ id: 'apartment', label: t('buildingType.apartment'), icon: Building },
{ id: 'villa', label: t('buildingType.villa'), icon: Home },
{ id: 'sweet', label: t('buildingType.sweet'), icon: Sofa },
{ id: 'room', label: t('buildingType.room'), icon: DoorOpen },
{ id: 'studio', label: t('buildingType.studio'), icon: Sofa },
{ id: 'office', label: t('buildingType.office'), icon: Building },
{ id: 'farms', label: t('buildingType.farms'), icon: Trees },
{ id: 'shop', label: t('buildingType.shop'), icon: Warehouse },
{ id: 'warehouse', label: t('buildingType.warehouse'), icon: Warehouse },
];
const serviceList = [
@ -199,9 +201,9 @@ export default function AddPropertyPage() {
];
const offerTypes = [
{ id: 'daily', label: 'إيجار يومي', icon: Clock },
{ id: 'monthly', label: 'إيجار شهري', icon: Calendar },
{ id: 'both', label: 'إيجار يومي وشهري', icon: Calendar },
{ id: 'daily', label: t('dailyRent'), icon: Clock },
{ id: 'monthly', label: t('monthlyRent'), icon: Calendar },
{ id: 'both', label: t('addProperty.dailyAndMonthly'), icon: Calendar },
].filter(Boolean);
useEffect(() => {
@ -218,7 +220,7 @@ export default function AddPropertyPage() {
const handleSearch = async () => {
if (!searchQuery) return;
toast.loading('جاري البحث...', { id: 'search' });
toast.loading(t('toast.searching'), { id: 'search' });
try {
const response = await fetch(
@ -245,23 +247,23 @@ export default function AddPropertyPage() {
address: addressData.display_name || result.display_name
});
toast.success('تم العثور على الموقع', { id: 'search' });
toast.success(t('toast.locationFound'), { id: 'search' });
} else {
toast.error('لم يتم العثور على العنوان', { id: 'search' });
toast.error(t('toast.locationNotFound'), { id: 'search' });
}
} catch (error) {
console.error('خطأ في البحث:', error);
toast.error('حدث خطأ في البحث', { id: 'search' });
console.error(t('search.error'), error);
toast.error(t('search.error'), { id: 'search' });
}
};
const handleGeolocation = () => {
if (!navigator.geolocation) {
toast.error('المتصفح لا يدعم تحديد الموقع');
if (!navigator.geolocation) {
toast.error(t('toast.geolocationNotSupported'));
return;
}
toast.loading('جاري تحديد موقعك...', { id: 'geolocation' });
toast.loading(t('toast.geolocationLoading'), { id: 'geolocation' });
navigator.geolocation.getCurrentPosition(
async (position) => {
@ -269,30 +271,30 @@ export default function AddPropertyPage() {
setMapCenter([latitude, longitude]);
setMapZoom(18);
try {
const response = await fetch(
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}&accept-language=ar`
);
const data = await response.json();
setSelectedLocation({
lat: latitude,
lng: longitude,
address: data.display_name || 'موقعك الحالي'
});
toast.success('تم تحديد موقعك', { id: 'geolocation' });
} catch (error) {
setSelectedLocation({
lat: latitude,
lng: longitude,
address: 'موقعك الحالي'
});
toast.success('تم تحديد موقعك', { id: 'geolocation' });
}
try {
const response = await fetch(
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}&accept-language=ar`
);
const data = await response.json();
setSelectedLocation({
lat: latitude,
lng: longitude,
address: data.display_name || t('addProperty.currentLocation')
});
toast.success(t('toast.geolocationSuccess'), { id: 'geolocation' });
} catch (error) {
setSelectedLocation({
lat: latitude,
lng: longitude,
address: t('addProperty.currentLocation')
});
toast.success(t('toast.geolocationSuccess'), { id: 'geolocation' });
}
},
(error) => {
toast.error('فشل في تحديد الموقع', { id: 'geolocation' });
toast.error(t('toast.geolocationFailed'), { id: 'geolocation' });
}
);
};
@ -301,32 +303,32 @@ const handleMapClick = async (coords) => {
try {
const [lat, lng] = coords;
toast.loading('جاري تحديد الموقع...', { id: 'location' });
toast.loading(t('addProperty.locationSelecting'), { id: 'location' });
const response = await fetch(
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&accept-language=ar`
);
const data = await response.json();
setSelectedLocation({
lat: lat,
lng: lng,
address: data.display_name || 'موقع محدد'
});
setMapZoom(18);
toast.success('تم تحديد الموقع بنجاح!', { id: 'location' });
const response = await fetch(
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&accept-language=ar`
);
const data = await response.json();
setSelectedLocation({
lat: lat,
lng: lng,
address: data.display_name || t('addProperty.defaultAddress')
});
setMapZoom(18);
toast.success(t('toast.locationSelectedSuccess'), { id: 'location' });
} catch (error) {
console.error('خطأ في تحديد الموقع:', error);
console.error(t('addProperty.locationError'), error);
const [lat, lng] = coords;
setSelectedLocation({
lat: lat,
lng: lng,
address: 'موقع محدد'
address: t('addProperty.defaultAddress')
});
setMapZoom(18);
toast.success('تم تحديد الموقع', { id: 'location' });
toast.success(t('toast.locationConfirmed'), { id: 'location' });
}
};
const handleMarkerDragEnd = async (lat, lng) => {
@ -338,13 +340,13 @@ const handleMarkerDragEnd = async (lat, lng) => {
setSelectedLocation({
lat,
lng,
address: data.display_name || 'موقع محدد'
address: data.display_name || t('addProperty.defaultAddress')
});
} catch (error) {
setSelectedLocation({
lat,
lng,
address: 'موقع محدد'
address: t('addProperty.defaultAddress')
});
}
};
@ -357,7 +359,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
address: selectedLocation.address
});
toast.success('تم تأكيد الموقع بنجاح');
toast.success(t('toast.locationConfirmed'));
}
};
@ -370,25 +372,25 @@ const handleMarkerDragEnd = async (lat, lng) => {
address: ''
});
setMapZoom(15);
toast.info('تم إلغاء تحديد الموقع');
toast.info(t('toast.clearLocation'));
};
const handleImageUpload = async (files) => {
const newImages = Array.from(files);
if (formData.images.length + newImages.length > 5) {
toast.error('يمكنك رفع 5 صور كحد أقصى');
toast.error(t('toast.maxImages'));
return;
}
for (const file of newImages) {
if (!file.type.startsWith('image/')) {
toast.error('الرجاء اختيار صور صالحة فقط');
toast.error(t('toast.imageRequired'));
continue;
}
if (file.size > 5 * 1024 * 1024) {
toast.error('حجم الصورة يجب أن يكون أقل من 5 ميجابايت');
toast.error(t('toast.imageSize'));
continue;
}
@ -410,7 +412,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
setUploadedImagePaths(prev => [...prev, path]);
} catch (err) {
console.error('[AddProperty] Image upload failed:', err);
toast.error('فشل رفع الصورة: ' + file.name);
toast.error(`${t('toast.imageUploadFailed')}: ${file.name}`);
}
}
};
@ -519,48 +521,48 @@ const handleMarkerDragEnd = async (lat, lng) => {
switch(step) {
case 1:
if (!formData.propertyType) {
newErrors.propertyType = 'نوع العقار مطلوب';
newErrors.propertyType = t('addProperty.propertyTypeRequired');
}
break;
case 2:
if (!formData.bedrooms) {
newErrors.bedrooms = 'عدد الغرف مطلوب';
newErrors.bedrooms = t('addProperty.bedroomsRequired');
}
if (!formData.bathrooms) {
newErrors.bathrooms = 'عدد الحمامات مطلوب';
newErrors.bathrooms = t('addProperty.bathroomsRequired');
}
if (!formData.livingRooms) {
newErrors.livingRooms = 'عدد الصالونات مطلوب';
newErrors.livingRooms = t('addProperty.livingRoomsRequired');
}
break;
case 3:
if (purpose === 'sale') {
if (!formData.salePrice) newErrors.salePrice = 'سعر البيع مطلوب';
if (!formData.salePrice) newErrors.salePrice = t('addProperty.salePriceRequired');
} else {
if (!formData.offerType) {
newErrors.offerType = 'الرجاء اختيار نوع العرض';
newErrors.offerType = t('addProperty.offerTypeRequired');
} else if (formData.offerType === 'daily' && !formData.dailyPrice) {
newErrors.dailyPrice = 'السعر اليومي مطلوب';
newErrors.dailyPrice = t('addProperty.dailyPriceRequired');
} else if (formData.offerType === 'monthly' && !formData.monthlyPrice) {
newErrors.monthlyPrice = 'السعر الشهري مطلوب';
newErrors.monthlyPrice = t('addProperty.monthlyPriceRequired');
} else if (formData.offerType === 'both') {
if (!formData.dailyPrice) newErrors.dailyPrice = 'السعر اليومي مطلوب';
if (!formData.monthlyPrice) newErrors.monthlyPrice = 'السعر الشهري مطلوب';
if (!formData.dailyPrice) newErrors.dailyPrice = t('addProperty.dailyPriceRequired');
if (!formData.monthlyPrice) newErrors.monthlyPrice = t('addProperty.monthlyPriceRequired');
}
}
break;
case 4:
if (!formData.lat || !formData.lng) {
newErrors.location = 'الرجاء تحديد موقع العقار على الخريطة';
newErrors.location = t('addProperty.locationRequired');
}
if (formData.images.length < 2) {
newErrors.images = 'يجب رفع صورتين على الأقل';
newErrors.images = t('addProperty.minImagesRequired');
}
if (formData.images.length > 5) {
newErrors.images = 'يمكن رفع 5 صور كحد أقصى';
newErrors.images = t('addProperty.maxImagesError');
}
break;
}
@ -670,7 +672,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
currencyId: selectedCurrencyId,
};
const res = await addSaleProperty(payload);
toast.success('تم إضافة عقار للبيع بنجاح!');
toast.success(t('toast.salePropertySuccess'));
} else {
const rentTypeMap = { daily: RentType.DAILY, monthly: RentType.MONTHLY, both: RentType.MONTHLY };
const payload = {
@ -685,14 +687,14 @@ const handleMarkerDragEnd = async (lat, lng) => {
allowedPaymentPeriod: formData.allowedPaymentPeriod || '',
};
const res = await addRentProperty(payload);
toast.success('تم إضافة عقار للإيجار بنجاح!');
toast.success(t('toast.rentPropertySuccess'));
}
setTimeout(() => {
router.push('/owner/properties');
}, 1500);
} catch (err) {
console.error('[AddProperty] API error:', err);
toast.error(err.message || 'فشل في إضافة العقار');
toast.error(err.message || t('toast.propertyAddFailed'));
} finally {
setIsLoading(false);
}
@ -713,15 +715,15 @@ const handleMarkerDragEnd = async (lat, lng) => {
<div className="container mx-auto px-4 max-w-4xl">
<div className="mb-8">
<div className="flex items-center justify-between mb-4">
<Link
href="/owner/properties"
className="flex items-center gap-2 text-gray-600 hover:text-amber-600 transition-colors group"
>
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
<span>العودة للعقارات</span>
</Link>
<Link
href="/owner/properties"
className="flex items-center gap-2 text-gray-600 hover:text-amber-600 transition-colors group"
>
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
<span>{t('addProperty.backToProperties')}</span>
</Link>
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-amber-600">خطوة {step} من {totalSteps}</span>
<span className="text-sm font-medium text-amber-600">{t('addProperty.stepCounter', { step, total: totalSteps })}</span>
</div>
</div>
@ -738,10 +740,10 @@ const handleMarkerDragEnd = async (lat, lng) => {
</div>
<div className="flex justify-between mt-2 text-xs text-gray-500">
<span>معلومات العقار</span>
<span>التفاصيل والخدمات</span>
<span>{purpose === 'sale' ? 'سعر البيع' : 'السعر'}</span>
<span>الموقع والصور</span>
<span>{t('addProperty.stepInfo')}</span>
<span>{t('addProperty.stepDetails')}</span>
<span>{purpose === 'sale' ? t('addProperty.stepSalePrice') : t('addProperty.stepRentPrice')}</span>
<span>{t('addProperty.stepLocation')}</span>
</div>
</div>
@ -759,14 +761,14 @@ const handleMarkerDragEnd = async (lat, lng) => {
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
<Home className="w-10 h-10 text-amber-600" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">معلومات العقار</h2>
<p className="text-gray-600">اختر نوع العقار والحالة</p>
<h2 className="text-2xl font-bold text-gray-900 mb-2">{t('addProperty.stepInfo')}</h2>
<p className="text-gray-600">{t('addProperty.propertyInfoSubtitle')}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
نوع العقار <span className="text-red-500">*</span>
</label>
<label className="block text-sm font-medium text-gray-700 mb-3">
{t('addProperty.propertyTypeLabel')} <span className="text-red-500">*</span>
</label>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{propertyTypes.map((type) => {
const Icon = type.icon;
@ -794,7 +796,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
حالة العقار
{t('addProperty.furnishedStatus')}
</label>
<div className="flex gap-4">
<label className="flex items-center gap-2 p-3 border rounded-xl cursor-pointer hover:bg-gray-50 flex-1">
@ -805,7 +807,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
onChange={() => setFormData({...formData, furnished: true})}
className="w-4 h-4 text-amber-500"
/>
<span className="text-gray-700">مفروش</span>
<span className="text-gray-700">{t('addProperty.furnished')}</span>
</label>
<label className="flex items-center gap-2 p-3 border rounded-xl cursor-pointer hover:bg-gray-50 flex-1">
<input
@ -815,21 +817,21 @@ const handleMarkerDragEnd = async (lat, lng) => {
onChange={() => setFormData({...formData, furnished: false})}
className="w-4 h-4 text-amber-500"
/>
<span className="text-gray-700">غير مفروش</span>
<span className="text-gray-700">{t('addProperty.unfurnished')}</span>
</label>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
وصف إضافي (اختياري)
{t('addProperty.additionalDescription')}
</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({...formData, description: e.target.value})}
rows="4"
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
placeholder="أضف وصفاً إضافياً للعقار..."
placeholder={t('addProperty.descriptionPlaceholder')}
/>
</div>
</motion.div>
@ -841,14 +843,14 @@ const handleMarkerDragEnd = async (lat, lng) => {
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
<Layers className="w-10 h-10 text-amber-600" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">تفاصيل العقار</h2>
<p className="text-gray-600">أدخل التفاصيل والخدمات المتاحة</p>
<h2 className="text-2xl font-bold text-gray-900 mb-2">{t('addProperty.propertyDetailsTitle')}</h2>
<p className="text-gray-600">{t('addProperty.propertyDetailsSubtitle')}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
المساحة (م²)
{t('addProperty.space')}
</label>
<div className="relative">
<Square className="absolute right-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
@ -858,14 +860,14 @@ const handleMarkerDragEnd = async (lat, lng) => {
value={formData.space}
onChange={(e) => setFormData({...formData, space: e.target.value})}
className="w-full pr-10 pl-3 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
placeholder="مثال: 120"
placeholder={t('addProperty.spacePlaceholder')}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
عدد الغرف <span className="text-red-500">*</span>
{t('addProperty.bedrooms')} <span className="text-red-500">*</span>
</label>
<div className="flex items-center gap-2">
<button
@ -893,7 +895,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
عدد الحمامات <span className="text-red-500">*</span>
{t('addProperty.bathrooms')} <span className="text-red-500">*</span>
</label>
<div className="flex items-center gap-2">
<button
@ -921,7 +923,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
عدد الصالونات <span className="text-red-500">*</span>
{t('addProperty.livingRooms')} <span className="text-red-500">*</span>
</label>
<div className="flex items-center gap-2">
<button
@ -950,41 +952,41 @@ const handleMarkerDragEnd = async (lat, lng) => {
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">رقم الطابق</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('addProperty.floorNumber')}</label>
<input
type="number"
value={formData.floorNumber}
onChange={(e) => setFormData({...formData, floorNumber: e.target.value})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
placeholder="مثال: 3"
placeholder={t('addProperty.floorPlaceholder')}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">عدد الصالونات</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('addProperty.salons')}</label>
<input
type="number"
min="0"
value={formData.salons}
onChange={(e) => setFormData({...formData, salons: e.target.value})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
placeholder="مثال: 1"
placeholder={t('addProperty.salonsPlaceholder')}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">عدد الشرفات</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('addProperty.balconies')}</label>
<input
type="number"
min="0"
value={formData.balconies}
onChange={(e) => setFormData({...formData, balconies: e.target.value})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
placeholder="مثال: 1"
placeholder={t('addProperty.balconiesPlaceholder')}
/>
</div>
</div>
<div>
<h3 className="text-lg font-bold text-gray-900 mb-4">الخدمات المتوفرة <span className="text-red-500">*</span></h3>
<h3 className="text-lg font-bold text-gray-900 mb-4">{t('addProperty.servicesLabel')} <span className="text-red-500">*</span></h3>
<div className="space-y-3">
{serviceList.map((service) => {
const Icon = service.icon;
@ -1010,7 +1012,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
value={formData.serviceDetails[service.id] || ''}
onChange={(e) => updateServiceDetail(service.id, e.target.value)}
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-amber-500"
placeholder="تفاصيل الخدمة (مثال: في جميع الغرف)"
placeholder={t('addProperty.serviceDetailPlaceholder')}
/>
</div>
)}
@ -1023,11 +1025,11 @@ const handleMarkerDragEnd = async (lat, lng) => {
<div className="bg-white rounded-xl p-6 border border-gray-200 mt-6">
<h3 className="text-lg font-bold text-gray-800 mb-4 flex items-center gap-2">
<MapPin className="w-5 h-5 text-amber-600" />
القرب من الخدمات
{t('addProperty.nearbyServicesTitle')}
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">المسافة إلى أقرب مدرسة (كم)</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('addProperty.nearbySchoolLabel')}</label>
<input
type="number"
min="0"
@ -1035,11 +1037,11 @@ const handleMarkerDragEnd = async (lat, lng) => {
value={formData.nearbySchool}
onChange={(e) => setFormData({...formData, nearbySchool: e.target.value})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
placeholder="مثال: 0.5"
placeholder={t('addProperty.nearbyPlaceholder')}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">المسافة إلى أقرب مستشفى (كم)</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('addProperty.nearbyHospitalLabel')}</label>
<input
type="number"
min="0"
@ -1047,11 +1049,11 @@ const handleMarkerDragEnd = async (lat, lng) => {
value={formData.nearbyHospital}
onChange={(e) => setFormData({...formData, nearbyHospital: e.target.value})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
placeholder="مثال: 1.0"
placeholder={t('addProperty.nearbyPlaceholder')}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">المسافة إلى أقرب مطعم (كم)</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('addProperty.nearbyRestaurantLabel')}</label>
<input
type="number"
min="0"
@ -1059,11 +1061,11 @@ const handleMarkerDragEnd = async (lat, lng) => {
value={formData.nearbyRestaurant}
onChange={(e) => setFormData({...formData, nearbyRestaurant: e.target.value})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
placeholder="مثال: 0.3"
placeholder={t('addProperty.nearbyPlaceholder')}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">المسافة إلى أقرب جامعة (كم)</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('addProperty.nearbyUniversityLabel')}</label>
<input
type="number"
min="0"
@ -1071,11 +1073,11 @@ const handleMarkerDragEnd = async (lat, lng) => {
value={formData.nearbyUniversity}
onChange={(e) => setFormData({...formData, nearbyUniversity: e.target.value})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
placeholder="مثال: 2.0"
placeholder={t('addProperty.nearbyPlaceholder')}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">المسافة إلى أقرب حديقة (كم)</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('addProperty.nearbyParkLabel')}</label>
<input
type="number"
min="0"
@ -1083,11 +1085,11 @@ const handleMarkerDragEnd = async (lat, lng) => {
value={formData.nearbyPark}
onChange={(e) => setFormData({...formData, nearbyPark: e.target.value})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
placeholder="مثال: 0.5"
placeholder={t('addProperty.nearbyPlaceholder')}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">المسافة إلى أقرب مول (كم)</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('addProperty.nearbyMallLabel')}</label>
<input
type="number"
min="0"
@ -1095,7 +1097,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
value={formData.nearbyMall}
onChange={(e) => setFormData({...formData, nearbyMall: e.target.value})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
placeholder="مثال: 1.5"
placeholder={t('addProperty.nearbyPlaceholder')}
/>
</div>
</div>
@ -1103,7 +1105,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
{purpose === 'rent' && (
<div>
<h3 className="text-lg font-bold text-gray-900 mb-4">شروط استخدام العقار</h3>
<h3 className="text-lg font-bold text-gray-900 mb-4">{t('addProperty.termsTitle')}</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
{termsList.map((term) => {
const Icon = term.icon;
@ -1137,14 +1139,14 @@ const handleMarkerDragEnd = async (lat, lng) => {
{/* Custom Terms */}
<div className="mt-4 p-4 border border-dashed border-gray-300 rounded-xl">
<p className="text-sm font-medium text-gray-700 mb-2">إضافة شرط مخصص</p>
<p className="text-sm font-medium text-gray-700 mb-2">{t('addProperty.customTermsTitle')}</p>
<div className="flex gap-2">
<input
type="text"
value={customTermInput}
onChange={(e) => setCustomTermInput(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addCustomTerm(); } }}
placeholder="اكتب شرطاً مخصصاً..."
placeholder={t('addProperty.customTermPlaceholder')}
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none"
/>
<button
@ -1153,7 +1155,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
disabled={!customTermInput.trim()}
className="px-4 py-2 bg-amber-500 text-white rounded-lg text-sm font-medium hover:bg-amber-600 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors"
>
إضافة
{t('addProperty.add')}
</button>
</div>
{customTerms.length > 0 && (
@ -1187,13 +1189,13 @@ const handleMarkerDragEnd = async (lat, lng) => {
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
<DollarSign className="w-10 h-10 text-amber-600" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">سعر البيع</h2>
<p className="text-gray-600">حدد سعر البيع والعملة</p>
<h2 className="text-2xl font-bold text-gray-900 mb-2">{t('addProperty.salePriceTitle')}</h2>
<p className="text-gray-600">{t('addProperty.salePriceSubtitle')}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
سعر البيع (ل.س) <span className="text-red-500">*</span>
{t('addProperty.salePriceLabel')} <span className="text-red-500">*</span>
</label>
<div className="relative">
<span className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400 font-bold text-sm flex items-center justify-center">{currencySymbol}</span>
@ -1211,7 +1213,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 ${
errors.salePrice ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="مثال: 50000000"
placeholder={t('addProperty.salePricePlaceholder')}
/>
</div>
{errors.salePrice && <p className="text-red-500 text-sm mt-1">{errors.salePrice}</p>}
@ -1219,16 +1221,16 @@ const handleMarkerDragEnd = async (lat, lng) => {
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
العملة <span className="text-red-500">*</span>
{t('addProperty.currencyLabel')} <span className="text-red-500">*</span>
</label>
<select
value={selectedCurrencyId}
onChange={(e) => setSelectedCurrencyId(parseInt(e.target.value))}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
>
<option value={Currency.USD}>دولار امريكي $</option>
<option value={Currency.SYP}>عملة سورية SP</option>
</select>
<select
value={selectedCurrencyId}
onChange={(e) => setSelectedCurrencyId(parseInt(e.target.value))}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
>
<option value={Currency.USD}>{t('addProperty.usdOption')}</option>
<option value={Currency.SYP}>{t('addProperty.sypOption')}</option>
</select>
</div>
</motion.div>
)}
@ -1239,13 +1241,13 @@ const handleMarkerDragEnd = async (lat, lng) => {
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
<DollarSign className="w-10 h-10 text-amber-600" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">نوع العرض والسعر</h2>
<p className="text-gray-600">اختر نوع العرض وحدد السعر المناسب</p>
<h2 className="text-2xl font-bold text-gray-900 mb-2">{t('addProperty.offerAndPriceTitle')}</h2>
<p className="text-gray-600">{t('addProperty.offerAndPriceSubtitle')}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
نوع العرض <span className="text-red-500">*</span>
{t('addProperty.offerTypeLabel')} <span className="text-red-500">*</span>
</label>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{offerTypes.map((type) => {
@ -1272,15 +1274,15 @@ const handleMarkerDragEnd = async (lat, lng) => {
{/* Currency dropdown */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
العملة <span className="text-red-500">*</span>
{t('addProperty.currencyLabel')} <span className="text-red-500">*</span>
</label>
<select
value={selectedCurrencyId}
onChange={(e) => setSelectedCurrencyId(parseInt(e.target.value))}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
>
<option value={Currency.USD}>دولار امريكي $</option>
<option value={Currency.SYP}>عملة سورية SP</option>
<option value={Currency.USD}>{t('addProperty.usdOption')}</option>
<option value={Currency.SYP}>{t('addProperty.sypOption')}</option>
</select>
</div>
@ -1292,7 +1294,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
{!formData.offerType && (
<div className="border-2 border-dashed border-gray-200 rounded-2xl p-8 text-center">
<DollarSign className="w-8 h-8 text-gray-300 mx-auto mb-3" />
<p className="text-gray-400 font-medium">اختر نوع العرض اولا</p>
<p className="text-gray-400 font-medium">{t('addProperty.selectOfferTypeFirst')}</p>
</div>
)}
@ -1307,7 +1309,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
سعر الايجار اليومي ({currencySymbol}) <span className="text-red-500">*</span>
{t('addProperty.dailyPriceLabel')} ({currencySymbol}) <span className="text-red-500">*</span>
</label>
<div className="relative">
<span className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400 font-bold text-sm flex items-center justify-center">{currencySymbol}</span>
@ -1325,7 +1327,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 ${
errors.dailyPrice ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="مثال: 50000"
placeholder={t('addProperty.dailyPricePlaceholder')}
/>
</div>
{errors.dailyPrice && (
@ -1334,7 +1336,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
مبلغ التأمين ({currencySymbol})
{t('addProperty.depositLabel')} ({currencySymbol})
</label>
<div className="relative">
<span className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400 font-bold text-sm flex items-center justify-center">{currencySymbol}</span>
@ -1350,7 +1352,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
}}
onKeyDown={(e) => { if (['-', 'e', 'E', '+'].includes(e.key)) e.preventDefault() }}
className="w-full pr-12 pl-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
placeholder="مثال: 500000"
placeholder={t('addProperty.depositPlaceholder')}
/>
</div>
</div>
@ -1367,7 +1369,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
سعر الايجار الشهري ({currencySymbol}) <span className="text-red-500">*</span>
{t('addProperty.monthlyPriceLabel')} ({currencySymbol}) <span className="text-red-500">*</span>
</label>
<div className="relative">
<span className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400 font-bold text-sm flex items-center justify-center">{currencySymbol}</span>
@ -1385,7 +1387,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 ${
errors.monthlyPrice ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="مثال: 1000000"
placeholder={t('addProperty.monthlyPricePlaceholder')}
/>
</div>
{errors.monthlyPrice && (
@ -1394,7 +1396,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
مبلغ التأمين ({currencySymbol})
{t('addProperty.depositLabel')} ({currencySymbol})
</label>
<div className="relative">
<span className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400 font-bold text-sm flex items-center justify-center">{currencySymbol}</span>
@ -1410,7 +1412,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
}}
onKeyDown={(e) => { if (['-', 'e', 'E', '+'].includes(e.key)) e.preventDefault() }}
className="w-full pr-12 pl-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
placeholder="مثال: 500000"
placeholder={t('addProperty.depositPlaceholder')}
/>
</div>
</div>
@ -1427,7 +1429,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
سعر الايجار اليومي ({currencySymbol}) <span className="text-red-500">*</span>
{t('addProperty.dailyPriceLabel')} ({currencySymbol}) <span className="text-red-500">*</span>
</label>
<div className="relative">
<span className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400 font-bold text-sm flex items-center justify-center">{currencySymbol}</span>
@ -1445,7 +1447,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 ${
errors.dailyPrice ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="مثال: 50000"
placeholder={t('addProperty.dailyPricePlaceholder')}
/>
</div>
{errors.dailyPrice && (
@ -1454,7 +1456,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
سعر الايجار الشهري ({currencySymbol}) <span className="text-red-500">*</span>
{t('addProperty.monthlyPriceLabel')} ({currencySymbol}) <span className="text-red-500">*</span>
</label>
<div className="relative">
<span className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400 font-bold text-sm flex items-center justify-center">{currencySymbol}</span>
@ -1472,7 +1474,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 ${
errors.monthlyPrice ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="مثال: 1000000"
placeholder={t('addProperty.monthlyPricePlaceholder')}
/>
</div>
{errors.monthlyPrice && (
@ -1481,7 +1483,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
مبلغ التأمين ({currencySymbol})
{t('addProperty.depositLabel')} ({currencySymbol})
</label>
<div className="relative">
<span className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400 font-bold text-sm flex items-center justify-center">{currencySymbol}</span>
@ -1497,7 +1499,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
}}
onKeyDown={(e) => { if (['-', 'e', 'E', '+'].includes(e.key)) e.preventDefault() }}
className="w-full pr-12 pl-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
placeholder="مثال: 500000"
placeholder={t('addProperty.depositPlaceholder')}
/>
</div>
</div>
@ -1513,12 +1515,12 @@ const handleMarkerDragEnd = async (lat, lng) => {
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
<MapPin className="w-10 h-10 text-amber-600" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">الموقع والصور</h2>
<p className="text-gray-600">حدد موقع العقار وأضف الصور</p>
<h2 className="text-2xl font-bold text-gray-900 mb-2">{t('addProperty.locationAndImagesTitle')}</h2>
<p className="text-gray-600">{t('addProperty.locationAndImagesSubtitle')}</p>
</div>
<div>
<h3 className="text-lg font-bold text-gray-900 mb-4">حدد موقع العقار على الخريطة</h3>
<h3 className="text-lg font-bold text-gray-900 mb-4">{t('addProperty.selectLocationTitle')}</h3>
<div className="flex gap-2 mb-4">
<div className="flex-1 relative">
@ -1527,7 +1529,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
placeholder="ابحث عن عنوان..."
placeholder={t('addProperty.searchAddress')}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 pr-12"
/>
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
@ -1536,16 +1538,16 @@ const handleMarkerDragEnd = async (lat, lng) => {
onClick={handleSearch}
className="px-6 py-3 bg-amber-500 text-white rounded-xl hover:bg-amber-600 transition-colors"
>
بحث
{t('addProperty.searchButton')}
</button>
</div>
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4 mb-4 text-sm text-amber-800 leading-relaxed">
<Search className="w-4 h-4 inline ml-1" />
ابحث أولاً ثم اضغط على النتيجة لتحريك الخريطة و تثبيت العلامة .
{t('addProperty.searchHint')}
<br />
<MapPin className="w-4 h-4 inline ml-1" />
اضغط على الخريطة لتحديد موقع العقار
{t('addProperty.clickMapHint')}
</div>
<div className="relative w-full h-96 rounded-xl overflow-hidden border-2 border-gray-200 mb-4">
@ -1564,7 +1566,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
className="w-full mb-4 bg-green-500 text-white py-3 rounded-xl font-medium hover:bg-green-600 transition-colors flex items-center justify-center gap-2"
>
<CheckCircle className="w-5 h-5" />
تأكيد هذا الموقع
{t('addProperty.confirmLocation')}
</button>
)}
@ -1572,7 +1574,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
<div className="bg-green-50 border border-green-200 rounded-xl p-4 mb-4">
<div className="flex items-center gap-2">
<CheckCircle className="w-5 h-5 text-green-600" />
<span className="text-green-800 font-medium">تم تأكيد الموقع:</span>
<span className="text-green-800 font-medium">{t('addProperty.locationConfirmed')}:</span>
</div>
<p className="text-green-700 text-sm mt-2 line-clamp-2">{formData.address}</p>
</div>
@ -1584,7 +1586,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
</div>
<div>
<h3 className="text-lg font-bold text-gray-900 mb-4">صور العقار</h3>
<h3 className="text-lg font-bold text-gray-900 mb-4">{t('addProperty.imagesTitle')}</h3>
<div
onClick={() => fileInputRef.current?.click()}
@ -1602,16 +1604,16 @@ const handleMarkerDragEnd = async (lat, lng) => {
/>
<Upload className="w-12 h-12 text-gray-400 mx-auto mb-3" />
<p className="text-gray-600 font-medium">اضغط لرفع الصور</p>
<p className="text-gray-600 font-medium">{t('addProperty.uploadImages')}</p>
<p className="text-xs text-gray-500 mt-2">
JPEG, PNG, JPG حتى 5MB 800x600 بكسل 2-5 صور
{t('addProperty.imageFormatHint')}
</p>
</div>
<p className="text-sm text-gray-500 mt-2 mb-4 text-center">
يرجى رفع عدة صور واضحة
{t('addProperty.uploadMultipleHintLine1')}
<br />
(اضغط على الرفع أكثر من مرة)
{t('addProperty.uploadMultipleHintLine2')}
</p>
{errors.images && (
@ -1649,8 +1651,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
<div className="border-t border-gray-200 pt-6 mt-8">
<div className="bg-gray-50 rounded-2xl p-5 border border-gray-200">
<p className="text-sm text-gray-700 leading-relaxed mb-4">
أنا صاحب العقار أقر أنني مالك العقار أو مخول قانونياً بعرضه للإيجار أو البيع،
وأن جميع المعلومات المدخلة صحيحة، وأتحمل كامل المسؤولية عن أي معلومات غير صحيحة.
{t('addProperty.ownerAgreement')}
</p>
<label className="flex items-start gap-3 cursor-pointer">
<input
@ -1660,9 +1661,9 @@ const handleMarkerDragEnd = async (lat, lng) => {
className="w-5 h-5 mt-0.5 text-amber-500 rounded shrink-0"
/>
<span className="text-sm text-gray-600">
أوافق على{' '}
{t('addProperty.agreeToText')}{' '}
<Link href="/terms" target="_blank" className="text-amber-600 underline hover:text-amber-700">
شروط الاستخدام
{t('addProperty.termsOfUseText')}
</Link>
</span>
</label>
@ -1680,7 +1681,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
className="flex-1 py-3 px-4 bg-gray-100 text-gray-700 rounded-xl font-medium hover:bg-gray-200 transition-colors flex items-center justify-center gap-2"
>
<ChevronRight className="w-5 h-5" />
السابق
{t('addProperty.previous')}
</button>
)}
@ -1691,7 +1692,7 @@ const handleMarkerDragEnd = async (lat, lng) => {
step === 1 ? 'w-full' : ''
}`}
>
التالي
{t('addProperty.next')}
<ChevronLeft className="w-5 h-5" />
</button>
) : (
@ -1703,12 +1704,12 @@ const handleMarkerDragEnd = async (lat, lng) => {
{isLoading ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
جاري الحفظ...
{t('addProperty.saving')}
</>
) : (
<>
<Save className="w-5 h-5" />
حفظ العقار
{t('addProperty.saveProperty')}
</>
)}
</button>

File diff suppressed because it is too large Load Diff

View File

@ -643,12 +643,12 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { motion } from 'framer-motion';
import { useRouter } from 'next/navigation';
import { useTranslation } from 'react-i18next';
import {
Calendar,
Clock,
@ -660,8 +660,6 @@ import {
MapPin,
DollarSign,
Home,
ArrowLeft,
RefreshCw,
Flag,
} from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
@ -680,34 +678,37 @@ const STATUS_MAP = [
'cancelled',
];
const STATUS_I18N_KEYS = {
pending: 'bookingStatus.pending',
ownerConfirmed: 'bookingStatus.ownerConfirmed',
depositPaid: 'bookingStatus.depositPaid',
depositConfirmed: 'bookingStatus.depositConfirmed',
completed: 'bookingStatus.completed',
cancelled: 'bookingStatus.cancelled',
};
const STATUS_UI = {
pending: {
label: 'قيد الانتظار',
color: 'bg-yellow-100 text-yellow-800',
icon: Clock,
},
ownerConfirmed: {
label: 'مؤكد',
color: 'bg-green-100 text-green-800',
icon: CheckCircle,
},
depositPaid: {
label: 'تم دفع السلفة',
color: 'bg-indigo-100 text-indigo-800',
icon: DollarSign,
},
depositConfirmed: {
label: 'مؤكد نهائياً',
color: 'bg-green-100 text-green-800',
icon: CheckCircle,
},
completed: {
label: 'منتهي',
color: 'bg-blue-100 text-blue-800',
icon: CheckCircle,
},
cancelled: {
label: 'ملغي',
color: 'bg-gray-100 text-gray-800',
icon: XCircle,
},
@ -715,7 +716,7 @@ const STATUS_UI = {
const getStatusKey = (code) => STATUS_MAP[Number(code)] || 'pending';
const sLabel = (code) => STATUS_UI[getStatusKey(code)]?.label ?? String(code);
const sLabel = (code, t) => t(STATUS_I18N_KEYS[getStatusKey(code)]) || String(code);
const sColor = (code) =>
STATUS_UI[getStatusKey(code)]?.color ?? 'bg-gray-100 text-gray-700';
@ -723,6 +724,7 @@ const sColor = (code) =>
const sIcon = (code) => STATUS_UI[getStatusKey(code)]?.icon ?? Clock;
function StatusBadge({ code }) {
const { t } = useTranslation();
const Icon = sIcon(code);
return (
@ -731,7 +733,7 @@ function StatusBadge({ code }) {
code
)}`}
>
<Icon className="w-3 h-3" /> {sLabel(code)}
<Icon className="w-3 h-3" /> {sLabel(code, t)}
</span>
);
}
@ -839,10 +841,10 @@ async function reportReservation(reservationId, message) {
const rid = Number(reservationId);
if (!Number.isInteger(rid)) {
throw new Error('رقم الحجز غير صالح');
throw new Error(t('invalidReservationId'));
}
if (!Number.isInteger(reporter)) {
throw new Error('تعذر تحديد المستخدم الحالي');
throw new Error(t('couldNotIdentifyUser'));
}
const token = getAuthToken();
@ -860,7 +862,7 @@ async function reportReservation(reservationId, message) {
});
if (!res.ok) {
let errorMessage = 'فشل إرسال البلاغ';
let errorMessage = t('reportFailed');
try {
const data = await res.json();
errorMessage = data?.message || data?.title || errorMessage;
@ -889,6 +891,7 @@ function OwnerCard({
actionLoadingId,
reportingId,
}) {
const { t } = useTranslation();
const p = r._prop;
const imgs = pImgs(p);
const img = imgs.length > 0 ? buildImageUrl(imgs[0]) : null;
@ -926,21 +929,21 @@ function OwnerCard({
<div className="text-lg font-bold text-amber-600">
{r.totalPrice?.toLocaleString() ?? '—'}
</div>
<div className="text-xs text-gray-500">السعر الإجمالي</div>
<div className="text-xs text-gray-500">{t('totalPrice')}</div>
</div>
</div>
{(pBeds(p) || pBaths(p)) && (
<div className="flex gap-3 mb-3 text-sm text-gray-600">
{pBeds(p) > 0 && <span>{pBeds(p)} غرف</span>}
{pBaths(p) > 0 && <span>{pBaths(p)} حمامات</span>}
{pBeds(p) > 0 && <span>{pBeds(p)} {t('reservationsBedrooms')}</span>}
{pBaths(p) > 0 && <span>{pBaths(p)} {t('reservationsBathrooms')}</span>}
</div>
)}
<div className="grid grid-cols-2 gap-3 mb-4 text-center">
<div className="bg-gray-50 p-2 rounded-lg">
<Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1" />
<div className="text-xs text-gray-500">من</div>
<div className="text-xs text-gray-500">{t('from')}</div>
<div className="text-sm font-medium">
{r.startDate
? new Date(r.startDate).toLocaleDateString('ar')
@ -950,7 +953,7 @@ function OwnerCard({
<div className="bg-gray-50 p-2 rounded-lg">
<Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1" />
<div className="text-xs text-gray-500">إلى</div>
<div className="text-xs text-gray-500">{t('to')}</div>
<div className="text-sm font-medium">
{r.endDate ? new Date(r.endDate).toLocaleDateString('ar') : '—'}
</div>
@ -968,7 +971,7 @@ function OwnerCard({
className="flex-1 bg-gray-100 text-gray-700 py-2 rounded-xl text-sm font-medium hover:bg-gray-200 transition-colors flex items-center justify-center gap-2"
>
<Eye className="w-4 h-4" />
التفاصيل
{t('details')}
</button>
{isPending && (
@ -984,7 +987,7 @@ function OwnerCard({
) : (
<CheckCircle className="w-4 h-4" />
)}
قبول
{t('accept')}
</button>
<button
@ -998,7 +1001,7 @@ function OwnerCard({
) : (
<XCircle className="w-4 h-4" />
)}
رفض
{t('reject')}
</button>
</>
)}
@ -1019,7 +1022,7 @@ function OwnerCard({
) : (
<Flag className="w-4 h-4" />
)}
{isReporting ? 'جاري الإبلاغ...' : 'إبلاغ'}
{isReporting ? t('reporting') : t('report')}
</button>
</div>
</motion.div>
@ -1027,6 +1030,7 @@ function OwnerCard({
}
function DetailsModal({ r, isOpen, onClose, onReport, reportingId }) {
const { t } = useTranslation();
if (!isOpen || !r) return null;
const p = r._prop;
@ -1049,7 +1053,7 @@ function DetailsModal({ r, isOpen, onClose, onReport, reportingId }) {
>
<div className="sticky top-0 bg-gradient-to-r from-amber-500 to-amber-600 p-6 text-white">
<div className="flex justify-between items-center">
<h2 className="text-xl font-bold">طلب حجز #{r.id}</h2>
<h2 className="text-xl font-bold">{t('reservationRequest', { id: r.id })}</h2>
<button
type="button"
@ -1066,11 +1070,11 @@ function DetailsModal({ r, isOpen, onClose, onReport, reportingId }) {
<div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2">
<Home className="w-5 h-5 text-amber-500" />
معلومات العقار
{t('propertyInfo')}
</h3>
<p>
<span className="text-gray-500">العنوان:</span>{' '}
<span className="text-gray-500">{t('address')}</span>{' '}
{pAddr(p) || '—'}
</p>
@ -1078,13 +1082,13 @@ function DetailsModal({ r, isOpen, onClose, onReport, reportingId }) {
<div className="flex gap-3 mt-2">
{pBeds(p) > 0 && (
<span className="text-sm bg-white px-2 py-1 rounded-lg">
{pBeds(p)} غرف
{pBeds(p)} {t('reservationsBedrooms')}
</span>
)}
{pBaths(p) > 0 && (
<span className="text-sm bg-white px-2 py-1 rounded-lg">
{pBaths(p)} حمامات
{pBaths(p)} {t('reservationsBathrooms')}
</span>
)}
</div>
@ -1095,12 +1099,12 @@ function DetailsModal({ r, isOpen, onClose, onReport, reportingId }) {
<div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2">
<Calendar className="w-5 h-5 text-amber-500" />
تفاصيل الحجز
{t('reservationDetails')}
</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-gray-500">تاريخ البداية</p>
<p className="text-gray-500">{t('startDate')}</p>
<p className="font-medium">
{r.startDate
? new Date(r.startDate).toLocaleDateString('ar')
@ -1109,19 +1113,19 @@ function DetailsModal({ r, isOpen, onClose, onReport, reportingId }) {
</div>
<div>
<p className="text-gray-500">تاريخ النهاية</p>
<p className="text-gray-500">{t('endDate')}</p>
<p className="font-medium">
{r.endDate ? new Date(r.endDate).toLocaleDateString('ar') : '—'}
</p>
</div>
<div>
<p className="text-gray-500">الحالة</p>
<p className="text-gray-500">{t('statusLabel')}</p>
<StatusBadge code={r.status} />
</div>
<div>
<p className="text-gray-500">تاريخ الإنشاء</p>
<p className="text-gray-500">{t('createdAt')}</p>
<p className="font-medium">
{r.createdAt
? new Date(r.createdAt).toLocaleDateString('ar')
@ -1134,11 +1138,11 @@ function DetailsModal({ r, isOpen, onClose, onReport, reportingId }) {
<div className="bg-amber-50 p-4 rounded-xl">
<h3 className="font-bold text-amber-700 mb-3 flex items-center gap-2">
<DollarSign className="w-5 h-5" />
المعلومات المالية
{t('financialInfo')}
</h3>
<div className="flex justify-between font-bold">
<span className="text-gray-900">الإجمالي</span>
<span className="text-gray-900">{t('total')}</span>
<span className="text-amber-600 text-lg">
{r.totalPrice?.toLocaleString() ?? '—'}
</span>
@ -1151,6 +1155,7 @@ function DetailsModal({ r, isOpen, onClose, onReport, reportingId }) {
}
function ReportDialog({ isOpen, reservation, onClose, onSubmit, submitting }) {
const { t } = useTranslation();
const [message, setMessage] = useState('');
useEffect(() => {
@ -1177,8 +1182,8 @@ function ReportDialog({ isOpen, reservation, onClose, onSubmit, submitting }) {
<div className="bg-gradient-to-r from-red-500 to-red-600 p-6 text-white">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold">الإبلاغ عن الحجز</h2>
<p className="text-red-100 text-sm mt-1">رقم الحجز: #{reservation.id}</p>
<h2 className="text-xl font-bold">{t('reportReservation')}</h2>
<p className="text-red-100 text-sm mt-1">{t('reservationId', { id: reservation.id })}</p>
</div>
<button onClick={onClose} className="rounded-full p-1 hover:bg-white/20">
<XCircle className="h-6 w-6" />
@ -1188,13 +1193,13 @@ function ReportDialog({ isOpen, reservation, onClose, onSubmit, submitting }) {
<div className="p-6">
<p className="text-gray-700 mb-4 leading-7">
اخبر فريق الدعم بما حدث التفاصيل الواضحة تساعدنا على مراجعة هذا الحجز بشكل اسرع
{t('reportDescription')}
</p>
<textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="اكتب تفاصيل البلاغ هنا..."
placeholder={t('reportPlaceholder')}
rows={5}
className="w-full resize-none rounded-xl border border-gray-300 p-3 text-sm focus:outline-none focus:ring-2 focus:ring-red-500"
/>
@ -1208,14 +1213,14 @@ function ReportDialog({ isOpen, reservation, onClose, onSubmit, submitting }) {
}`}
>
{submitting ? <Loader2 className="h-4 w-4 animate-spin" /> : <Flag className="h-4 w-4" />}
{submitting ? 'جاري الإرسال...' : 'إرسال البلاغ'}
{submitting ? t('submitting') : t('submitReport')}
</button>
<button
onClick={onClose}
disabled={submitting}
className="rounded-xl bg-gray-200 px-4 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-300 transition-colors disabled:cursor-not-allowed"
>
إلغاء
{t('cancel')}
</button>
</div>
</div>
@ -1225,6 +1230,7 @@ function ReportDialog({ isOpen, reservation, onClose, onSubmit, submitting }) {
}
export default function OwnerReservationRequestsPage() {
const { t, i18n } = useTranslation();
const router = useRouter();
const [reservations, setReservations] = useState([]);
@ -1270,7 +1276,7 @@ export default function OwnerReservationRequestsPage() {
setFiltered(enriched);
} catch (err) {
console.error(err);
toast.error('فشل تحميل طلبات الحجز');
toast.error(t('loadFailedReservations'));
setReservations([]);
setFiltered([]);
} finally {
@ -1321,18 +1327,18 @@ export default function OwnerReservationRequestsPage() {
throw new Error(errorText || `HTTP ${res.status}`);
}
toast.success('تم قبول الحجز بنجاح');
toast.success(t('acceptSuccess'));
await loadReservations();
} catch (err) {
console.error(err);
toast.error('فشل قبول الحجز');
toast.error(t('acceptFailed'));
} finally {
setActionLoadingId(null);
}
};
const handleReject = async (r) => {
if (!confirm('هل أنت متأكد من رفض هذا الحجز؟')) return;
if (!confirm(t('rejectConfirm'))) return;
try {
setActionLoadingId(r.id);
@ -1348,11 +1354,11 @@ export default function OwnerReservationRequestsPage() {
throw new Error(errorText || `HTTP ${res.status}`);
}
toast.success('تم رفض الحجز');
toast.success(t('rejectSuccess'));
await loadReservations();
} catch (err) {
console.error(err);
toast.error('فشل رفض الحجز');
toast.error(t('rejectFailed'));
} finally {
setActionLoadingId(null);
}
@ -1372,11 +1378,11 @@ export default function OwnerReservationRequestsPage() {
setReportingId(reportDialog.reservation.id);
try {
await reportReservation(reportDialog.reservation.id, message.trim() || null);
toast.success('تم إرسال البلاغ بنجاح');
toast.success(t('reportSuccess'));
closeReportDialog();
} catch (err) {
console.error(err);
toast.error(err?.message || 'فشل إرسال البلاغ');
toast.error(err?.message || t('reportFailed'));
} finally {
setReportingId(null);
}
@ -1405,7 +1411,7 @@ export default function OwnerReservationRequestsPage() {
}
return (
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">
<div className="min-h-screen bg-gray-50 py-8" dir={i18n.language === 'ar' ? 'rtl' : 'ltr'}>
<Toaster position="top-center" reverseOrder={false} />
<DetailsModal
@ -1429,30 +1435,11 @@ export default function OwnerReservationRequestsPage() {
animate={{ opacity: 1, y: 0 }}
className="mb-8"
>
<button
type="button"
onClick={() => router.back()}
className="flex items-center gap-2 text-gray-600 hover:text-amber-600 mb-4"
>
<ArrowLeft className="w-5 h-5" />
الرجوع
</button>
<div className="flex items-center justify-between mb-2">
<div>
<h1 className="text-3xl font-bold text-gray-900">
طلبات الحجز
</h1>
<p className="text-gray-600">لديك {reservations.length} طلب</p>
</div>
<button
type="button"
onClick={loadReservations}
className="p-2 bg-white shadow rounded-xl hover:shadow-md transition-all"
>
<RefreshCw className="w-5 h-5 text-gray-600" />
</button>
<div className="mb-4">
<h1 className="text-3xl font-bold text-gray-900">
{t('reservationsTitle')}
</h1>
<p className="text-gray-600">{t('reservationsCount', { count: reservations.length })}</p>
</div>
</motion.div>
@ -1471,7 +1458,7 @@ export default function OwnerReservationRequestsPage() {
>
<div className="text-2xl font-bold text-amber-600">{c}</div>
<div className="text-sm text-gray-600">
{s === 'all' ? 'الكل' : STATUS_UI[s]?.label || s}
{s === 'all' ? t('filterAll') : t(STATUS_I18N_KEYS[s]) || s}
</div>
</motion.div>
))}
@ -1482,7 +1469,7 @@ export default function OwnerReservationRequestsPage() {
<input
type="text"
placeholder="ابحث بعنوان العقار أو رقم الحجز..."
placeholder={t('searchPlaceholderReservations')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
@ -1493,10 +1480,10 @@ export default function OwnerReservationRequestsPage() {
<div className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
<Calendar className="w-12 h-12 text-amber-600 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-900 mb-2">
لا توجد طلبات
{t('noReservations')}
</h3>
<p className="text-gray-600">
لم يتم استلام أي طلبات حجز حتى الآن
{t('noReservationsHint')}
</p>
</div>
) : (
@ -1518,4 +1505,4 @@ export default function OwnerReservationRequestsPage() {
</div>
</div>
);
}
}

View File

@ -32,8 +32,9 @@ import Image from 'next/image';
import { getRentProperties, getSaleProperties } from './utils/api';
import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from './enums';
import AuthService from './services/AuthService';
import { useTranslation } from 'react-i18next';
function mapApiProperty(item, index) {
function mapApiProperty(t, item, index) {
const info = item.propertyInformation || {};
const dailyPrice = item.dailyRent ?? 0;
@ -48,11 +49,11 @@ function mapApiProperty(item, index) {
const status = PropertyStatusKeys[info.status] ?? PropertyStatusKeys[item.status] ?? 'available';
const features = [];
if (item.isSmokeAllow) features.push('يسمح بالتدخين');
if (item.isVisitorAllow) features.push('يسمح بالزوار');
if (item.specializedFor) features.push('متخصص');
if (info.numberOfBedRooms) features.push(`${info.numberOfBedRooms} غرف نوم`);
if (info.numberOfBathRooms) features.push(`${info.numberOfBathRooms} حمامات`);
if (item.isSmokeAllow) features.push(t('smoke-allowed'));
if (item.isVisitorAllow) features.push(t('visitors-allowed'));
if (item.specializedFor) features.push(t('specialized'));
if (info.numberOfBedRooms) features.push(`${info.numberOfBedRooms} ${t('rooms')}`);
if (info.numberOfBathRooms) features.push(`${info.numberOfBathRooms} ${t('bathrooms')}`);
const apiBase = typeof window !== 'undefined' ? (process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api') : '';
const rawImages = Array.isArray(info.images) ? info.images : [];
@ -68,7 +69,7 @@ function mapApiProperty(item, index) {
return {
id: item.id ?? index + 1,
title: info.address || `عقار #${item.id || index + 1}`,
title: info.address || t('propertyWithId', { id: item.id || index + 1 }),
description: info.description || '',
type: propType,
price: price,
@ -76,7 +77,7 @@ function mapApiProperty(item, index) {
priceUnit,
listingType: isRentListing ? 'rent' : 'sale',
location: {
city: extractCity(info.address) || 'دمشق',
city: extractCity(info.address) || 'damascus',
district: info.address || '',
address: info.address || '',
lat: parseFloat(info.cordsX) || 0,
@ -103,6 +104,7 @@ function mapApiProperty(item, index) {
}
export default function HomePage() {
const { t } = useTranslation();
const mapSectionRef = useRef(null);
const [searchFilters, setSearchFilters] = useState(null);
const [showMap, setShowMap] = useState(false);
@ -143,8 +145,8 @@ export default function HomePage() {
const rentList = Array.isArray(rentData) ? rentData : [];
const saleList = Array.isArray(saleData) ? saleData : [];
const mappedRent = rentList.map((p, i) => mapApiProperty(p, i));
const mappedSale = saleList.map((p, i) => mapApiProperty(p, rentList.length + i));
const mappedRent = rentList.map((p, i) => mapApiProperty(t, p, i));
const mappedSale = saleList.map((p, i) => mapApiProperty(t, p, rentList.length + i));
setRentProperties(mappedRent);
setSaleProperties(mappedSale);
@ -297,7 +299,7 @@ export default function HomePage() {
visible: { opacity: 1, y: 0 }
}}
>
إيجاد منزلك الجديد<br />
{t("heroTitleLine1")}<br />
<motion.span
className="text-amber-300"
animate={{
@ -309,7 +311,7 @@ export default function HomePage() {
ease: "easeInOut"
}}
>
أصبح سهلاً
{t("heroTitleLine2")}
</motion.span>
</motion.h1>
<motion.p
@ -319,7 +321,7 @@ export default function HomePage() {
visible: { opacity: 1, y: 0 }
}}
>
نوفر قوائم عقارات عالية الجودة لمساعدتك في إيجاد المنزل المثالي
{t("heroSubtitle")}
</motion.p>
</motion.div>
@ -333,17 +335,17 @@ export default function HomePage() {
className="bg-white/10 backdrop-blur-lg rounded-2xl p-8 text-center border border-white/20"
>
<h2 className="text-2xl font-bold text-amber-50 mb-2">
مرحباً {user?.name}!
{t("ownerGreeting")} {user?.name}!
</h2>
<p className="text-amber-50/80 mb-4">
يمكنك إدارة عقاراتك من خلال لوحة التحكم الخاصة بك
{t("ownerDescription")}
</p>
<Link
href="/owner/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"
>
<Building className="w-5 h-5" />
إدارة عقاراتي
{t("manageMyProperties")}
</Link>
</motion.div>
)}
@ -373,7 +375,7 @@ export default function HomePage() {
)}
</section>
{!isOwner && (
{!isOwner && (<>
<AnimatePresence mode="wait">
{showMap && (
<motion.section
@ -407,7 +409,7 @@ export default function HomePage() {
>
<div className="flex items-center justify-center gap-4 mb-2">
<h2 className="text-3xl font-bold text-gray-900">
{filteredProperties.length > 0 ? 'نتائج البحث' : 'لا توجد نتائج'}
{filteredProperties.length > 0 ? t('searchResults') : t('no-properties')}
</h2>
<motion.button
onClick={resetSearch}
@ -418,17 +420,17 @@ export default function HomePage() {
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
بحث جديد
{t('newSearch')}
</motion.button>
</div>
{filteredProperties.length > 0 ? (
<p className="text-gray-600">
تم العثور على {filteredProperties.length} عقار يطابق معايير البحث
{t('foundPropertiesCount', { count: filteredProperties.length })}
</p>
) : (
<p className="text-gray-600">
لا توجد عقارات تطابق معايير البحث. جرب تغيير الفلاتر.
{t('noPropertiesMatchFilters')}
</p>
)}
</motion.div>
@ -456,156 +458,143 @@ export default function HomePage() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد نتائج</h3>
<p className="text-gray-500">حاول تغيير معايير البحث</p>
<h3 className="text-xl font-bold text-gray-700 mb-2">{t('no-properties')}</h3>
<p className="text-gray-500">{t('try-changing-search-criteria')}</p>
</div>
)}
</motion.div>
</div>
</motion.section>
)}
</AnimatePresence>
</>)}
{filteredProperties.length > 0 && searchFilters && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="mt-6 flex flex-wrap gap-3 justify-center"
>
<div className="bg-white px-4 py-2 rounded-full shadow-sm border border-gray-200 text-sm">
<span className="text-gray-600">المدينة: </span>
<span className="font-bold text-gray-900">
{searchFilters.city === 'all' ? 'جميع المدن' : searchFilters.city}
</span>
</div>
<div className="bg-white px-4 py-2 rounded-full shadow-sm border border-gray-200 text-sm">
<span className="text-gray-600">نوع العقار: </span>
<span className="font-bold text-gray-900">
{searchFilters.propertyType === 'all' ? 'الكل' :
searchFilters.propertyType === 'apartment' ? 'شقة' :
searchFilters.propertyType === 'villa' ? 'فيلا' : 'بيت'}
</span>
</div>
<div className="bg-white px-4 py-2 rounded-full shadow-sm border border-gray-200 text-sm">
<span className="text-gray-600">نطاق السعر: </span>
<span className="font-bold text-gray-900">
{searchFilters.priceRange === 'all' ? 'جميع الأسعار' :
searchFilters.priceRange === '0-500' ? 'أقل من 50$' :
searchFilters.priceRange === '500-1000' ? '50$ - 100$' :
searchFilters.priceRange === '1000-2000' ? '100$ - 200$' :
searchFilters.priceRange === '2000-3000' ? '200$ - 300$' : 'أكثر من 300$'}
</span>
</div>
<div className="bg-white px-4 py-2 rounded-full shadow-sm border border-gray-200 text-sm">
<span className="text-gray-600">مصدر العرض: </span>
<span className="font-bold text-gray-900">
{searchFilters.ownerSource === 'all' ? 'الكل' :
searchFilters.ownerSource === 'owner' ? 'من المالك' : 'من مكتب عقاري'}
</span>
</div>
<div className="bg-white px-4 py-2 rounded-full shadow-sm border border-gray-200 text-sm">
<span className="text-gray-600">نوع الإيجار: </span>
<span className="font-bold text-gray-900">
{searchFilters.rentPeriod === 'all' ? 'الكل' :
searchFilters.rentPeriod === 'daily' ? 'إيجار يومي' : 'إيجار شهري'}
</span>
</div>
{searchFilters.availableToday && (
<div className="bg-white px-4 py-2 rounded-full shadow-sm border border-gray-200 text-sm">
<span className="font-bold text-gray-900">فقط المتاحة من اليوم</span>
</div>
)}
</motion.div>
)}
</div>
</motion.section>
)}
</AnimatePresence>
)}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="text-center mb-12"
>
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4 tracking-tight">
{t('whyChooseUsTitle')}
</h2>
<p className="text-gray-600 max-w-2xl mx-auto text-lg">
{t('whyChooseUsSubtitle')}
</p>
</motion.div>
<section className="py-20 bg-gradient-to-b from-white to-gray-50">
<div className="container mx-auto px-4">
<motion.div
className="text-center mb-12"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4 tracking-tight">
لماذا تختار سويت هوم؟
</h2>
<p className="text-gray-600 max-w-2xl mx-auto text-lg">
نجعل عملية إيجاد منزلك المثالي سهلة وسريعة
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
<motion.div
className="group bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.1 }}
whileHover={{ y: -4 }}
>
<div className="flex items-center gap-4 mb-4">
<div className="w-12 h-12 bg-amber-100 rounded-xl flex items-center justify-center group-hover:bg-amber-200 transition-colors duration-300">
<ShieldCheck className="w-6 h-6 text-amber-600" />
</div>
<h3 className="text-lg font-bold text-gray-900">
{t('feature1Title')}
</h3>
</div>
<p className="text-gray-600 text-sm leading-relaxed">
{t('feature1Description')}
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<motion.div
className="group bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.1 }}
whileHover={{ y: -4 }}
>
<div className="flex items-center gap-4 mb-4">
<div className="w-12 h-12 bg-amber-100 rounded-xl flex items-center justify-center group-hover:bg-amber-200 transition-colors duration-300">
<ShieldCheck className="w-6 h-6 text-amber-600" />
</div>
<h3 className="text-lg font-bold text-gray-900">
قوائم موثوقة
</h3>
<motion.div
className="group bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.2 }}
whileHover={{ y: -4 }}
>
<div className="flex items-center gap-4 mb-4">
<div className="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center group-hover:bg-blue-200 transition-colors duration-300">
<Lock className="w-6 h-6 text-blue-600" />
</div>
<h3 className="text-lg font-bold text-gray-900">
{t('feature2Title')}
</h3>
</div>
<p className="text-gray-600 text-sm leading-relaxed">
{t('feature2Description')}
</p>
</motion.div>
<motion.div
className="group bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.3 }}
whileHover={{ y: -4 }}
>
<div className="flex items-center gap-4 mb-4">
<div className="w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center group-hover:bg-green-200 transition-colors duration-300">
<Zap className="w-6 h-6 text-green-600" />
</div>
<h3 className="text-lg font-bold text-gray-900">
{t('feature3Title')}
</h3>
</div>
<p className="text-gray-600 text-sm leading-relaxed">
{t('feature3Description')}
</p>
</motion.div>
</div>
<p className="text-gray-600 text-sm leading-relaxed">
كل عقار يتم التحقق منه بدقة لضمان الدقة والجودة.
</p>
</motion.div>
<motion.div
className="group bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.2 }}
whileHover={{ y: -4 }}
>
<div className="flex items-center gap-4 mb-4">
<div className="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center group-hover:bg-blue-200 transition-colors duration-300">
<Lock className="w-6 h-6 text-blue-600" />
</div>
<h3 className="text-lg font-bold text-gray-900">
عمليات آمنة
</h3>
</div>
<p className="text-gray-600 text-sm leading-relaxed">
سلامتك هي أولويتنا. نوفر معاملات آمنة ونحمي معلوماتك الشخصية.
</p>
</motion.div>
<motion.div
className="group bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.3 }}
whileHover={{ y: -4 }}
>
<div className="flex items-center gap-4 mb-4">
<div className="w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center group-hover:bg-green-200 transition-colors duration-300">
<Zap className="w-6 h-6 text-green-600" />
</div>
<h3 className="text-lg font-bold text-gray-900">
نتائج سريعة
</h3>
</div>
<p className="text-gray-600 text-sm leading-relaxed">
اعثر على منزلك المثالي في دقائق باستخدام خوارزميات البحث والمطابقة المتقدمة لدينا.
</p>
</motion.div>
</div>
</div>
</section>
{filteredProperties.length > 0 && searchFilters && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4, type: "spring" }}
className="flex flex-wrap items-center justify-center gap-3"
>
<div className="bg-white px-4 py-2 rounded-full shadow-sm border border-gray-200 text-sm">
<span className="text-gray-600">{t('filterPropertyTypeLabel')}</span>
<span className="font-bold text-gray-900">
{searchFilters.propertyType === 'all' ? t('all') :
searchFilters.propertyType === 'apartment' ? t('buildingType.apartment') :
searchFilters.propertyType === 'villa' ? t('buildingType.villa') : t('house')}
</span>
</div>
<div className="bg-white px-4 py-2 rounded-full shadow-sm border border-gray-200 text-sm">
<span className="text-gray-600">{t('filterPriceRangeLabel')}</span>
<span className="font-bold text-gray-900">
{searchFilters.priceRange === 'all' ? t('allPrices') :
searchFilters.priceRange === '0-500' ? t('priceRangeUnder500') :
searchFilters.priceRange === '500-1000' ? t('priceRange50to100') :
searchFilters.priceRange === '1000-2000' ? t('priceRange100to200') :
searchFilters.priceRange === '2000-3000' ? t('priceRange200to300') : t('priceRangeOver300')}
</span>
</div>
<div className="bg-white px-4 py-2 rounded-full shadow-sm border border-gray-200 text-sm">
<span className="text-gray-600">{t('filterSourceLabel')}</span>
<span className="font-bold text-gray-900">
{searchFilters.ownerSource === 'all' ? t('all') :
searchFilters.ownerSource === 'owner' ? t('fromOwner') : t('fromAgency')}
</span>
</div>
<div className="bg-white px-4 py-2 rounded-full shadow-sm border border-gray-200 text-sm">
<span className="text-gray-600">{t('filterRentTypeLabel')}</span>
<span className="font-bold text-gray-900">
{searchFilters.rentPeriod === 'all' ? t('all') :
searchFilters.rentPeriod === 'daily' ? t('dailyRent') : t('monthlyRent')}
</span>
</div>
{searchFilters.availableToday && (
<div className="bg-white px-4 py-2 rounded-full shadow-sm border border-gray-200 text-sm">
<span className="font-bold text-gray-900">{t('availableFromToday')}</span>
</div>
)}
</motion.div>
)}
</div>
);
}

View File

@ -1,8 +1,9 @@
'use client';
"use client";
import { useEffect, useState, useCallback } from 'react';
import Link from 'next/link';
import { motion } from 'framer-motion';
import { useTranslation } from 'react-i18next';
import {
Home,
MapPin,
@ -66,7 +67,7 @@ const PAYMENT_METHODS = [
desc: 'سيتم تفعيل الدفع الإلكتروني والتحويل لاحقاً',
icon: CreditCard,
active: false,
},
}
];
function formatCurrency(v, sign = 'ل.س') {
@ -81,6 +82,7 @@ function formatDate(date) {
}
export default function PaymentsPage() {
const { t } = useTranslation();
const [reservations, setReservations] = useState([]);
const [loading, setLoading] = useState(true);
const [payingId, setPayingId] = useState(null);
@ -121,7 +123,7 @@ export default function PaymentsPage() {
setReservations(mapped);
} catch (err) {
console.error(err);
toast.error('فشل تحميل المدفوعات');
toast.error(t('payments.loadingTransactions'));
} finally {
setLoading(false);
}
@ -141,10 +143,10 @@ export default function PaymentsPage() {
setPayingId(reservation.id);
try {
await payDeposit({ reservationId: reservation.id });
toast.success('تم دفع السلفة بنجاح!');
toast.success(t('payments.depositPaidSuccess'));
loadReservations();
} catch (err) {
toast.error(err?.message || 'فشل عملية الدفع');
toast.error(err?.message || t('payments.paymentFailed'));
} finally {
setPayingId(null);
}
@ -171,16 +173,16 @@ export default function PaymentsPage() {
<div className="w-20 h-20 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-6">
<Lock className="w-10 h-10 text-amber-600" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-3">المدفوعات</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">
دفعاتك مرتبطة بحاسبك لذلك يرجى تسجيل الدخول أولاً
{t('payments.loginRequiredDesc')}
</p>
<Link
href="/login"
className="inline-flex items-center gap-2 bg-amber-500 hover:bg-amber-600 text-white px-8 py-3 rounded-2xl text-lg font-semibold transition shadow-lg shadow-amber-200"
>
<LogIn className="w-5 h-5" />
تسجيل الدخول
{t('login')}
</Link>
</motion.div>
</div>
@ -199,15 +201,15 @@ export default function PaymentsPage() {
animate={{ opacity: 1, y: 0 }}
className="mb-8"
>
<h1 className="text-3xl font-bold text-gray-900 mb-2">المدفوعات</h1>
<p className="text-gray-600">إدارة مدفوعات الحجوزات والدفعات المقدمة</p>
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t('payments.title')}</h1>
<p className="text-gray-600">{t('payments.description')}</p>
</motion.div>
{payables.length > 0 && (
<div className="space-y-6 mb-10">
<h2 className="text-xl font-bold text-gray-800 flex items-center gap-2">
<Wallet className="w-5 h-5 text-amber-500" />
المدفوعات المطلوبة
{t('payments.payablesTitle')}
</h2>
{payables.map((r, i) => (
<PaymentCard
@ -226,7 +228,7 @@ export default function PaymentsPage() {
<div className="space-y-4">
<h2 className="text-xl font-bold text-gray-800 flex items-center gap-2">
<Clock className="w-5 h-5 text-gray-500" />
الحجوزات السابقة
{t('payments.previousReservationsTitle')}
</h2>
{others.map((r, i) => {
const statusKey = STATUS_MAP[r.status] || 'pending';
@ -263,137 +265,138 @@ export default function PaymentsPage() {
</div>
)}
{reservations.length === 0 && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300"
>
<CreditCard className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد معاملات مالية</h3>
<p className="text-gray-500">ستظهر هنا مدفوعاتك للحجوزات</p>
</motion.div>
)}
{reservations.length === 0 && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300"
>
<CreditCard className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-700 mb-2">{t('payments.noTransactions')}</h3>
<p className="text-gray-500">{t('payments.noTransactionsDesc')}</p>
</motion.div>
)}
</div>
</div>
</div>
);
}
);
}
function PaymentCard({ reservation, payingId, selectedPayment, onSelectPayment, onPay }) {
const r = reservation;
const amount = r.depositAmount || r.totalPrice || 0;
const [showCashDialog, setShowCashDialog] = useState(false);
// eslint-disable-next-line react/prop-types
function PaymentCard({ reservation, payingId, selectedPayment, onSelectPayment, onPay }) {
const r = reservation;
const amount = r.depositAmount || r.totalPrice || 0;
const [showCashDialog, setShowCashDialog] = useState(false);
return (
<>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-3xl shadow-md border border-gray-200 overflow-hidden"
>
{/* Property details */}
<div className="p-6 border-b border-gray-100">
<div className="flex items-start gap-4 mb-4">
<div className="w-14 h-14 bg-amber-100 rounded-2xl flex items-center justify-center shrink-0">
<Home className="w-7 h-7 text-amber-600" />
return (
<>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-3xl shadow-md border border-gray-200 overflow-hidden"
>
{/* Property details */}
<div className="p-6 border-b border-gray-100">
<div className="flex items-start gap-4 mb-4">
<div className="w-14 h-14 bg-amber-100 rounded-2xl flex items-center justify-center shrink-0">
<Home className="w-7 h-7 text-amber-600" />
</div>
<div className="min-w-0 flex-1">
<h3 className="text-xl font-bold text-gray-900 mb-1">
{r.propertyName}
</h3>
{(r.propertyAddress || r.propertyCity) && (
<p className="text-sm text-gray-500 flex items-center gap-1">
<MapPin className="w-3.5 h-3.5" />
{[r.propertyCity, r.propertyAddress].filter(Boolean).join(' - ')}
</p>
)}
</div>
</div>
<div className="min-w-0 flex-1">
<h3 className="text-xl font-bold text-gray-900 mb-1">
{r.propertyName}
</h3>
{(r.propertyAddress || r.propertyCity) && (
<p className="text-sm text-gray-500 flex items-center gap-1">
<MapPin className="w-3.5 h-3.5" />
{[r.propertyCity, r.propertyAddress].filter(Boolean).join(' - ')}
</p>
)}
<div className="flex items-center gap-4 text-sm text-gray-600">
<span className="flex items-center gap-1.5">
<Calendar className="w-4 h-4 text-gray-400" />
{formatDate(r.startDate)} - {formatDate(r.endDate)}
</span>
<span className="flex items-center gap-1.5">
<Clock className="w-4 h-4 text-gray-400" />
#{r.reservationId || r.id}
</span>
</div>
</div>
<div className="flex items-center gap-4 text-sm text-gray-600">
<span className="flex items-center gap-1.5">
<Calendar className="w-4 h-4 text-gray-400" />
{formatDate(r.startDate)} - {formatDate(r.endDate)}
</span>
<span className="flex items-center gap-1.5">
<Clock className="w-4 h-4 text-gray-400" />
#{r.reservationId || r.id}
</span>
{/* Deposit amount */}
<div className="px-6 py-5 border-b border-gray-100">
<div className="text-center">
<p className="text-sm text-gray-500 mb-1">{t('payments.depositAmount')}</p>
<p className="text-4xl font-bold text-amber-600">
{formatCurrency(amount, r.currencySign)}
</p>
<p className="text-xs text-gray-400 mt-2">
{t('payments.depositPaidToPlatform')}
</p>
</div>
</div>
</div>
{/* Deposit amount */}
<div className="px-6 py-5 border-b border-gray-100">
<div className="text-center">
<p className="text-sm text-gray-500 mb-1">مبلغ التأمين (الرعبون)</p>
<p className="text-4xl font-bold text-amber-600">
{formatCurrency(amount, r.currencySign)}
</p>
<p className="text-xs text-gray-400 mt-2">
يتم دفع التأمين إلى المنصة وليس إلى المالك
</p>
{/* Payment methods note */}
<div className="px-6 py-4 bg-amber-50 border-b border-amber-100">
<div className="flex items-start gap-3">
<Info className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" />
<p className="text-sm text-amber-800 leading-relaxed">
{t('payments.cashOnlyNotice')}
</p>
</div>
</div>
</div>
{/* Payment methods note */}
<div className="px-6 py-4 bg-amber-50 border-b border-amber-100">
<div className="flex items-start gap-3">
<Info className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" />
<p className="text-sm text-amber-800 leading-relaxed">
الدفع النقدي فقط هو المتاح. سيتم تفعيل الدفع الإلكتروني والتحويل لاحقاً.
</p>
</div>
</div>
{/* Payment method options */}
<div className="px-6 py-5 border-b border-gray-100">
<p className="text-sm font-bold text-gray-700 mb-3">طريقة الدفع</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{PAYMENT_METHODS.map((method) => {
const isSelected = selectedPayment === method.id;
const Icon = method.icon;
return (
<button
key={method.id}
type="button"
disabled={!method.active}
onClick={() => method.active && onSelectPayment(method.id)}
className={`relative flex items-start gap-3 p-4 rounded-2xl border-2 text-right transition-all ${
!method.active
? 'border-gray-100 bg-gray-50 opacity-50 cursor-not-allowed'
: isSelected
? 'border-amber-500 bg-amber-50 shadow-sm'
: 'border-gray-200 bg-white hover:border-amber-300 cursor-pointer'
}`}
>
<div className={`w-10 h-10 rounded-xl flex items-center justify-center shrink-0 ${
isSelected ? 'bg-amber-500 text-white' : 'bg-gray-100 text-gray-500'
}`}>
<Icon className="w-5 h-5" />
</div>
<div className="min-w-0 flex-1">
<p className={`text-sm font-bold ${isSelected ? 'text-amber-900' : 'text-gray-800'}`}>
{method.label}
</p>
<p className="text-xs text-gray-500 mt-0.5 leading-relaxed">
{method.desc}
</p>
{!method.active && (
<span className="inline-block mt-1 text-[10px] font-medium text-gray-400 bg-gray-200 px-2 py-0.5 rounded-full">
غير متاح
</span>
)}
</div>
{isSelected && (
<div className="absolute top-2 left-2 w-5 h-5 bg-amber-500 rounded-full flex items-center justify-center">
<Check className="w-3 h-3 text-white" />
{/* Payment method options */}
<div className="px-6 py-5 border-b border-gray-100">
<p className="text-sm font-bold text-gray-700 mb-3">{t('payments.paymentMethodLabel')}</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{PAYMENT_METHODS.map((method) => {
const isSelected = selectedPayment === method.id;
const Icon = method.icon;
return (
<button
key={method.id}
type="button"
disabled={!method.active}
onClick={() => method.active && onSelectPayment(method.id)}
className={`relative flex items-start gap-3 p-4 rounded-2xl border-2 text-right transition-all ${
!method.active
? 'border-gray-100 bg-gray-50 opacity-50 cursor-not-allowed'
: isSelected
? 'border-amber-500 bg-amber-50 shadow-sm'
: 'border-gray-200 bg-white hover:border-amber-300 cursor-pointer'
}`}
>
<div className={`w-10 h-10 rounded-xl flex items-center justify-center shrink-0 ${
isSelected ? 'bg-amber-500 text-white' : 'bg-gray-100 text-gray-500'
}`}>
<Icon className="w-5 h-5" />
</div>
)}
</button>
);
})}
<div className="min-w-0 flex-1">
<p className={`text-sm font-bold ${isSelected ? 'text-amber-900' : 'text-gray-800'}`}>
{method.label}
</p>
<p className="text-xs text-gray-500 mt-0.5 leading-relaxed">
{method.desc}
</p>
{!method.active && (
<span className="inline-block mt-1 text-[10px] font-medium text-gray-400 bg-gray-200 px-2 py-0.5 rounded-full">
{t('validation.notAvailable', { defaultValue: 'غير متاح' })}
</span>
)}
</div>
{isSelected && (
<div className="absolute top-2 left-2 w-5 h-5 bg-amber-500 rounded-full flex items-center justify-center">
<Check className="w-3 h-3 text-white" />
</div>
)}
</button>
);
})}
</div>
</div>
</div>
{/* Pay button */}
<div className="px-6 py-5 border-b border-gray-100">
@ -408,12 +411,12 @@ function PaymentCard({ reservation, payingId, selectedPayment, onSelectPayment,
{payingId === r.id ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
جاري الدفع...
{t('payments.processingPayment')}
</>
) : (
<>
<Banknote className="w-5 h-5" />
دفع التأمين ({formatCurrency(amount, r.currencySign)})
{t('payments.payButton', { amount: formatCurrency(amount, r.currencySign) })}
</>
)}
</motion.button>
@ -421,14 +424,14 @@ function PaymentCard({ reservation, payingId, selectedPayment, onSelectPayment,
{/* Cash pay button */}
<div className="px-6 py-4 border-b border-gray-100">
<button
type="button"
onClick={() => setShowCashDialog(true)}
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" />
سأدفع نقداً
</button>
<button
type="button"
onClick={() => setShowCashDialog(true)}
className="w-full bg-white border-2 border-amber-200 hover:border-amber-400 text-amber-700 font-bold py-3 px-6 rounded-2xl text-base transition-all flex items-center justify-center gap-2"
>
<Building className="w-5 h-5" />
{t('payments.payCashButton')}
</button>
</div>
{/* Cash payment dialog */}
@ -442,15 +445,15 @@ function PaymentCard({ reservation, payingId, selectedPayment, onSelectPayment,
<div className="w-16 h-16 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-5">
<Clock className="w-8 h-8 text-amber-600" />
</div>
<h3 className="text-xl font-bold text-gray-900 mb-3">الدفع النقدي قيد الانتظار</h3>
<h3 className="text-xl font-bold text-gray-900 mb-3">{t('payments.cashPendingDialogTitle')}</h3>
<p className="text-gray-600 leading-relaxed mb-6">
اذهب إلى موقع المنصة وادفع العربون.
{t('payments.cashPendingDialogDesc1')}
<br />
ستقوم المنصة بتأكيد الدفع بعد استلام المبلغ.
{t('payments.cashPendingDialogDesc2')}
</p>
<div className="bg-gray-50 rounded-2xl p-4 mb-6 text-right">
<p className="text-sm font-bold text-gray-800 mb-1">موقع المنصة</p>
<p className="text-sm text-gray-600">مكتب المنصة: أبو رمانة، شارع المالكي، دمشق</p>
<p className="text-sm font-bold text-gray-800 mb-1">{t('payments.platformOfficeLabel')}</p>
<p className="text-sm text-gray-600">{t('payments.platformOfficeFullAddress')}</p>
<p className="text-sm text-gray-600 flex items-center gap-1 mt-1" dir="ltr">
<Phone className="w-3.5 h-3.5" />
+963567823411
@ -462,13 +465,13 @@ function PaymentCard({ reservation, payingId, selectedPayment, onSelectPayment,
onClick={() => setShowCashDialog(false)}
className="w-full px-5 py-3 rounded-xl border border-gray-300 text-gray-700 hover:bg-gray-100 transition-colors font-medium"
>
إغلاق
{t('payments.closeButton')}
</button>
<Link
href="/reservations"
className="w-full px-5 py-3 rounded-xl bg-amber-500 text-white font-semibold text-center hover:bg-amber-600 transition-colors"
>
حجوزاتي
{t('payments.myReservationsLink')}
</Link>
</div>
</motion.div>
@ -480,9 +483,9 @@ function PaymentCard({ reservation, payingId, selectedPayment, onSelectPayment,
<div className="flex items-start gap-3">
<Landmark className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" />
<div>
<p className="text-sm font-bold text-gray-800 mb-1">موقع الدفع النقدي</p>
<p className="text-sm font-bold text-gray-800 mb-1">{t('payments.cashPaymentLocationLabel')}</p>
<p className="text-sm text-gray-600 leading-relaxed">
مكتب المنصة: أبو رمانة، شارع المالكي، دمشق
{t('payments.platformOfficeFullAddress')}
</p>
<p className="text-sm text-gray-600 flex items-center gap-1 mt-1">
<Phone className="w-3.5 h-3.5" />

View File

@ -4,6 +4,7 @@ import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { useTranslation } from "react-i18next";
import {
User,
Mail,
@ -24,6 +25,7 @@ import AuthService from "../services/AuthService";
import { getCustomerByUserId, getOwnerByUserId } from "../utils/api";
export default function ProfilePage() {
const { t, i18n } = useTranslation();
const router = useRouter();
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
@ -76,11 +78,11 @@ export default function ProfilePage() {
bio: profile.bio || "",
location: profile.address || profile.location || "",
joinedDate: profile.createdAt
? new Date(profile.createdAt).toLocaleDateString("ar-SA", {
? new Date(profile.createdAt).toLocaleDateString(i18n.language === 'ar' ? 'ar-SA' : 'en-US', {
month: "long",
year: "numeric",
})
: new Date().toLocaleDateString("ar-SA", {
: new Date().toLocaleDateString(i18n.language === 'ar' ? 'ar-SA' : 'en-US', {
month: "long",
year: "numeric",
}),
@ -107,7 +109,7 @@ export default function ProfilePage() {
whatsapp: "",
bio: "",
location: "",
joinedDate: new Date().toLocaleDateString("ar-SA", {
joinedDate: new Date().toLocaleDateString(i18n.language === 'ar' ? 'ar-SA' : 'en-US', {
month: "long",
year: "numeric",
}),
@ -144,7 +146,7 @@ export default function ProfilePage() {
setFormData(updatedData);
localStorage.setItem("userProfile", JSON.stringify(updatedData));
setEditingBio(false);
toast.success("تم تحديث نبذة عني بنجاح");
toast.success(t('bio-updated-success'));
};
const fadeInUp = {
@ -158,14 +160,14 @@ export default function ProfilePage() {
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<Loader2 className="w-12 h-12 text-amber-500 animate-spin mx-auto mb-4" />
<p className="text-gray-600">جاري تحميل الملف الشخصي...</p>
<p className="text-gray-600">{t('loading-profile')}</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 py-8">
<div dir={i18n.language === 'ar' ? 'rtl' : 'ltr'} className="min-h-screen bg-gray-50 py-8">
<Toaster position="top-center" reverseOrder={false} />
<div className="container mx-auto px-4 max-w-4xl">
@ -179,7 +181,7 @@ export default function ProfilePage() {
className="flex items-center gap-2 text-gray-600 hover:text-amber-600 transition-colors"
>
<ArrowLeft className="w-5 h-5" />
<span>العودة للرئيسية</span>
<span>{t('backToHome')}</span>
</Link>
</motion.div>
@ -229,12 +231,12 @@ export default function ProfilePage() {
<div className="flex items-center justify-center gap-2 text-gray-500 mt-2">
<MapPin className="w-4 h-4" />
<span>{formData.location || "الموقع غير محدد"}</span>
<span>{formData.location || t('addressNotSpecified')}</span>
</div>
<div className="flex items-center justify-center gap-2 text-gray-500 mt-1">
<Calendar className="w-4 h-4" />
<span>عضو منذ {formData.joinedDate}</span>
<span> {t('member-since')} {formData.joinedDate}</span>
</div>
</div>
@ -242,7 +244,7 @@ export default function ProfilePage() {
<div className="bg-gray-50 p-4 rounded-xl group">
<div className="flex justify-between items-start mb-2">
<label className="text-sm font-medium text-gray-600">
البريد الإلكتروني
{t('emailLabel')}
</label>
</div>
<div className="flex items-center gap-2 text-gray-900">
@ -254,41 +256,41 @@ export default function ProfilePage() {
<div className="bg-gray-50 p-4 rounded-xl group">
<div className="flex justify-between items-start mb-2">
<label className="text-sm font-medium text-gray-600">
رقم الهاتف
{t('phoneLabel')}
</label>
</div>
<div className="flex items-center gap-2 text-gray-900">
<Phone className="w-5 h-5 text-gray-400" />
<span>{formData.phone || "غير محدد"}</span>
<span>{formData.phone || t('not-specified')}</span>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-xl group">
<div className="flex justify-between items-start mb-2">
<label className="text-sm font-medium text-gray-600">
رقم الواتساب
{t('whatsapp-label')}
</label>
</div>
<div className="flex items-center gap-2 text-gray-900">
<MessageCircle className="w-5 h-5 text-gray-400" />
<span>{formData.whatsapp || "غير محدد"}</span>
<span>{formData.whatsapp || t('not-specified')}</span>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-xl">
<label className="block text-sm font-medium text-gray-600 mb-2">
نوع الحساب
</label>
<label className="block text-sm font-medium text-gray-600 mb-2">
{t('account-type')}
</label>
<div className="flex items-center gap-2">
{user?.role === "owner" ? (
<>
<Building className="w-5 h-5 text-amber-500" />
<span className="text-gray-900">مالك عقار</span>
<span className="text-gray-900">{t('property-owner')}</span>
</>
) : (
<>
<Home className="w-5 h-5 text-blue-500" />
<span className="text-gray-900">مستأجر</span>
<span className="text-gray-900">{t('customer')}</span>
</>
)}
</div>
@ -348,15 +350,15 @@ export default function ProfilePage() {
<div className="grid grid-cols-3 gap-4 mt-6">
<div className="bg-amber-50 p-4 rounded-xl text-center">
<div className="text-2xl font-bold text-amber-600">12</div>
<div className="text-xs text-gray-600">عقارات</div>
<div className="text-xs text-gray-600">{t('propertiesListed')}</div>
</div>
<div className="bg-blue-50 p-4 rounded-xl text-center">
<div className="text-2xl font-bold text-blue-600">8</div>
<div className="text-xs text-gray-600">حجوزات نشطة</div>
<div className="text-xs text-gray-600">{t('citiesCovered')}</div>
</div>
<div className="bg-green-50 p-4 rounded-xl text-center">
<div className="text-2xl font-bold text-green-600">4.8</div>
<div className="text-xs text-gray-600">تقييم</div>
<div className="text-xs text-gray-600">{t('customerSatisfaction')}</div>
</div>
</div>
)}

View File

@ -35,9 +35,10 @@ import { getRentProperties, getSaleProperties } from '../utils/api';
import { useFavorites } from '@/app/contexts/FavoritesContext';
import AuthService from '@/app/services/AuthService';
import toast, { Toaster } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
// Map API data to UI format
function mapApiProperty(item, index) {
function mapApiProperty(t, item, index) {
const info = item.propertyInformation || {};
const dailyPrice = item.dailyRent ?? item.monthlyRent ?? item.price ?? 0;
@ -50,11 +51,11 @@ function mapApiProperty(item, index) {
const status = statusMap[info.status] ?? statusMap[item.status] ?? 'available';
const features = [];
if (item.isSmokeAllow) features.push('يسمح بالتدخين');
if (item.isVisitorAllow) features.push('يسمح بالزوار');
if (item.specializedFor) features.push('متخصص');
if (info.numberOfBedRooms) features.push(`${info.numberOfBedRooms} غرف نوم`);
if (info.numberOfBathRooms) features.push(`${info.numberOfBathRooms} حمامات`);
if (item.isSmokeAllow) features.push(t('smoke-allowed'));
if (item.isVisitorAllow) features.push(t('visitors-allowed'));
if (item.specializedFor) features.push(t('specialized'));
if (info.numberOfBedRooms) features.push(`${info.numberOfBedRooms} ${t('bedrooms')}`);
if (info.numberOfBathRooms) features.push(`${info.numberOfBathRooms} ${t('bathrooms')}`);
// Extract images from API and build full URLs
const apiBase = typeof window !== 'undefined' ? (process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api') : '';
@ -65,13 +66,13 @@ function mapApiProperty(item, index) {
return {
id: item.id ?? index + 1,
title: info.address || `عقار #${item.id || index + 1}`,
title: info.address || `${t('property')} #${item.id || index + 1}`,
description: info.description || '',
type: propType,
price: dailyPrice,
priceUnit: 'daily',
location: {
city: extractCity(info.address) || 'دمشق',
city: extractCity(info.address) || 'damascus',
district: info.address || '',
},
bedrooms: info.numberOfBedRooms || 0,
@ -88,9 +89,23 @@ function mapApiProperty(item, index) {
function extractCity(address) {
if (!address) return '';
const cities = ['دمشق', 'حلب', 'حمص', 'اللاذقية', 'درعا', 'طرطوس', 'السويداء', 'دير الزور', 'الرقة', 'إدلب', 'الحسكة', 'القامشلي', 'ريف دمشق'];
for (const city of cities) {
if (address.includes(city)) return city;
const cityMap = {
'دمشق': 'damascus',
'حلب': 'aleppo',
'حمص': 'homs',
'اللاذقية': 'latakia',
'درعا': 'daraa',
'طرطوس': 'tartous',
'السويداء': 'suweida',
'دير الزور': 'deirEzzor',
'الرقة': 'raqqa',
'إدلب': 'idlib',
'الحسكة': 'hasakah',
'القامشلي': 'qamishli',
'ريف دمشق': 'ruralDamascus'
};
for (const [arabic, key] of Object.entries(cityMap)) {
if (address.includes(arabic)) return key;
}
return '';
}
@ -98,6 +113,7 @@ function extractCity(address) {
// API-only — no fallback data
const PropertyCard = ({ property, viewMode = 'grid', onLoginRequired }) => {
const { t, i18n } = useTranslation();
const { isFavorite: checkFavorite, addFavorite, removeFavorite } = useFavorites();
const [favLoading, setFavLoading] = useState(false);
const [currentImage, setCurrentImage] = useState(0);
@ -118,7 +134,7 @@ const PropertyCard = ({ property, viewMode = 'grid', onLoginRequired }) => {
};
const formatCurrency = (amount) => {
return amount?.toLocaleString() + ' ل.س';
return amount?.toLocaleString() + ' ' + t('syp-symbol');
};
const getPropertyTypeIcon = (type) => {
@ -133,10 +149,10 @@ const PropertyCard = ({ property, viewMode = 'grid', onLoginRequired }) => {
const getPropertyTypeLabel = (type) => {
switch (type) {
case 'villa': return 'فيلا';
case 'apartment': return 'شقة';
case 'house': return 'بيت';
case 'studio': return 'استوديو';
case 'villa': return t('buildingType.villa');
case 'apartment': return t('buildingType.apartment');
case 'house': return t('house');
case 'studio': return t('buildingType.studio');
default: return type;
}
};
@ -187,33 +203,33 @@ const PropertyCard = ({ property, viewMode = 'grid', onLoginRequired }) => {
{getPropertyTypeLabel(property.type)}
</span>
<span className={`px-2 py-1 rounded-lg text-xs font-medium ${property.status === 'available' ? 'bg-gray-800 text-white' : 'bg-gray-200 text-gray-600'}`}>
{property.status === 'available' ? 'متاح' : 'محجوز'}
{property.status === 'available' ? t('available') : t('booked')}
</span>
</div>
<h3 className="text-xl font-bold text-gray-900 mb-1">{property.title}</h3>
<div className="flex items-center gap-1 text-gray-500 text-sm mb-3">
<MapPin className="w-4 h-4" />
{property.location.city}، {property.location.district}
{t('city.' + property.location.city)}، {property.location.district}
</div>
</div>
<div className="text-left">
<div className="text-2xl font-bold text-gray-900">{formatCurrency(property.price)}</div>
<div className="text-xs text-gray-500">/{property.priceUnit === 'daily' ? 'يوم' : 'شهر'}</div>
<div className="text-xs text-gray-500">/{property.priceUnit === 'daily' ? t('day') : t('month')}</div>
</div>
</div>
<div className="flex flex-wrap gap-4 mb-4">
<div className="flex items-center gap-1 text-gray-600">
<Bed className="w-4 h-4" />
<span>{property.bedrooms} غرف</span>
<span>{property.bedrooms} {t('rooms')}</span>
</div>
<div className="flex items-center gap-1 text-gray-600">
<Bath className="w-4 h-4" />
<span>{property.bathrooms} حمامات</span>
<span>{property.bathrooms} {t('bathrooms')}</span>
</div>
<div className="flex items-center gap-1 text-gray-600">
<Square className="w-4 h-4" />
<span>{property.area} م²</span>
<span>{property.area} {t('sqm')}</span>
</div>
</div>
@ -224,7 +240,7 @@ const PropertyCard = ({ property, viewMode = 'grid', onLoginRequired }) => {
href={`/property/${property.id}`}
className="flex-1 bg-gray-800 text-white py-3 rounded-xl font-medium hover:bg-gray-900 transition-colors text-center"
>
عرض التفاصيل
{t('viewDetails')}
</Link>
<button className="px-4 bg-gray-100 text-gray-700 py-3 rounded-xl font-medium hover:bg-gray-200 transition-colors flex items-center gap-2">
<Phone className="w-4 h-4" />
@ -269,18 +285,18 @@ const PropertyCard = ({ property, viewMode = 'grid', onLoginRequired }) => {
{getPropertyTypeLabel(property.type)}
</span>
{property.status === 'available' && (
<span className="px-2 py-1 bg-gray-800 text-white rounded-lg text-xs font-medium">متاح</span>
<span className="px-2 py-1 bg-gray-800 text-white rounded-lg text-xs font-medium">{t('available')}</span>
)}
</div>
<h3 className="font-bold text-gray-900 mb-1 line-clamp-1">{property.title}</h3>
<div className="flex items-center gap-1 text-gray-500 text-xs mb-2">
<MapPin className="w-3 h-3" />
<span className="line-clamp-1">{property.location.city}، {property.location.district}</span>
<span className="line-clamp-1">{t('city.' + property.location.city)}، {property.location.district}</span>
</div>
</div>
<div className="text-left">
<div className="text-xl font-bold text-gray-900">{formatCurrency(property.price)}</div>
<div className="text-xs text-gray-500">/{property.priceUnit === 'daily' ? 'يوم' : 'شهر'}</div>
<div className="text-xs text-gray-500">/{property.priceUnit === 'daily' ? t('day') : t('month')}</div>
</div>
</div>
@ -296,7 +312,7 @@ const PropertyCard = ({ property, viewMode = 'grid', onLoginRequired }) => {
</div>
<div className="flex items-center gap-1">
<Square className="w-4 h-4" />
<span>{property.area}م²</span>
<span>{property.area}{t('sqm')}</span>
</div>
</div>
{property.rating > 0 && (
@ -311,7 +327,7 @@ const PropertyCard = ({ property, viewMode = 'grid', onLoginRequired }) => {
href={`/property/${property.id}`}
className="block w-full bg-gray-800 text-white py-3 rounded-xl font-medium hover:bg-gray-900 transition-colors text-center"
>
عرض التفاصيل
{t('viewDetails')}
</Link>
</div>
</motion.div>
@ -319,31 +335,40 @@ const PropertyCard = ({ property, viewMode = 'grid', onLoginRequired }) => {
};
const FilterBar = ({ filters, onFilterChange }) => {
const { t, i18n } = useTranslation();
const [showFilters, setShowFilters] = useState(false);
const propertyTypes = [
{ id: 'all', label: 'الكل' },
{ id: 'apartment', label: 'شقة', icon: Building2 },
{ id: 'villa', label: 'فيلا', icon: Home },
{ id: 'house', label: 'بيت', icon: Home },
{ id: 'all', label: t('all') },
{ id: 'apartment', label: t('buildingType.apartment'), icon: Building2 },
{ id: 'villa', label: t('buildingType.villa'), icon: Home },
{ id: 'house', label: t('house'), icon: Home },
];
const priceRanges = [
{ id: 'all', label: 'جميع الأسعار' },
{ id: '0-500000', label: 'أقل من 500,000' },
{ id: '500000-1000000', label: '500,000 - 1,000,000' },
{ id: '1000000-2000000', label: '1,000,000 - 2,000,000' },
{ id: '2000000-5000000', label: '2,000,000 - 5,000,000' },
{ id: '5000000+', label: 'أكثر من 5,000,000' }
{ id: 'all', label: t('allPrices') },
{ id: '0-500000', label: t('price-range-less-than-500k') },
{ id: '500000-1000000', label: t('price-range-500k-to-1m') },
{ id: '1000000-2000000', label: t('price-range-1m-to-2m') },
{ id: '2000000-5000000', label: t('price-range-2m-to-5m') },
{ id: '5000000+', label: t('price-range-more-than-5m') }
];
const cities = [
{ id: 'all', label: 'جميع المدن' },
{ id: 'دمشق', label: 'دمشق' },
{ id: 'حلب', label: 'حلب' },
{ id: 'حمص', label: 'حمص' },
{ id: 'اللاذقية', label: 'اللاذقية' },
{ id: 'درعا', label: 'درعا' }
{ id: 'all', label: t('allCities') },
{ id: 'damascus', label: t('city.damascus') },
{ id: 'aleppo', label: t('city.aleppo') },
{ id: 'homs', label: t('city.homs') },
{ id: 'latakia', label: t('city.latakia') },
{ id: 'daraa', label: t('city.daraa') },
{ id: 'tartous', label: t('city.tartous') },
{ id: 'suweida', label: t('city.suweida') },
{ id: 'deirEzzor', label: t('city.deirEzzor') },
{ id: 'raqqa', label: t('city.raqqa') },
{ id: 'idlib', label: t('city.idlib') },
{ id: 'hasakah', label: t('city.hasakah') },
{ id: 'qamishli', label: t('city.qamishli') },
{ id: 'ruralDamascus', label: t('city.ruralDamascus') }
];
return (
@ -353,7 +378,7 @@ const FilterBar = ({ filters, onFilterChange }) => {
<Search className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
type="text"
placeholder="ابحث عن عقار..."
placeholder={t('search-property-placeholder')}
className="w-full pr-12 px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300 transition-all"
value={filters.search}
onChange={(e) => onFilterChange({ ...filters, search: e.target.value })}
@ -364,7 +389,7 @@ const FilterBar = ({ filters, onFilterChange }) => {
className="px-6 py-3 bg-gray-100 rounded-xl font-medium hover:bg-gray-200 transition-colors flex items-center gap-2 text-gray-700"
>
<Filter className="w-5 h-5" />
فلاتر متقدمة
{t('advanced-filters')}
<ChevronDown className={`w-4 h-4 transition-transform ${showFilters ? 'rotate-180' : ''}`} />
</button>
</div>
@ -379,7 +404,7 @@ const FilterBar = ({ filters, onFilterChange }) => {
>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 pt-4 border-t border-gray-100">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">نوع العقار</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('property-type')}</label>
<div className="flex flex-wrap gap-2">
{propertyTypes.map((type) => {
const Icon = type.icon;
@ -398,7 +423,7 @@ const FilterBar = ({ filters, onFilterChange }) => {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">المدينة</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('city')}</label>
<select
value={filters.city}
onChange={(e) => onFilterChange({ ...filters, city: e.target.value })}
@ -411,7 +436,7 @@ const FilterBar = ({ filters, onFilterChange }) => {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">نطاق السعر</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('price-range')}</label>
<select
value={filters.priceRange}
onChange={(e) => onFilterChange({ ...filters, priceRange: e.target.value })}
@ -424,13 +449,13 @@ const FilterBar = ({ filters, onFilterChange }) => {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">غرف النوم</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('bedrooms')}</label>
<select
value={filters.bedrooms}
onChange={(e) => onFilterChange({ ...filters, bedrooms: e.target.value })}
className="w-full px-4 py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300"
>
<option value="all">جميع الأعداد</option>
<option value="all">{t('all-numbers')}</option>
<option value="1">1+</option>
<option value="2">2+</option>
<option value="3">3+</option>
@ -440,18 +465,18 @@ const FilterBar = ({ filters, onFilterChange }) => {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">المساحة (م²)</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('area-sqm')}</label>
<div className="flex gap-2">
<input
type="number"
placeholder="من"
placeholder={t('from')}
className="w-full px-4 py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300"
value={filters.minArea}
onChange={(e) => onFilterChange({ ...filters, minArea: e.target.value })}
/>
<input
type="number"
placeholder="إلى"
placeholder={t('to')}
className="w-full px-4 py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300"
value={filters.maxArea}
onChange={(e) => onFilterChange({ ...filters, maxArea: e.target.value })}
@ -474,13 +499,13 @@ const FilterBar = ({ filters, onFilterChange }) => {
})}
className="px-6 py-2 bg-gray-100 rounded-xl font-medium hover:bg-gray-200 transition-colors text-gray-700"
>
إعادة تعيين
{t('reset')}
</button>
<button
onClick={() => setShowFilters(false)}
className="px-6 py-2 bg-gray-800 text-white rounded-xl font-medium hover:bg-gray-900 transition-colors"
>
تطبيق الفلاتر
{t('apply-filters')}
</button>
</div>
</motion.div>
@ -491,6 +516,7 @@ const FilterBar = ({ filters, onFilterChange }) => {
};
export default function PropertiesPage() {
const { t, i18n } = useTranslation();
const [purposeTab, setPurposeTab] = useState('rent');
const [viewMode, setViewMode] = useState('grid');
const [sortBy, setSortBy] = useState('newest');
@ -520,8 +546,8 @@ export default function PropertiesPage() {
const saleList = Array.isArray(saleData) ? saleData : [];
const mapped = [
...rentList.map((p, i) => ({ ...mapApiProperty(p, i), purpose: 'rent' })),
...saleList.map((p, i) => ({ ...mapApiProperty(p, rentList.length + i), purpose: 'sale' })),
...rentList.map((p, i) => ({ ...mapApiProperty(t, p, i), purpose: 'rent' })),
...saleList.map((p, i) => ({ ...mapApiProperty(t, p, rentList.length + i), purpose: 'sale' })),
];
if (mapped.length > 0) {
@ -575,26 +601,26 @@ export default function PropertiesPage() {
});
return (
<div 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">
<div className="container mx-auto px-4">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center mb-8"
>
<h1 className="text-4xl font-bold text-gray-900 mb-2">{purposeTab === 'rent' ? 'عقارات للإيجار' : 'عقارات للبيع'}</h1>
<p className="text-gray-500">أفضل العقارات في سوريا</p>
<h1 className="text-4xl font-bold text-gray-900 mb-2">{purposeTab === 'rent' ? t('properties-for-rent') : t('properties-for-sale')}</h1>
<p className="text-gray-500">{t('best-properties-in-syria')}</p>
{/* Purpose Toggle */}
<div className="flex justify-center mt-4">
<div className="inline-flex bg-gray-100 rounded-xl p-1">
<button onClick={() => setPurposeTab('rent')}
className={`px-6 py-2 rounded-lg text-sm font-medium transition-all ${purposeTab === 'rent' ? 'bg-white text-amber-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}>
للإيجار
{t('for-rent')}
</button>
<button onClick={() => setPurposeTab('sale')}
className={`px-6 py-2 rounded-lg text-sm font-medium transition-all ${purposeTab === 'sale' ? 'bg-white text-blue-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}>
للبيع
{t('for-sale')}
</button>
</div>
</div>
@ -609,7 +635,7 @@ export default function PropertiesPage() {
<div className="flex justify-between items-center my-6">
<div className="text-gray-600">
<span className="font-bold text-gray-900">{filteredProperties.length}</span> عقار متاح
<span className="font-bold text-gray-900">{filteredProperties.length}</span> {t('property-available')}
</div>
<div className="flex gap-3">
<select
@ -617,10 +643,10 @@ export default function PropertiesPage() {
onChange={(e) => setSortBy(e.target.value)}
className="px-4 py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300 text-gray-700"
>
<option value="newest">الأحدث</option>
<option value="price_asc">السعر: من الأقل</option>
<option value="price_desc">السعر: من الأعلى</option>
<option value="rating">التقييم</option>
<option value="newest">{t('newest')}</option>
<option value="price_asc">{t('price-low-to-high')}</option>
<option value="price_desc">{t('price-high-to-low')}</option>
<option value="rating">{t('rating')}</option>
</select>
<div className="flex gap-2">
<button
@ -657,8 +683,8 @@ export default function PropertiesPage() {
<div className="w-24 h-24 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Home className="w-12 h-12 text-gray-400" />
</div>
<h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد عقارات</h3>
<p className="text-gray-500">جرب تغيير معايير البحث</p>
<h3 className="text-xl font-bold text-gray-700 mb-2">{t('no-properties')}</h3>
<p className="text-gray-500">{t('try-changing-search-criteria')}</p>
</motion.div>
)}
</div>
@ -674,20 +700,20 @@ export default function PropertiesPage() {
<div className="w-14 h-14 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Heart className="w-7 h-7 text-amber-600" />
</div>
<h3 className="text-xl font-bold text-gray-900 mb-2">تسجيل الدخول مطلوب</h3>
<p className="text-gray-500 mb-6">يجب تسجيل الدخول لإضافة العقارات إلى المفضلة</p>
<h3 className="text-xl font-bold text-gray-900 mb-2">{t('login-required-title')}</h3>
<p className="text-gray-500 mb-6">{t('login-required-favorites-desc')}</p>
<div className="flex gap-3">
<button
onClick={() => setShowLoginDialog(false)}
className="flex-1 py-3 border border-gray-200 rounded-xl font-medium text-gray-600 hover:bg-gray-50 transition-colors"
>
إلغاء
{t('cancel')}
</button>
<Link
href="/login"
className="flex-1 py-3 bg-amber-500 text-white rounded-xl font-medium hover:bg-amber-600 transition-colors text-center"
>
تسجيل الدخول
{t('login')}
</Link>
</div>
</motion.div>

View File

@ -1,6 +1,7 @@
"use client";
import { useState, useEffect, useMemo, useRef } from "react";
import { useTranslation } from "react-i18next";
import { motion, AnimatePresence } from "framer-motion";
import toast, { Toaster } from "react-hot-toast";
import Link from "next/link";
@ -149,62 +150,9 @@ function buildImageUrl(img) {
return `${apiBase}${img.startsWith("/") ? "" : "/Pictures/"}${img}`;
}
const serviceLabels = {
Electricity: "كهرباء",
Internet: "إنترنت",
Heating: "تدفئة",
Water: "ماء",
Pool: "مسبح",
PrivateGarden: "حديقة خاصة",
Parking: "موقف سيارات",
Security247: "حراسة 24 س",
CentralHeating: "تدفئة مركزية",
CentralAirConditioning: "تكييف مركزي",
EquippedKitchen: "مطبخ مجهز",
MaidsRoom: "غرفة خادمة",
Elevator: "مصعد",
Gym: "نادي رياضي",
Sauna: "ساونا",
Jacuzzi: "جاكوزي",
Balcony: "بلكونة",
Rooftop: "سطح",
Furnished: "مفروش",
AirConditioning: "تكييف",
SatelliteTV: "تلفاز",
Fireplace: "مدفأة",
StudyRoom: "غرفة دراسة",
Storage: "مستودع",
Laundry: "غرفة غسيل",
SmartHome: "منزل ذكي",
};
const termLabels = {
NoSmoking: "ممنوع التدخين",
NoAnimals: "ممنوع الحيوانات الأليفة",
NoParties: "ممنوع الحفلات",
NoAlcohol: "ممنوع الكحول",
SuitableForChildren: "مناسب للأطفال",
SuitableForFamilies: "مناسب للعائلات",
SuitableForStudents: "مناسب للطلاب",
SuitableForElderly: "مناسب لكبار السن",
OnlyFemales: "إناث فقط",
OnlyMales: "ذكور فقط",
};
const proximityLabels = {
School: "مدرسة",
Hospital: "مستشفى",
Restaurant: "مطعم",
University: "جامعة",
Park: "حديقة",
Mall: "مركز تسوق",
Supermarket: "سوبر ماركت",
Pharmacy: "صيدلية",
Mosque: "مسجد",
Bank: "بنك",
Airport: "مطار",
BusStation: "موقف باص",
};
const svcKey = (k) => "propertyService." + k.charAt(0).toLowerCase() + k.slice(1);
const termKey = (k) => "propertyTerm." + k.charAt(0).toLowerCase() + k.slice(1);
const proxKey = (k) => "proximity." + k.toLowerCase();
function mapApiDetail(item) {
if (!item) return null;
@ -231,30 +179,10 @@ function mapApiDetail(item) {
BuildingTypeKeys[info.buildingType] ??
BuildingTypeKeys[item.type] ??
"apartment";
const typeLabel =
{
apartment: "شقة",
villa: "فيلا",
house: "بيت",
sweet: "سويت",
studio: "استوديو",
office: "مكتب",
shop: "متجر",
warehouse: "مستودع",
farms: "مزرعة",
room: "غرفة ضمن شقة",
}[propType] || "عقار";
const status =
PropertyStatusKeys[info.status] ??
PropertyStatusKeys[item.status] ??
"available";
const statusLabel =
{
available: "متاح",
booked: "محجوز",
notAvailable: "غير متاح",
maintenance: "صيانة",
}[status] || "متاح";
const rawImages = Array.isArray(info.images) ? info.images : [];
const photosFallback = Array.isArray(details.photos) ? details.photos : [];
@ -312,10 +240,10 @@ function mapApiDetail(item) {
return {
id: item.id,
propertyInformationId: info.id,
title: details.description || info.address || `عقار #${item.id}`,
title: details.description || info.address || "",
description: details.description || info.description || "",
type: propType,
typeLabel,
typeLabel: propType,
price,
priceUnit,
priceDisplay: { daily: dailyPrice, monthly: monthlyPrice, sale: salePrice },
@ -324,7 +252,7 @@ function mapApiDetail(item) {
propertyCondition,
furnished,
location: {
city: extractCity(info.address) || "دمشق",
city: extractCity(info.address) || "damascus",
address: info.address || "",
lat: parseFloat(info.cordsX) || 0,
lng: parseFloat(info.cordsY) || 0,
@ -337,7 +265,7 @@ function mapApiDetail(item) {
balconies,
images,
status,
statusLabel,
statusLabel: status,
services,
terms,
proximity,
@ -355,6 +283,7 @@ function mapApiDetail(item) {
}
export default function PropertyDetailsPage() {
const { t, i18n } = useTranslation();
const params = useParams();
const router = useRouter();
const { isFavorite, addFavorite, removeFavorite } = useFavorites();
@ -483,7 +412,7 @@ export default function PropertyDetailsPage() {
setContactInfo(info);
setShowContact(true);
} catch (err) {
toast.error("فشل تحميل معلومات الاتصال");
toast.error(t("contactLoadFailed"));
}
};
@ -501,7 +430,7 @@ export default function PropertyDetailsPage() {
await addFavorite(property.id);
}
} catch (err) {
toast.error("حدث خطأ");
toast.error(t("errorOccurred"));
} finally {
setFavLoading(false);
}
@ -513,7 +442,7 @@ export default function PropertyDetailsPage() {
return;
}
if (!bookingDates.start || !bookingDates.end) {
setBookingError("يرجى تحديد تاريخ البداية والنهاية");
setBookingError(t("validationDateRequired"));
return;
}
setBookingLoading(true);
@ -525,29 +454,23 @@ export default function PropertyDetailsPage() {
bookingDates.end,
);
setBookingSuccess(true);
toast.success("تم إرسال طلب الحجز بنجاح");
toast.success(t("bookingSentSuccess"));
} catch (err) {
setBookingError(err.message || "فشل الحجز");
setBookingError(err.message || t("bookingFailed"));
} finally {
setBookingLoading(false);
}
};
const MONTHS_AR = [
"يناير",
"فبراير",
"مارس",
"أبريل",
"مايو",
"يونيو",
"يوليو",
"أغسطس",
"سبتمبر",
"أكتوبر",
"نوفمبر",
"ديسمبر",
const MONTH_KEYS = [
"month.january", "month.february", "month.march", "month.april",
"month.may", "month.june", "month.july", "month.august",
"month.september", "month.october", "month.november", "month.december",
];
const DAY_KEYS = [
"dayAbbr.sun", "dayAbbr.mon", "dayAbbr.tue", "dayAbbr.wed",
"dayAbbr.thu", "dayAbbr.fri", "dayAbbr.sat",
];
const DAYS_AR = ["ح", "ن", "ث", "ر", "خ", "ج", "س"];
const availableDatesSet = useMemo(() => {
const dates = new Set();
@ -592,7 +515,7 @@ export default function PropertyDetailsPage() {
return;
}
if (!selectedStart || !selectedEnd) {
setBookingError("يرجى تحديد تاريخ البداية والنهاية");
setBookingError(t("validationDateRequired"));
return;
}
setBookingLoading(true);
@ -603,9 +526,9 @@ export default function PropertyDetailsPage() {
const endDate = new Date(selectedEnd + "T00:00:00.000").toISOString();
await bookReservation(propInfoId, startDate, endDate);
setBookingSuccess(true);
toast.success("تم إرسال طلب الحجز بنجاح");
toast.success(t("bookingSentSuccess"));
} catch (err) {
setBookingError(err.message || "فشل الحجز");
setBookingError(err.message || t("bookingFailed"));
} finally {
setBookingLoading(false);
}
@ -636,7 +559,7 @@ export default function PropertyDetailsPage() {
<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">جاري تحميل العقار...</p>
<p className="text-gray-600">{t("loadingProperty")}</p>
</div>
</div>
);
@ -650,15 +573,15 @@ export default function PropertyDetailsPage() {
<Home className="w-12 h-12 text-amber-600" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">
العقار غير موجود
{t("propertyNotFound")}
</h2>
<p className="text-gray-600 mb-6">لم يتم العثور على العقار المطلوب</p>
<p className="text-gray-600 mb-6">{t("propertyNotFoundDesc")}</p>
<Link
href="/properties"
className="inline-flex items-center gap-2 bg-amber-500 text-white px-6 py-3 rounded-xl font-medium hover:bg-amber-600"
>
<ArrowLeft className="w-5 h-5" />
العودة إلى العقارات
{t("backToProperties")}
</Link>
</div>
</div>
@ -679,7 +602,7 @@ export default function PropertyDetailsPage() {
: "daily";
return (
<div className="min-h-screen bg-gray-50" dir="rtl">
<div className="min-h-screen bg-gray-50" dir={i18n.language === 'ar' ? 'rtl' : 'ltr'}>
<Toaster position="top-center" reverseOrder={false} />
<div className="container mx-auto px-4 py-6">
@ -693,7 +616,7 @@ export default function PropertyDetailsPage() {
className="inline-flex items-center gap-2 text-gray-600 hover:text-amber-600 transition-colors"
>
<ArrowLeft className="w-5 h-5" />
العودة إلى العقارات
{t("backToProperties")}
</Link>
</motion.div>
@ -723,15 +646,15 @@ export default function PropertyDetailsPage() {
>
<div className="text-center">
<ImageIcon className="w-20 h-20 text-gray-500 mx-auto mb-2" />
<p className="text-gray-400 text-sm">لا توجد صور</p>
<p className="text-gray-400 text-sm">{t("noImages")}</p>
</div>
</div>
)}
{isMostRequested && (
<div className="absolute top-4 right-4 z-10">
<span className="bg-gradient-to-l from-amber-500 to-amber-600 text-white px-3 py-1.5 rounded-full text-xs font-bold shadow-lg flex items-center gap-1">
<Star className="w-3 h-3 fill-white" /> الأكثر طلباً
<span className="bg-gradient-to-l from-amber-500 to-amber-600 text-white px-3 py-1.5 rounded-full text-xs font-bold shadow-lg flex items-center gap-1">
<Star className="w-3 h-3 fill-white" /> {t("mostRequested")}
</span>
</div>
)}
@ -765,10 +688,10 @@ export default function PropertyDetailsPage() {
<div className="absolute bottom-4 right-4 flex gap-2 z-10">
<span className="bg-black/70 text-white px-3 py-1 rounded-full text-sm font-medium backdrop-blur-sm">
{property.statusLabel}
{t("propertyStatus." + property.statusLabel)}
</span>
<span className="bg-black/70 text-white px-3 py-1 rounded-full text-sm font-medium backdrop-blur-sm">
{property.typeLabel}
{t("buildingType." + property.typeLabel)}
</span>
</div>
<div className="absolute bottom-4 left-4 bg-black/70 text-white px-3 py-1 rounded-full text-xs backdrop-blur-sm z-10">
@ -808,21 +731,21 @@ export default function PropertyDetailsPage() {
<div>
<div className="flex items-center gap-1.5 mb-1 flex-wrap">
<span className="px-2 py-0.5 bg-amber-100 text-amber-800 rounded-full text-xs">
{property.typeLabel}
{t("buildingType." + property.typeLabel)}
</span>
<span
className={`px-2 py-0.5 rounded-full text-xs ${property.status === "available" ? "bg-green-100 text-green-800" : "bg-yellow-100 text-yellow-800"}`}
>
{property.statusLabel}
{t("propertyStatus." + property.statusLabel)}
</span>
{property.isRent && property.displayType && (
<span className="px-2 py-0.5 bg-blue-100 text-blue-800 rounded-full text-xs">
{(() => {
const dt = property.displayType.toLowerCase();
if (dt === "both" || dt.includes("both"))
return "يومي وشهري";
if (dt.includes("daily")) return "يومي";
if (dt.includes("monthly")) return "شهري";
return t("dailyAndMonthly");
if (dt.includes("daily")) return t("rentType.daily");
if (dt.includes("monthly")) return t("rentType.monthly");
return dt;
})()}
</span>
@ -830,16 +753,16 @@ export default function PropertyDetailsPage() {
<span
className={`px-2 py-0.5 rounded-full text-xs ${property.furnished ? "bg-purple-100 text-purple-800" : "bg-gray-100 text-gray-600"}`}
>
{property.furnished ? "مفروش" : "غير مفروش"}
{property.furnished ? t("rentCondition.furnished") : t("rentCondition.unfurnished")}
</span>
</div>
<h1 className="text-xl font-bold text-gray-900">
{property.title}
{property.title || t("propertyWithId", { id: property.id })}
</h1>
<div className="flex items-center gap-1 text-gray-500 text-xs mt-0.5">
<MapPin className="w-3 h-3" />
<span>
{property.location.address || property.location.city}
{property.location.address || t("city." + property.location.city)}
</span>
</div>
</div>
@ -863,7 +786,7 @@ export default function PropertyDetailsPage() {
<span className="text-2xl font-bold text-amber-600">
{formatCurrency(property.priceDisplay.monthly)}
</span>
<span className="text-gray-500 mr-1">ل.س / شهرياً</span>
<span className="text-gray-500 mr-1">{t("sypPerMonth")}</span>
</div>
)}
{property.priceDisplay.daily > 0 && (
@ -871,13 +794,13 @@ export default function PropertyDetailsPage() {
<span className="text-2xl font-bold text-amber-600">
{formatCurrency(property.priceDisplay.daily)}
</span>
<span className="text-gray-500 mr-1">ل.س / يومياً</span>
<span className="text-gray-500 mr-1">{t("sypPerDay")}</span>
</div>
)}
{property.deposit > 0 && (
<div className="text-sm text-gray-500">
<span className="font-medium">تأمين:</span>{" "}
{formatCurrency(property.deposit)} ل.س
<span className="font-medium">{t("depositLabel")}</span>{" "}
{formatCurrency(property.deposit)} {t("syp")}
</div>
)}
</div>
@ -886,8 +809,8 @@ export default function PropertyDetailsPage() {
<span className="text-2xl font-bold text-blue-600">
{formatCurrency(property.price)}
</span>
<span className="text-gray-500 mr-1">ل.س</span>
<span className="text-sm text-gray-400 mr-2">للبيع</span>
<span className="text-gray-500 mr-1">{t("syp")}</span>
<span className="text-sm text-gray-400 mr-2">{t("forSale")}</span>
</div>
)}
</div>
@ -900,7 +823,7 @@ export default function PropertyDetailsPage() {
<div className="font-bold text-gray-900 text-sm">
{property.bedrooms}
</div>
<div className="text-[10px] text-gray-500">غرف نوم</div>
<div className="text-[10px] text-gray-500">{t("bedrooms")}</div>
</div>
)}
{property.bathrooms > 0 && (
@ -909,7 +832,7 @@ export default function PropertyDetailsPage() {
<div className="font-bold text-gray-900 text-sm">
{property.bathrooms}
</div>
<div className="text-[10px] text-gray-500">حمامات</div>
<div className="text-[10px] text-gray-500">{t("bathrooms")}</div>
</div>
)}
{property.area > 0 && (
@ -918,7 +841,7 @@ export default function PropertyDetailsPage() {
<div className="font-bold text-gray-900 text-sm">
{property.area}
</div>
<div className="text-[10px] text-gray-500">م²</div>
<div className="text-[10px] text-gray-500">{t("sqm")}</div>
</div>
)}
{property.floor > 0 && (
@ -927,7 +850,7 @@ export default function PropertyDetailsPage() {
<div className="font-bold text-gray-900 text-sm">
{property.floor}
</div>
<div className="text-[10px] text-gray-500">طابق</div>
<div className="text-[10px] text-gray-500">{t("floor")}</div>
</div>
)}
{property.salons > 0 && (
@ -936,7 +859,7 @@ export default function PropertyDetailsPage() {
<div className="font-bold text-gray-900 text-sm">
{property.salons}
</div>
<div className="text-[10px] text-gray-500">صالونات</div>
<div className="text-[10px] text-gray-500">{t("salons")}</div>
</div>
)}
{property.balconies > 0 && (
@ -945,7 +868,7 @@ export default function PropertyDetailsPage() {
<div className="font-bold text-gray-900 text-sm">
{property.balconies}
</div>
<div className="text-[10px] text-gray-500">بلكونات</div>
<div className="text-[10px] text-gray-500">{t("balconies")}</div>
</div>
)}
{avgRating !== null && avgRating > 0 && (
@ -954,7 +877,7 @@ export default function PropertyDetailsPage() {
<div className="font-bold text-gray-900 text-sm">
{avgRating.toFixed(1)}
</div>
<div className="text-[10px] text-gray-500">التقييم</div>
<div className="text-[10px] text-gray-500">{t("rateAndReview")}</div>
</div>
)}
{property.bookedCount > 0 && (
@ -963,7 +886,7 @@ export default function PropertyDetailsPage() {
<div className="font-bold text-gray-900 text-sm">
{property.bookedCount}
</div>
<div className="text-[10px] text-gray-500">حجوزات</div>
<div className="text-[10px] text-gray-500">{t("bookingCount")}</div>
</div>
)}
</div>
@ -972,7 +895,7 @@ export default function PropertyDetailsPage() {
{property.description && (
<div className="mb-4">
<h3 className="font-bold text-gray-900 mb-1 text-sm">
الوصف
{t("description")}
</h3>
<p className="text-gray-600 text-sm leading-relaxed">
{property.description}
@ -984,17 +907,17 @@ export default function PropertyDetailsPage() {
<div className="flex flex-wrap gap-1.5 mb-4">
{property.isSmokeAllow && (
<span className="px-2 py-0.5 bg-gray-100 text-gray-700 rounded-full text-xs border flex items-center gap-1">
<Wind className="w-3 h-3" /> يسمح بالتدخين
<Wind className="w-3 h-3" /> {t("smokingAllowed")}
</span>
)}
{!property.isSmokeAllow && (
<span className="px-2 py-0.5 bg-gray-100 text-gray-700 rounded-full text-xs border flex items-center gap-1">
<Ban className="w-3 h-3" /> ممنوع التدخين
<Ban className="w-3 h-3" /> {t("propertyTerm.noSmoking")}
</span>
)}
{property.isVisitorAllow && (
<span className="px-2 py-0.5 bg-gray-100 text-gray-700 rounded-full text-xs border flex items-center gap-1">
<Users className="w-3 h-3" /> يسمح بالزوار
<Users className="w-3 h-3" /> {t("visitorsAllowed")}
</span>
)}
{property.specializedFor && (
@ -1011,7 +934,7 @@ export default function PropertyDetailsPage() {
: Object.keys(property.services).length > 0) && (
<div className="mb-4">
<h3 className="font-bold text-gray-900 mb-2 text-sm">
الخدمات
{t("services")}
</h3>
<div className="flex flex-wrap gap-1.5">
{Array.isArray(property.services)
@ -1020,7 +943,7 @@ export default function PropertyDetailsPage() {
key={i}
className="px-3 py-1 bg-green-50 text-green-700 rounded-full text-sm border border-green-200 flex items-center gap-1"
>
{serviceLabels[svc] || svc}
{t(svcKey(svc))}
</span>
))
: Object.entries(property.services).map(
@ -1037,7 +960,7 @@ export default function PropertyDetailsPage() {
key={key}
className="px-3 py-1 bg-green-50 text-green-700 rounded-full text-sm border border-green-200 flex items-center gap-1"
>
{serviceLabels[key] || key}
{t(svcKey(key))}
{detail && (
<span className="text-green-400">
· {detail}
@ -1056,7 +979,7 @@ export default function PropertyDetailsPage() {
<div className="mb-4 bg-blue-50 rounded-xl p-3">
<h3 className="font-bold text-gray-900 mb-2 text-sm flex items-center gap-2">
<Info className="w-4 h-4 text-blue-500" />
تفاصيل الغرفة
{t("roomDetails")}
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{(() => {
@ -1064,117 +987,117 @@ export default function PropertyDetailsPage() {
const items = [];
if (rd.areaType)
items.push({
label: "نوع المساحة",
label: t("roomDetail.areaType"),
value:
rd.areaType === "private room"
? "غرفة خاصة"
? t("roomDetail.privateRoom")
: rd.areaType === "shared room"
? "غرفة مشتركة"
? t("roomDetail.sharedRoom")
: rd.areaType,
});
if (rd.peopleAllowed)
items.push({
label: "عدد الأشخاص",
label: t("roomDetail.numberOfPeople"),
value: rd.peopleAllowed,
});
if (rd.furnitureDetails || rd.furniture)
items.push({
label: "الأثاث",
label: t("roomDetail.furniture"),
value: rd.furnitureDetails || rd.furniture,
});
if (rd.entranceType)
items.push({
label: "نوع المدخل",
label: t("roomDetail.entranceType"),
value:
rd.entranceType === "shared entrance"
? "مدخل مشترك"
? t("roomDetail.sharedEntrance")
: rd.entranceType === "independent entrance"
? "مدخل مستقل"
? t("roomDetail.independentEntrance")
: rd.entranceType,
});
if (rd.bathroomType)
items.push({
label: "الحمام",
label: t("roomDetail.bathroom"),
value:
rd.bathroomType === "room specific"
? "خاص بالغرفة"
? t("roomDetail.bathroomPrivate")
: rd.bathroomType === "shared"
? "مشترك"
? t("roomDetail.bathroomShared")
: rd.bathroomType,
});
if (rd.kitchenType)
items.push({
label: "المطبخ",
label: t("roomDetail.kitchen"),
value:
rd.kitchenType === "shared"
? "مشترك"
? t("roomDetail.kitchenShared")
: rd.kitchenType === "not available"
? "غير متوفر"
? t("roomDetail.kitchenNotAvailable")
: rd.kitchenType,
});
if (rd.homeResidentsCount ?? rd.residents)
items.push({
label: "عدد السكان",
label: t("roomDetail.residents"),
value: rd.homeResidentsCount ?? rd.residents,
});
if (rd.currentPopulationGender)
items.push({
label: "جنس السكان",
label: t("roomDetail.gender"),
value:
rd.currentPopulationGender === "men"
? "رجال"
? t("roomDetail.men")
: rd.currentPopulationGender === "women"
? "نساء"
? t("roomDetail.women")
: rd.currentPopulationGender === "family"
? "عائلة"
? t("roomDetail.family")
: rd.currentPopulationGender,
});
if (rd.dedicatedTo)
items.push({
label: "مخصص لـ",
label: t("roomDetail.dedicatedTo"),
value:
rd.dedicatedTo === "men only"
? "رجال فقط"
? t("roomDetail.menOnly")
: rd.dedicatedTo === "women only"
? "نساء فقط"
? t("roomDetail.womenOnly")
: rd.dedicatedTo === "families only"
? "عائلات فقط"
? t("roomDetail.familiesOnly")
: rd.dedicatedTo === "everyone"
? "الجميع"
? t("roomDetail.everyone")
: rd.dedicatedTo,
});
if (rd.hasRestrictedOwnerAreas !== undefined)
items.push({
label: "مناطق ممنوعة",
value: rd.hasRestrictedOwnerAreas ? "نعم" : "لا",
label: t("roomDetail.restrictedAreas"),
value: rd.hasRestrictedOwnerAreas ? t("roomDetail.yes") : t("roomDetail.no"),
});
if (rd.hasChildren !== undefined)
items.push({
label: "أطفال",
value: rd.hasChildren ? "مسموح" : "غير مسموح",
label: t("roomDetail.children"),
value: rd.hasChildren ? t("roomDetail.allowed") : t("roomDetail.notAllowed"),
});
if (rd.hasPets !== undefined)
items.push({
label: "حيوانات أليفة",
value: rd.hasPets ? "مسموح" : "غير مسموح",
label: t("roomDetail.pets"),
value: rd.hasPets ? t("roomDetail.allowed") : t("roomDetail.notAllowed"),
});
if (rd.languageDialect)
items.push({
label: "اللغة",
label: t("roomDetail.language"),
value: rd.languageDialect,
});
if (rd.visitorsAllowed !== undefined)
items.push({
label: "الزوار",
value: rd.visitorsAllowed ? "مسموح" : "ممنوع",
label: t("roomDetail.visitors"),
value: rd.visitorsAllowed ? t("roomDetail.allowed") : t("roomDetail.forbidden"),
});
if (rd.quietTimesEnabled ?? rd.quietTimes)
items.push({
label: "أوقات الهدوء",
label: t("roomDetail.quietHours"),
value:
rd.quietTimesDetails ||
rd.quietTimes ||
(rd.quietTimesEnabled ? "مفعلة" : ""),
(rd.quietTimesEnabled ? t("roomDetail.quietHoursEnabled") : ""),
});
return items.map((item, i) => (
<div
@ -1198,7 +1121,7 @@ export default function PropertyDetailsPage() {
{Object.keys(property.proximity).length > 0 && (
<div className="mb-4">
<h3 className="font-bold text-gray-900 mb-2 text-sm">
القرب من الخدمات
{t("proximityToServices")}
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-1.5">
{Object.entries(property.proximity).map(([key, val]) => {
@ -1239,10 +1162,10 @@ export default function PropertyDetailsPage() {
)}
<div>
<div className="text-xs font-medium text-gray-900">
{proximityLabels[key] || key}
{t(proxKey(key))}
</div>
<div className="text-[10px] text-gray-500">
{dist} {typeof dist === "number" ? "كم" : ""}
{dist} {typeof dist === "number" ? t("km") : ""}
</div>
</div>
</div>
@ -1256,7 +1179,7 @@ export default function PropertyDetailsPage() {
{Object.keys(property.terms).length > 0 && (
<div className="mb-4">
<h3 className="font-bold text-gray-900 mb-2 text-sm">
الشروط
{t("terms")}
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-1.5">
{Object.entries(property.terms).map(([key, val]) => {
@ -1272,7 +1195,7 @@ export default function PropertyDetailsPage() {
<Check className="w-3.5 h-3.5 text-green-500 flex-shrink-0" />
)}
<span className="text-xs text-gray-700">
{termLabels[key] || key}
{t(termKey(key))}
</span>
</div>
);
@ -1298,7 +1221,7 @@ export default function PropertyDetailsPage() {
</div>
<div className="p-3 bg-amber-50 text-center text-sm text-amber-700 flex items-center justify-center gap-2">
<Info className="w-4 h-4" />
<span>موقع تقريبي يظهر الموقع الدقيق بعد تأكيد الحجز</span>
<span>{t("approximateLocation")}</span>
</div>
<div className="p-3 border-t border-gray-100">
<a
@ -1308,7 +1231,7 @@ export default function PropertyDetailsPage() {
className="flex items-center justify-center gap-2 text-blue-600 hover:text-blue-700 font-medium text-sm"
>
<ExternalLink className="w-4 h-4" />
فتح في Google Maps
{t("openInGoogleMaps")}
</a>
</div>
</motion.div>
@ -1340,10 +1263,10 @@ export default function PropertyDetailsPage() {
<Home className="w-7 h-7 text-gray-400" />
</div>
<h4 className="font-bold text-gray-700 text-sm mb-1">
هذا عقارك
{t("thisIsYourProperty")}
</h4>
<p className="text-xs text-gray-500">
لا يمكنك حجز عقارك الخاص
{t("cannotBookOwnProperty")}
</p>
</div>
) : bookingSuccess ? (
@ -1352,10 +1275,10 @@ export default function PropertyDetailsPage() {
<Check className="w-7 h-7 text-green-600" />
</div>
<h4 className="font-bold text-green-700 text-sm mb-1">
تم إرسال طلب الحجز
{t("bookingRequestSent")}
</h4>
<p className="text-xs text-gray-500">
سيتم مراجعة طلبك من قبل المالك
{t("bookingRequestWillBeReviewed")}
</p>
</div>
) : (
@ -1368,10 +1291,10 @@ export default function PropertyDetailsPage() {
className={`p-2.5 rounded-xl text-center border-2 transition-all ${effectivePricingMode === "daily" ? "border-amber-500 bg-amber-50" : "border-gray-200 hover:border-gray-300"}`}
>
<div className="text-xs font-bold text-gray-900">
إيجار يومي
{t("dailyRentLabel")}
</div>
<div className="text-sm font-bold text-amber-600">
{formatCurrency(property.priceDisplay.daily)} ل.س
{formatCurrency(property.priceDisplay.daily)} {t("syp")}
</div>
</button>
<button
@ -1379,10 +1302,10 @@ export default function PropertyDetailsPage() {
className={`p-2.5 rounded-xl text-center border-2 transition-all ${effectivePricingMode === "monthly" ? "border-amber-500 bg-amber-50" : "border-gray-200 hover:border-gray-300"}`}
>
<div className="text-xs font-bold text-gray-900">
إيجار شهري
{t("monthlyRentLabel")}
</div>
<div className="text-sm font-bold text-amber-600">
{formatCurrency(property.priceDisplay.monthly)} ل.س
{formatCurrency(property.priceDisplay.monthly)} {t("syp")}
</div>
</button>
</div>
@ -1396,7 +1319,7 @@ export default function PropertyDetailsPage() {
<div
className={`w-1.5 h-1.5 rounded-full ${bookingStep === "entry" ? "bg-amber-500" : "bg-gray-400"}`}
/>
تحديد تاريخ البداية
{t("selectStartDate")}
</div>
<div className="text-gray-300 text-xs"></div>
<div
@ -1405,7 +1328,7 @@ export default function PropertyDetailsPage() {
<div
className={`w-1.5 h-1.5 rounded-full ${bookingStep === "exit" ? "bg-amber-500" : "bg-gray-400"}`}
/>
تحديد تاريخ النهاية
{t("selectEndDate")}
</div>
</div>
@ -1420,7 +1343,7 @@ export default function PropertyDetailsPage() {
<ChevronRight className="w-4 h-4 text-gray-600" />
</button>
<span className="text-sm font-bold text-gray-900">
{MONTHS_AR[calendarMonth]} {calendarYear}
{t(MONTH_KEYS[calendarMonth])} {calendarYear}
</span>
<button
onClick={() => navigateMonth(1)}
@ -1431,12 +1354,12 @@ export default function PropertyDetailsPage() {
</div>
<div className="grid grid-cols-7 mb-1">
{DAYS_AR.map((d, i) => (
{DAY_KEYS.map((dk, i) => (
<div
key={i}
className="text-center text-[10px] text-gray-400 font-medium py-1"
>
{d}
{t(dk)}
</div>
))}
</div>
@ -1509,7 +1432,7 @@ export default function PropertyDetailsPage() {
</button>
</div>
<div className="grid grid-cols-3 gap-2">
{MONTHS_AR.map((name, idx) => {
{MONTH_KEYS.map((mk, idx) => {
const monthStr = `${calendarYear}-${String(idx + 1).padStart(2, "0")}`;
const isSelStart =
selectedStart &&
@ -1548,7 +1471,7 @@ export default function PropertyDetailsPage() {
}}
className={`p-2.5 rounded-xl text-center text-xs font-medium transition-all border ${isSelStart || isSelEnd ? "bg-amber-500 text-white border-amber-500 shadow-sm" : inRange ? "bg-amber-100 text-amber-800 border-amber-200" : "bg-white text-gray-700 border-gray-200 hover:border-amber-300"}`}
>
{name}
{t(mk)}
</button>
);
})}
@ -1560,7 +1483,7 @@ export default function PropertyDetailsPage() {
{selectedStart && (
<div className="bg-gray-50 rounded-xl p-3 mb-3 space-y-1.5">
<div className="flex justify-between text-xs">
<span className="text-gray-500">تاريخ البداية</span>
<span className="text-gray-500">{t("startDateLabel")}</span>
<span className="font-medium text-gray-900">
{selectedStart}
</span>
@ -1569,7 +1492,7 @@ export default function PropertyDetailsPage() {
<>
<div className="flex justify-between text-xs">
<span className="text-gray-500">
تاريخ النهاية
{t("endDateLabel")}
</span>
<span className="font-medium text-gray-900">
{selectedEnd}
@ -1579,8 +1502,8 @@ export default function PropertyDetailsPage() {
<div className="flex justify-between text-xs">
<span className="text-gray-500">
{effectivePricingMode === "daily"
? "عدد الأيام"
: "عدد الأشهر"}
? t("numberOfDays")
: t("numberOfMonths")}
</span>
<span className="font-medium text-gray-900">
{effectivePricingMode === "daily"
@ -1601,7 +1524,7 @@ export default function PropertyDetailsPage() {
</span>
</div>
<div className="flex justify-between text-xs font-bold">
<span className="text-gray-700">المجموع</span>
<span className="text-gray-700">{t("total")}</span>
<span className="text-amber-600">
{formatCurrency(
effectivePricingMode === "daily"
@ -1623,14 +1546,14 @@ export default function PropertyDetailsPage() {
1) *
property.priceDisplay.monthly,
)}{" "}
ل.س
{t("syp")}
</span>
</div>
{property.deposit > 0 && (
<div className="flex justify-between text-xs">
<span className="text-gray-500">تأمين</span>
<span className="text-gray-500">{t("deposit")}</span>
<span className="font-medium text-gray-900">
{formatCurrency(property.deposit)} ل.س
{formatCurrency(property.deposit)} {t("syp")}
</span>
</div>
)}
@ -1657,7 +1580,7 @@ export default function PropertyDetailsPage() {
) : (
<Calendar className="w-4 h-4" />
)}
{bookingLoading ? "جاري الحجز..." : "تأكيد الحجز"}
{bookingLoading ? t("bookingInProgress") : t("confirmBooking")}
</button>
</>
)}
@ -1669,7 +1592,7 @@ export default function PropertyDetailsPage() {
<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">معلومات المالك</h3>
<h3 className="font-bold text-gray-900">{t("ownerInfo")}</h3>
</div>
{showContact && contactInfo ? (
@ -1690,7 +1613,7 @@ export default function PropertyDetailsPage() {
<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>
@ -1712,27 +1635,27 @@ export default function PropertyDetailsPage() {
<div className="w-16 h-16 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-4">
<LogIn className="w-8 h-8 text-amber-600" />
</div>
<h3 className="text-xl font-bold mb-2">تسجيل الدخول مطلوب</h3>
<h3 className="text-xl font-bold mb-2">{t("loginRequiredTitle")}</h3>
<p className="text-gray-500 mb-4">
للحجز أو إضافة المفضلة، سجل دخولك.
{t("loginRequiredDesc")}
</p>
<Link
href="/login"
className="block w-full bg-amber-500 text-white py-3 rounded-xl font-medium mb-2 hover:bg-amber-600"
>
تسجيل الدخول
{t("login")}
</Link>
<Link
href="/auth/choose-role"
className="block w-full bg-gray-100 py-3 rounded-xl font-medium hover:bg-gray-200"
>
إنشاء حساب
{t("createAccount")}
</Link>
<button
onClick={() => setShowLoginDialog(false)}
className="mt-3 text-gray-400 hover:text-gray-600"
>
إلغاء
{t("cancel")}
</button>
</div>
</div>

View File

@ -31,7 +31,7 @@ function mapProperty(item) {
const priceUnit = item.monthlyRent || item.MonthlyRent ? 'monthly' : 'daily';
const buildingType = info.buildingType ?? info.BuildingType ?? 0;
const type = { 0: 'apartment', 1: 'villa', 2: 'house' }[buildingType] || 'apartment';
const typeLabel = { 0: 'شقة', 1: 'فيلا', 2: 'بيت' }[buildingType] || 'عقار';
const typeLabel = { 0: 'apartment', 1: 'villa', 2: 'house' }[buildingType] || 'apartment';
const address = info.address || info.Address || '';
const bedrooms = info.numberOfBedRooms || info.NumberOfBedRooms || 0;
const bathrooms = info.numberOfBathRooms || info.NumberOfBathRooms || 0;

View File

@ -5,6 +5,7 @@ import { motion, AnimatePresence } from 'framer-motion';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import Image from 'next/image';
import { useTranslation } from 'react-i18next';
import {
User, Mail, Phone, Lock, Eye, EyeOff, MessageCircle,
Camera, X, CheckCircle, XCircle, ArrowLeft,
@ -12,16 +13,10 @@ import {
BadgeCheck, Briefcase, Calendar
} from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
import {
registerRealEstateAgent,
loginWithEmail,
sendEmailOTP,
verifyEmail
} from '../../utils/api';
import AuthService from '../../services/AuthService';
export default function AgentRegisterPage() {
const router = useRouter();
const { t } = useTranslation();
const [step, setStep] = useState(1);
const [showOtpModal, setShowOtpModal] = useState(false);
const [showPassword, setShowPassword] = useState(false);
@ -68,12 +63,12 @@ export default function AgentRegisterPage() {
if (!file) return;
if (!file.type.startsWith('image/')) {
toast.error('الرجاء اختيار صورة صالحة');
toast.error(t('register.selectValidImage'));
return;
}
if (file.size > 5 * 1024 * 1024) {
toast.error('حجم الصورة يجب أن يكون أقل من 5 ميجابايت');
toast.error(t('register.imageSizeLimit'));
return;
}
@ -84,7 +79,7 @@ export default function AgentRegisterPage() {
reader.readAsDataURL(file);
setIdImages((prev) => ({ ...prev, [side]: file }));
toast.success('تم رفع الصورة بنجاح', {
toast.success(t('register.imageUploadSuccess'), {
style: { background: '#dcfce7', color: '#166534' }
});
};
@ -92,20 +87,20 @@ export default function AgentRegisterPage() {
const validateStep1 = () => {
const newErrors = {};
if (!formData.firstName) newErrors.firstName = 'الاسم الأول مطلوب';
if (!formData.lastName) newErrors.lastName = 'اسم العائلة مطلوب';
if (!formData.firstName) newErrors.firstName = t('register.firstNameRequired');
if (!formData.lastName) newErrors.lastName = t('register.lastNameRequired');
if (!formData.email) newErrors.email = 'البريد الإلكتروني مطلوب';
else if (!validateEmail(formData.email)) newErrors.email = 'البريد الإلكتروني غير صالح';
if (!formData.email) newErrors.email = t('register.emailRequired');
else if (!validateEmail(formData.email)) newErrors.email = t('register.emailInvalid');
if (!formData.phone) newErrors.phone = 'رقم الهاتف مطلوب';
else if (!validatePhone(formData.phone)) newErrors.phone = 'رقم الهاتف غير صالح (يجب أن يبدأ 09 أو 05)';
if (!formData.phone) newErrors.phone = t('register.phoneRequired');
else if (!validatePhone(formData.phone)) newErrors.phone = t('register.phoneInvalid') + ` (${t('register.phoneMustStartWith')} 09/05)`;
if (!formData.password) newErrors.password = 'كلمة المرور مطلوبة';
else if (formData.password.length < 6) newErrors.password = 'كلمة المرور يجب أن تكون 6 أحرف على الأقل';
if (!formData.password) newErrors.password = t('register.passwordRequired');
else if (formData.password.length < 6) newErrors.password = t('validation.passwordMinLength');
if (!formData.confirmPassword) newErrors.confirmPassword = 'تأكيد كلمة المرور مطلوب';
else if (formData.password !== formData.confirmPassword) newErrors.confirmPassword = 'كلمات المرور غير متطابقة';
if (!formData.confirmPassword) newErrors.confirmPassword = t('register.confirmPasswordRequired');
else if (formData.password !== formData.confirmPassword) newErrors.confirmPassword = t('validation.passwordsMatch');
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
@ -114,13 +109,13 @@ export default function AgentRegisterPage() {
const validateStep2 = () => {
const newErrors = {};
if (!formData.nationalNumber) newErrors.nationalNumber = 'الرقم الوطني مطلوب';
if (!formData.nationalNumber) newErrors.nationalNumber = t('register.nationalNumberRequired');
if (!formData.whatsapp) newErrors.whatsapp = 'رقم الواتساب مطلوب';
else if (!validatePhone(formData.whatsapp)) newErrors.whatsapp = 'رقم الواتساب غير صالح (يجب أن يبدأ 09 أو 05)';
if (!formData.whatsapp) newErrors.whatsapp = t('register.whatsappRequired');
else if (!validatePhone(formData.whatsapp)) newErrors.whatsapp = t('register.phoneInvalid') + ` (${t('register.phoneMustStartWith')} 09/05)`;
if (!idImages.front) newErrors.front = 'صورة الوجه الأمامي للهوية مطلوبة';
if (!idImages.back) newErrors.back = 'صورة الوجه الخلفي للهوية مطلوبة';
if (!idImages.front) newErrors.front = t('register.frontIdRequired');
if (!idImages.back) newErrors.back = t('register.backIdRequired');
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
@ -129,9 +124,9 @@ export default function AgentRegisterPage() {
const validateStep3 = () => {
const newErrors = {};
if (!formData.agencyAddress) newErrors.agencyAddress = 'عنوان الوكالة مطلوب';
if (!formData.licenseNumber) newErrors.licenseNumber = 'رقم الترخيص مطلوب';
if (!idImages.license) newErrors.license = 'صورة الترخيص مطلوبة';
if (!formData.agencyAddress) newErrors.agencyAddress = t('register.agent.agencyAddressRequired');
if (!formData.licenseNumber) newErrors.licenseNumber = t('register.agent.licenseNumberRequired');
if (!idImages.license) newErrors.license = t('register.agent.licenseImageRequired');
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
@ -148,14 +143,13 @@ export default function AgentRegisterPage() {
setStep(4);
window.scrollTo({ top: 0, behavior: 'smooth' });
} else {
toast.error('يرجى تصحيح الأخطاء في النموذج');
toast.error(t('validation.correctErrors'));
}
};
const buildFormData = () => {
const fd = new FormData();
// مطابق للـ API
fd.append('Address', formData.agencyAddress);
fd.append('LicenseNumber', formData.licenseNumber);
@ -169,7 +163,6 @@ export default function AgentRegisterPage() {
fd.append('Owner.WhatsAppNumber', formData.whatsapp);
fd.append('Owner.Language', '0');
// مطابق لأسماء الحقول الظاهرة في swagger
if (idImages.front) fd.append('FrontIdCarImage', idImages.front);
if (idImages.back) fd.append('RearIdCarImage', idImages.back);
if (idImages.license) fd.append('license', idImages.license);
@ -181,21 +174,20 @@ export default function AgentRegisterPage() {
e.preventDefault();
if (!formData.agreeTerms) {
toast.error('يجب الموافقة على الشروط والأحكام');
toast.error(t('validation.agreeToTerms'));
return;
}
setIsLoading(true);
try {
const fd = buildFormData();
const res = await registerRealEstateAgent(fd);
if (res?.ok || res?.status === 200) {
const tempToken = res?.data;
if (tempToken) AuthService.addToken(tempToken);
toast.success(res?.message || 'تم إنشاء الحساب! يرجى التحقق من بريدك الإلكتروني', {
toast.success(res?.message || t('register.accountCreated'), {
duration: 4000
});
@ -205,7 +197,7 @@ export default function AgentRegisterPage() {
const otpToken = loginRes?.data;
if (otpToken) AuthService.addToken(otpToken);
toast(loginRes?.message || 'تم إرسال رمز التحقق إلى بريدك الإلكتروني', {
toast(loginRes?.message || t('register.otpSentEmail'), {
icon: '📧'
});
setShowOtpModal(true);
@ -213,17 +205,17 @@ export default function AgentRegisterPage() {
const loginToken = loginRes?.data;
if (loginToken) AuthService.addToken(loginToken);
toast.success(loginRes?.message || 'تم تسجيل الدخول بنجاح!');
toast.success(loginRes?.message || t('register.loginSuccess'));
router.push('/');
} else {
toast.success('تم إنشاء الحساب بنجاح! يرجى تسجيل الدخول');
toast.success(t('register.accountCreatedLogin'));
setTimeout(() => router.push('/login'), 1500);
}
} else {
toast.error(res?.message || res?.data?.message || 'فشل في إنشاء الحساب');
toast.error(res?.message || res?.data?.message || t('register.accountCreationFailed'));
}
} catch (err) {
toast.error(err?.message || 'حدث خطأ أثناء التسجيل');
toast.error(err?.message || t('register.registrationError'));
} finally {
setIsLoading(false);
}
@ -231,7 +223,7 @@ export default function AgentRegisterPage() {
const handleVerifyOTP = async () => {
if (!otpCode || otpCode.length < 4) {
toast.error('يرجى إدخال رمز التحقق');
toast.error(t('register.otpRequired'));
return;
}
@ -241,14 +233,14 @@ export default function AgentRegisterPage() {
if (res?.status === 200) {
AuthService.deleteToken();
toast.success(res?.message || 'تم التحقق من البريد الإلكتروني بنجاح!');
toast.success(res?.message || t('register.emailVerified'));
setShowOtpModal(false);
setTimeout(() => router.push('/login'), 1500);
} else {
toast.error(res?.message || res?.data?.message || 'رمز التحقق غير صحيح');
toast.error(res?.message || res?.data?.message || t('accountVerification.otpInvalid'));
}
} catch (err) {
toast.error(err?.message || 'حدث خطأ أثناء التحقق');
toast.error(err?.message || t('register.verificationError'));
} finally {
setIsLoading(false);
}
@ -258,9 +250,9 @@ export default function AgentRegisterPage() {
setIsLoading(true);
try {
await sendEmailOTP();
toast.success('تم إرسال رمز تحقق جديد');
toast.success(t('register.newOtpSent'));
} catch (err) {
toast.error('فشل في إرسال الرمز');
toast.error(t('register.resendOtpFailed'));
} finally {
setIsLoading(false);
}
@ -330,17 +322,17 @@ export default function AgentRegisterPage() {
}, []);
const stepTitles = [
'المعلومات الأساسية',
'معلومات الهوية',
'معلومات الوكالة',
'تأكيد التسجيل'
t('register.agent.step1Title'),
t('register.agent.step2Title'),
t('register.agent.step3Title'),
t('register.agent.step4Title')
];
const stepDescriptions = [
'أدخل معلوماتك الأساسية',
'يرجى رفع صور الهوية للتحقق',
'أدخل بيانات الوكالة والترخيص',
'راجع معلوماتك قبل الإرسال'
t('register.agent.step1Desc'),
t('register.agent.step2Desc'),
t('register.agent.step3Desc'),
t('register.agent.step4Desc')
];
const renderStepFields = () => {
@ -351,7 +343,7 @@ export default function AgentRegisterPage() {
<motion.div variants={fadeInUp} className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
الاسم الأول <span className="text-red-500">*</span>
{t('register.firstName')} <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
@ -373,7 +365,7 @@ export default function AgentRegisterPage() {
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.firstName ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="الاسم الأول"
placeholder={t('register.firstName')}
/>
</div>
{errors.firstName && (
@ -383,7 +375,7 @@ export default function AgentRegisterPage() {
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
اسم العائلة <span className="text-red-500">*</span>
{t('register.lastName')} <span className="text-red-500">*</span>
</label>
<input
type="text"
@ -395,7 +387,7 @@ export default function AgentRegisterPage() {
className={`w-full px-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.lastName ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="اسم العائلة"
placeholder={t('register.lastName')}
/>
{errors.lastName && (
<p className="text-red-500 text-sm mt-1">{errors.lastName}</p>
@ -405,7 +397,7 @@ export default function AgentRegisterPage() {
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
البريد الإلكتروني <span className="text-red-500">*</span>
{t('register.email')} <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
@ -427,7 +419,7 @@ export default function AgentRegisterPage() {
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.email ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أدخل بريدك الإلكتروني"
placeholder={t('register.emailPlaceholder')}
/>
</div>
{errors.email && <p className="text-red-500 text-sm mt-1">{errors.email}</p>}
@ -435,7 +427,7 @@ export default function AgentRegisterPage() {
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
رقم الهاتف <span className="text-red-500">*</span>
{t('register.phone')} <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
@ -457,7 +449,7 @@ export default function AgentRegisterPage() {
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.phone ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أدخل رقم هاتفك"
placeholder={t('register.phonePlaceholder')}
/>
</div>
{errors.phone && <p className="text-red-500 text-sm mt-1">{errors.phone}</p>}
@ -465,7 +457,7 @@ export default function AgentRegisterPage() {
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
كلمة المرور <span className="text-red-500">*</span>
{t('register.password')} <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
@ -487,7 +479,7 @@ export default function AgentRegisterPage() {
className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.password ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أدخل كلمة المرور"
placeholder={t('register.passwordPlaceholder')}
/>
<button
type="button"
@ -506,7 +498,7 @@ export default function AgentRegisterPage() {
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
تأكيد كلمة المرور <span className="text-red-500">*</span>
{t('register.confirmPassword')} <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
@ -528,7 +520,7 @@ export default function AgentRegisterPage() {
className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.confirmPassword ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أعد إدخال كلمة المرور"
placeholder={t('register.confirmPasswordPlaceholder')}
/>
<button
type="button"
@ -563,7 +555,7 @@ export default function AgentRegisterPage() {
<>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
الرقم الوطني <span className="text-red-500">*</span>
{t('register.nationalNumber')} <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
@ -585,7 +577,7 @@ export default function AgentRegisterPage() {
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.nationalNumber ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أدخل الرقم الوطني"
placeholder={t('register.nationalNumberPlaceholder')}
/>
</div>
{errors.nationalNumber && (
@ -595,7 +587,7 @@ export default function AgentRegisterPage() {
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
رقم الواتساب <span className="text-red-500">*</span>
{t('register.whatsapp')} <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
@ -617,7 +609,7 @@ export default function AgentRegisterPage() {
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.whatsapp ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أدخل رقم الواتساب"
placeholder={t('register.whatsappPlaceholder')}
/>
</div>
{errors.whatsapp && <p className="text-red-500 text-sm mt-1">{errors.whatsapp}</p>}
@ -625,7 +617,7 @@ export default function AgentRegisterPage() {
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
صورة الهوية - الوجه الأمامي <span className="text-red-500">*</span>
{t('register.agent.frontIdLabel')} <span className="text-red-500">*</span>
</label>
<div
onClick={() => fileInputFrontRef.current?.click()}
@ -649,7 +641,7 @@ export default function AgentRegisterPage() {
<div className="relative">
<Image
src={idImagePreviews.front}
alt="Front ID"
alt={t('register.uploadFrontAlt')}
width={200}
height={120}
className="mx-auto rounded-lg object-cover"
@ -669,8 +661,8 @@ export default function AgentRegisterPage() {
) : (
<>
<Camera className="w-12 h-12 text-gray-500 mx-auto mb-3" />
<p className="text-gray-400">اضغط لرفع الصورة</p>
<p className="text-xs text-gray-500 mt-2">JPEG, PNG, JPG حتى 5MB</p>
<p className="text-gray-400">{t('register.agent.clickUploadImage')}</p>
<p className="text-xs text-gray-500 mt-2">JPEG, PNG, JPG {t('register.upTo5MB')}</p>
</>
)}
</div>
@ -679,7 +671,7 @@ export default function AgentRegisterPage() {
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
صورة الهوية - الوجه الخلفي <span className="text-red-500">*</span>
{t('register.agent.backIdLabel')} <span className="text-red-500">*</span>
</label>
<div
onClick={() => fileInputBackRef.current?.click()}
@ -703,7 +695,7 @@ export default function AgentRegisterPage() {
<div className="relative">
<Image
src={idImagePreviews.back}
alt="Back ID"
alt={t('register.uploadBackAlt')}
width={200}
height={120}
className="mx-auto rounded-lg object-cover"
@ -723,8 +715,8 @@ export default function AgentRegisterPage() {
) : (
<>
<Camera className="w-12 h-12 text-gray-500 mx-auto mb-3" />
<p className="text-gray-400">اضغط لرفع الصورة</p>
<p className="text-xs text-gray-500 mt-2">JPEG, PNG, JPG حتى 5MB</p>
<p className="text-gray-400">{t('register.agent.clickUploadImage')}</p>
<p className="text-xs text-gray-500 mt-2">JPEG, PNG, JPG {t('register.upTo5MB')}</p>
</>
)}
</div>
@ -738,7 +730,7 @@ export default function AgentRegisterPage() {
<>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
عنوان الوكالة <span className="text-red-500">*</span>
{t('register.agent.agencyAddressLabel')} <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
@ -760,7 +752,7 @@ export default function AgentRegisterPage() {
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.agencyAddress ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أدخل عنوان الوكالة"
placeholder={t('register.agent.agencyAddressPlaceholder')}
/>
</div>
{errors.agencyAddress && (
@ -770,7 +762,7 @@ export default function AgentRegisterPage() {
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
رقم الترخيص <span className="text-red-500">*</span>
{t('register.agent.licenseNumberLabel')} <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
@ -792,7 +784,7 @@ export default function AgentRegisterPage() {
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.licenseNumber ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أدخل رقم الترخيص العقاري"
placeholder={t('register.agent.licenseNumberPlaceholder')}
/>
</div>
{errors.licenseNumber && (
@ -802,7 +794,7 @@ export default function AgentRegisterPage() {
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
صورة الترخيص <span className="text-red-500">*</span>
{t('register.agent.licenseImageLabel')} <span className="text-red-500">*</span>
</label>
<div
onClick={() => fileInputLicenseStep3Ref.current?.click()}
@ -826,7 +818,7 @@ export default function AgentRegisterPage() {
<div className="relative">
<Image
src={idImagePreviews.license}
alt="License"
alt={t('register.uploadLicenseAlt')}
width={200}
height={120}
className="mx-auto rounded-lg object-cover"
@ -846,8 +838,8 @@ export default function AgentRegisterPage() {
) : (
<>
<Camera className="w-12 h-12 text-gray-500 mx-auto mb-3" />
<p className="text-gray-400">اضغط لرفع صورة الترخيص</p>
<p className="text-xs text-gray-500 mt-2">JPEG, PNG, JPG حتى 5MB</p>
<p className="text-gray-400">{t('register.agent.clickUploadLicense')}</p>
<p className="text-xs text-gray-500 mt-2">JPEG, PNG, JPG {t('register.upTo5MB')}</p>
</>
)}
</div>
@ -862,23 +854,23 @@ export default function AgentRegisterPage() {
<div className="bg-white/5 rounded-xl p-4 border border-gray-700">
<h3 className="text-lg font-semibold text-white mb-3 flex items-center gap-2">
<User className="w-5 h-5 text-purple-400" />
المعلومات الأساسية
{t('register.agent.reviewBasicInfo')}
</h3>
<div className="grid grid-cols-2 gap-3 text-sm">
<div>
<span className="text-gray-400">الاسم الأول:</span>{' '}
<span className="text-gray-400">{t('register.agent.reviewFirstName')}</span>{' '}
<span className="text-white">{formData.firstName}</span>
</div>
<div>
<span className="text-gray-400">اسم العائلة:</span>{' '}
<span className="text-gray-400">{t('register.agent.reviewLastName')}</span>{' '}
<span className="text-white">{formData.lastName}</span>
</div>
<div>
<span className="text-gray-400">البريد الإلكتروني:</span>{' '}
<span className="text-gray-400">{t('register.email')}:</span>{' '}
<span className="text-white">{formData.email}</span>
</div>
<div>
<span className="text-gray-400">رقم الهاتف:</span>{' '}
<span className="text-gray-400">{t('register.phone')}:</span>{' '}
<span className="text-white">{formData.phone}</span>
</div>
</div>
@ -887,15 +879,15 @@ export default function AgentRegisterPage() {
<div className="bg-white/5 rounded-xl p-4 border border-gray-700">
<h3 className="text-lg font-semibold text-white mb-3 flex items-center gap-2">
<FileText className="w-5 h-5 text-purple-400" />
معلومات الهوية
{t('register.agent.reviewIdInfo')}
</h3>
<div className="grid grid-cols-2 gap-3 text-sm">
<div>
<span className="text-gray-400">الرقم الوطني:</span>{' '}
<span className="text-gray-400">{t('register.nationalNumber')}:</span>{' '}
<span className="text-white">{formData.nationalNumber}</span>
</div>
<div>
<span className="text-gray-400">رقم الواتساب:</span>{' '}
<span className="text-gray-400">{t('register.whatsapp')}:</span>{' '}
<span className="text-white">{formData.whatsapp}</span>
</div>
</div>
@ -905,24 +897,24 @@ export default function AgentRegisterPage() {
<div className="text-center">
<Image
src={idImagePreviews.front}
alt="Front ID"
alt={t('register.uploadFrontAlt')}
width={100}
height={60}
className="rounded-lg object-cover mx-auto"
/>
<p className="text-xs text-gray-400 mt-1">الوجه الأمامي</p>
<p className="text-xs text-gray-400 mt-1">{t('register.agent.reviewFront')}</p>
</div>
)}
{idImagePreviews.back && (
<div className="text-center">
<Image
src={idImagePreviews.back}
alt="Back ID"
alt={t('register.uploadBackAlt')}
width={100}
height={60}
className="rounded-lg object-cover mx-auto"
/>
<p className="text-xs text-gray-400 mt-1">الوجه الخلفي</p>
<p className="text-xs text-gray-400 mt-1">{t('register.agent.reviewBack')}</p>
</div>
)}
</div>
@ -931,15 +923,15 @@ export default function AgentRegisterPage() {
<div className="bg-white/5 rounded-xl p-4 border border-gray-700">
<h3 className="text-lg font-semibold text-white mb-3 flex items-center gap-2">
<Briefcase className="w-5 h-5 text-purple-400" />
معلومات الوكالة
{t('register.agent.reviewAgencyInfo')}
</h3>
<div className="grid grid-cols-2 gap-3 text-sm">
<div>
<span className="text-gray-400">عنوان الوكالة:</span>{' '}
<span className="text-gray-400">{t('register.agent.reviewAgencyAddress')}:</span>{' '}
<span className="text-white">{formData.agencyAddress}</span>
</div>
<div>
<span className="text-gray-400">رقم الترخيص:</span>{' '}
<span className="text-gray-400">{t('register.agent.reviewLicenseNumber')}:</span>{' '}
<span className="text-white">{formData.licenseNumber}</span>
</div>
</div>
@ -948,12 +940,12 @@ export default function AgentRegisterPage() {
<div className="text-center mt-3">
<Image
src={idImagePreviews.license}
alt="License"
alt={t('register.uploadLicenseAlt')}
width={100}
height={60}
className="rounded-lg object-cover mx-auto"
/>
<p className="text-xs text-gray-400 mt-1">صورة الترخيص</p>
<p className="text-xs text-gray-400 mt-1">{t('register.agent.reviewLicense')}</p>
</div>
)}
</div>
@ -968,13 +960,13 @@ export default function AgentRegisterPage() {
required
/>
<label htmlFor="terms" className="text-sm text-gray-300">
أوافق على{' '}
{t('register.agreeTerms')}{' '}
<Link href="/terms" className="text-purple-400 hover:text-purple-300">
شروط الاستخدام
{t('register.termsOfUse')}
</Link>{' '}
و{' '}
{t('register.and')}{' '}
<Link href="/privacy" className="text-purple-400 hover:text-purple-300">
سياسة الخصوصية
{t('register.privacyPolicy')}
</Link>
</label>
</motion.div>
@ -1006,9 +998,9 @@ export default function AgentRegisterPage() {
<motion.div whileHover={{ x: -5 }}>
<ArrowLeft className="w-4 h-4" />
</motion.div>
<span>العودة</span>
<span>{t('forgotPassword.backToLogin')}</span>
</Link>
<span className="text-sm text-gray-400">خطوة {step} من 4</span>
<span className="text-sm text-gray-400">{t('register.stepOf4', { step: step })}</span>
</div>
<div className="flex gap-2">
{[1, 2, 3, 4].map((s) => (
@ -1072,7 +1064,7 @@ export default function AgentRegisterPage() {
}}
className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors"
>
السابق
{t('register.previous')}
</button>
)}
@ -1081,7 +1073,7 @@ export default function AgentRegisterPage() {
type="submit"
className="flex-1 bg-gradient-to-r from-purple-500 to-purple-600 text-white py-3 px-4 rounded-xl font-medium hover:from-purple-600 hover:to-purple-700 transition-all"
>
التالي
{t('register.next')}
</button>
) : (
<button
@ -1092,10 +1084,10 @@ export default function AgentRegisterPage() {
{isLoading ? (
<div className="flex items-center justify-center gap-2">
<Loader2 className="w-5 h-5 animate-spin" />
<span>جاري التسجيل...</span>
<span>{t('register.registering')}</span>
</div>
) : (
'إنشاء حساب وكيل'
t('register.agent.createAccount')
)}
</button>
)}
@ -1123,14 +1115,14 @@ export default function AgentRegisterPage() {
<div className="w-16 h-16 bg-purple-500/20 rounded-full flex items-center justify-center mx-auto mb-3">
<Shield className="w-8 h-8 text-purple-500" />
</div>
<h2 className="text-xl font-bold text-white">التحقق من البريد</h2>
<p className="text-gray-400 text-sm mt-1">تم إرسال رمز التحقق إلى</p>
<h2 className="text-xl font-bold text-white">{t('register.otpTitle')}</h2>
<p className="text-gray-400 text-sm mt-1">{t('register.otpSentTo')}</p>
<p className="text-purple-400 font-medium text-sm">{formData.email}</p>
</div>
<div className="mb-6">
<label className="block text-sm font-medium text-gray-300 mb-2">
رمز التحقق
{t('register.otpLabel')}
</label>
<div className="relative">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
@ -1156,10 +1148,10 @@ export default function AgentRegisterPage() {
{isLoading ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
<span>جاري التحقق...</span>
<span>{t('register.verifying')}</span>
</>
) : (
'تحقق'
t('register.verify')
)}
</button>
</div>
@ -1169,7 +1161,7 @@ export default function AgentRegisterPage() {
disabled={isLoading}
className="w-full text-center text-purple-400 hover:text-purple-300 text-sm mt-3 disabled:opacity-50"
>
إعادة إرسال الرمز
{t('register.resendOtp')}
</button>
</motion.div>
</motion.div>
@ -1177,4 +1169,4 @@ export default function AgentRegisterPage() {
</AnimatePresence>
</div>
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@ import { useState, useEffect, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { useTranslation } from 'react-i18next';
import {
User, Mail, Phone, Lock, Eye, EyeOff,
CheckCircle, XCircle, ArrowLeft, Home, Loader2,
@ -17,7 +18,8 @@ import { CustomerType, CustomerTypeLabels } from '../../enums';
export default function TenantRegisterPage() {
const router = useRouter();
const [step, setStep] = useState(1); // 1=form, 2=agree terms (no images)
const { t } = useTranslation();
const [step, setStep] = useState(1);
const [showOtpModal, setShowOtpModal] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
@ -45,18 +47,18 @@ export default function TenantRegisterPage() {
const validateStep1 = () => {
const newErrors = {};
if (!formData.firstName) newErrors.firstName = 'الاسم الأول مطلوب';
if (!formData.lastName) newErrors.lastName = 'اسم العائلة مطلوب';
if (!formData.email) newErrors.email = 'البريد الإلكتروني مطلوب';
else if (!validateEmail(formData.email)) newErrors.email = 'البريد الإلكتروني غير صالح';
if (!formData.phone) newErrors.phone = 'رقم الهاتف مطلوب';
else if (!validatePhone(formData.phone)) newErrors.phone = 'رقم الهاتف غير صالح (يجب أن يبدأ 09 أو 05)';
if (!formData.password) newErrors.password = 'كلمة المرور مطلوبة';
else if (formData.password.length < 6) newErrors.password = 'كلمة المرور يجب أن تكون 6 أحرف على الأقل';
if (!formData.whatsapp) newErrors.whatsapp = 'رقم الواتساب مطلوب';
if (!formData.phone2 || formData.phone2.length !== 7) newErrors.phone2 = 'رقم الهاتف يجب أن يكون 7 أرقام';
if (!formData.nationalNumber) newErrors.nationalNumber = 'الرقم الوطني مطلوب';
if (formData.password !== formData.confirmPassword) newErrors.confirmPassword = 'كلمات المرور غير متطابقة';
if (!formData.firstName) newErrors.firstName = t('register.firstNameRequired');
if (!formData.lastName) newErrors.lastName = t('register.lastNameRequired');
if (!formData.email) newErrors.email = t('register.emailRequired');
else if (!validateEmail(formData.email)) newErrors.email = t('register.emailInvalid');
if (!formData.phone) newErrors.phone = t('register.phoneRequired');
else if (!validatePhone(formData.phone)) newErrors.phone = t('register.phoneInvalid') + ` (${t('register.phoneMustStartWith')} 09/05)`;
if (!formData.password) newErrors.password = t('register.passwordRequired');
else if (formData.password.length < 6) newErrors.password = t('validation.passwordMinLength');
if (!formData.whatsapp) newErrors.whatsapp = t('register.whatsappRequired');
if (!formData.phone2 || formData.phone2.length !== 7) newErrors.phone2 = t('validation.phone7Digits');
if (!formData.nationalNumber) newErrors.nationalNumber = t('register.nationalNumberRequired');
if (formData.password !== formData.confirmPassword) newErrors.confirmPassword = t('validation.passwordsMatch');
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
@ -66,7 +68,7 @@ export default function TenantRegisterPage() {
setStep(2);
window.scrollTo({ top: 0, behavior: 'smooth' });
} else {
toast.error('يرجى تصحيح الأخطاء في النموذج');
toast.error(t('validation.correctErrors'));
}
};
@ -74,7 +76,7 @@ export default function TenantRegisterPage() {
e.preventDefault();
if (!formData.agreeTerms) {
toast.error('يجب الموافقة على الشروط والأحكام');
toast.error(t('validation.agreeToTerms'));
return;
}
@ -93,7 +95,6 @@ export default function TenantRegisterPage() {
};
try {
// لا توجد صور للمستأجر
const res = await addCustomer(payload, null, null);
if (res.status === 200 || res.ok) {
@ -103,26 +104,26 @@ export default function TenantRegisterPage() {
}
const apiMessage = res.message || res.data?.message;
toast.success(apiMessage || 'تم إنشاء الحساب! يرجى التحقق من بريدك الإلكتروني', { duration: 4000 });
toast.success(apiMessage || t('register.accountCreated'), { duration: 4000 });
const loginRes = await loginWithEmail(formData.email, formData.password);
if (loginRes.status === 206) {
const otpToken = loginRes.data;
if (otpToken) AuthService.addToken(otpToken);
toast(loginRes.message || 'تم إرسال رمز التحقق إلى بريدك الإلكتروني', { icon: '📧' });
toast(loginRes.message || t('register.otpSentEmail'), { icon: '📧' });
setShowOtpModal(true);
} else if (loginRes.status === 200) {
const loginToken = loginRes.data;
if (loginToken) AuthService.addToken(loginToken);
toast.success(loginRes.message || 'تم تسجيل الدخول بنجاح!');
toast.success(loginRes.message || t('register.loginSuccess'));
router.push('/');
}
} else {
const errMsg = res.message || res.data?.message || 'فشل في إنشاء الحساب';
const errMsg = res.message || res.data?.message || t('register.accountCreationFailed');
toast.error(errMsg);
}
} catch (err) {
toast.error(err.message || 'حدث خطأ أثناء التسجيل');
toast.error(err.message || t('register.registrationError'));
} finally {
setIsLoading(false);
}
@ -130,7 +131,7 @@ export default function TenantRegisterPage() {
const handleVerifyOTP = async () => {
if (!otpCode || otpCode.length < 4) {
toast.error('يرجى إدخال رمز التحقق');
toast.error(t('register.otpRequired'));
return;
}
setIsLoading(true);
@ -138,14 +139,14 @@ export default function TenantRegisterPage() {
const res = await verifyEmail(otpCode);
if (res.status === 200) {
AuthService.deleteToken();
toast.success(res.message || 'تم التحقق من البريد الإلكتروني بنجاح!');
toast.success(res.message || t('register.emailVerified'));
setShowOtpModal(false);
setTimeout(() => router.push('/login'), 1500);
} else {
toast.error(res.message || res.data?.message || 'رمز التحقق غير صحيح');
toast.error(res.message || res.data?.message || t('accountVerification.otpInvalid'));
}
} catch (err) {
toast.error(err.message || 'حدث خطأ أثناء التحقق');
toast.error(err.message || t('register.verificationError'));
} finally {
setIsLoading(false);
}
@ -155,9 +156,9 @@ export default function TenantRegisterPage() {
setIsLoading(true);
try {
await sendEmailOTP();
toast.success('تم إرسال رمز تحقق جديد');
toast.success(t('register.newOtpSent'));
} catch (err) {
toast.error('فشل في إرسال الرمز');
toast.error(t('register.resendOtpFailed'));
} finally {
setIsLoading(false);
}
@ -212,7 +213,7 @@ export default function TenantRegisterPage() {
<motion.div initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} className="mb-8">
<Link href="/auth/choose-role" className="flex items-center gap-2 text-gray-400 hover:text-white transition-colors group">
<motion.div whileHover={{ x: -5 }}><ArrowLeft className="w-4 h-4" /></motion.div>
<span>العودة</span>
<span>{t('forgotPassword.backToLogin')}</span>
</Link>
</motion.div>
@ -229,8 +230,8 @@ export default function TenantRegisterPage() {
<motion.div animate={{ rotate: [0, 10, -10, 0] }} transition={{ duration: 2, repeat: Infinity }} className="w-20 h-20 mx-auto mb-4 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-sm">
<Home className="w-10 h-10 text-white" />
</motion.div>
<h1 className="text-3xl font-bold text-white mb-2">{step === 1 ? 'إنشاء حساب مستأجر' : 'الموافقة على الشروط'}</h1>
<p className="text-blue-100">{step === 1 ? 'انضم إلينا وابحث عن منزل أحلامك' : 'يرجى الموافقة على الشروط لإكمال التسجيل'}</p>
<h1 className="text-3xl font-bold text-white mb-2">{step === 1 ? t('register.tenant.step1Title') : t('register.tenant.step2Title')}</h1>
<p className="text-blue-100">{step === 1 ? t('register.tenant.step1Desc') : t('register.tenant.step2Desc')}</p>
</motion.div>
</div>
@ -240,91 +241,91 @@ export default function TenantRegisterPage() {
<>
<motion.div variants={fadeInUp} className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">الاسم الأول <span className="text-red-500">*</span></label>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.firstName')} <span className="text-red-500">*</span></label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<User className={`w-5 h-5 ${errors.firstName ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'}`} />
</div>
<input type="text" value={formData.firstName} onChange={(e) => { setFormData({...formData, firstName: e.target.value}); setErrors({...errors, firstName: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.firstName ? 'border-red-500' : 'border-gray-700'}`} placeholder="الاسم الأول" />
<input type="text" value={formData.firstName} onChange={(e) => { setFormData({...formData, firstName: e.target.value}); setErrors({...errors, firstName: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.firstName ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.firstName')} />
</div>
{errors.firstName && <p className="text-red-500 text-sm mt-1">{errors.firstName}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">اسم العائلة <span className="text-red-500">*</span></label>
<input type="text" value={formData.lastName} onChange={(e) => { setFormData({...formData, lastName: e.target.value}); setErrors({...errors, lastName: null}); }} className={`w-full px-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.lastName ? 'border-red-500' : 'border-gray-700'}`} placeholder="اسم العائلة" />
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.lastName')} <span className="text-red-500">*</span></label>
<input type="text" value={formData.lastName} onChange={(e) => { setFormData({...formData, lastName: e.target.value}); setErrors({...errors, lastName: null}); }} className={`w-full px-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.lastName ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.lastName')} />
{errors.lastName && <p className="text-red-500 text-sm mt-1">{errors.lastName}</p>}
</div>
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">البريد الإلكتروني <span className="text-red-500">*</span></label>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.email')} <span className="text-red-500">*</span></label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Mail className={`w-5 h-5 ${errors.email ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'}`} />
</div>
<input type="email" value={formData.email} onChange={(e) => { setFormData({...formData, email: e.target.value}); setErrors({...errors, email: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.email ? 'border-red-500' : 'border-gray-700'}`} placeholder="أدخل بريدك الإلكتروني" />
<input type="email" value={formData.email} onChange={(e) => { setFormData({...formData, email: e.target.value}); setErrors({...errors, email: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.email ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.emailPlaceholder')} />
</div>
{errors.email && <p className="text-red-500 text-sm mt-1">{errors.email}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">رقم الهاتف <span className="text-red-500">*</span></label>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.phone')} <span className="text-red-500">*</span></label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Phone className={`w-5 h-5 ${errors.phone ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'}`} />
</div>
<input type="tel" value={formData.phone} onChange={(e) => { setFormData({...formData, phone: e.target.value}); setErrors({...errors, phone: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.phone ? 'border-red-500' : 'border-gray-700'}`} placeholder="أدخل رقم هاتفك" />
<input type="tel" value={formData.phone} onChange={(e) => { setFormData({...formData, phone: e.target.value}); setErrors({...errors, phone: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.phone ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.phonePlaceholder')} />
</div>
{errors.phone && <p className="text-red-500 text-sm mt-1">{errors.phone}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">رقم الواتساب <span className="text-red-500">*</span></label>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.whatsapp')} <span className="text-red-500">*</span></label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Phone className={`w-5 h-5 ${errors.whatsapp ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'}`} />
</div>
<input type="tel" value={formData.whatsapp} onChange={(e) => { setFormData({...formData, whatsapp: e.target.value}); setErrors({...errors, whatsapp: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.whatsapp ? 'border-red-500' : 'border-gray-700'}`} placeholder="أدخل رقم الواتساب" />
<input type="tel" value={formData.whatsapp} onChange={(e) => { setFormData({...formData, whatsapp: e.target.value}); setErrors({...errors, whatsapp: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.whatsapp ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.whatsappPlaceholder')} />
</div>
{errors.whatsapp && <p className="text-red-500 text-sm mt-1">{errors.whatsapp}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">رقم الهاتف (7 أرقام) <span className="text-red-500">*</span></label>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.phoneSecondary')} <span className="text-red-500">*</span></label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Phone className={`w-5 h-5 ${errors.phone2 ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'}`} />
</div>
<input type="tel" value={formData.phone2} onChange={(e) => { setFormData({...formData, phone2: e.target.value}); setErrors({...errors, phone2: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.phone2 ? 'border-red-500' : 'border-gray-700'}`} placeholder="أدخل رقم الهاتف" maxLength={7} />
<input type="tel" value={formData.phone2} onChange={(e) => { setFormData({...formData, phone2: e.target.value}); setErrors({...errors, phone2: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.phone2 ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.phoneSecondaryPlaceholder')} maxLength={7} />
</div>
{errors.phone2 && <p className="text-red-500 text-sm mt-1">{errors.phone2}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">الرقم الوطني <span className="text-red-500">*</span></label>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.nationalNumber')} <span className="text-red-500">*</span></label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<User className={`w-5 h-5 ${errors.nationalNumber ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'}`} />
</div>
<input type="text" value={formData.nationalNumber} onChange={(e) => { setFormData({...formData, nationalNumber: e.target.value}); setErrors({...errors, nationalNumber: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.nationalNumber ? 'border-red-500' : 'border-gray-700'}`} placeholder="أدخل الرقم الوطني" />
<input type="text" value={formData.nationalNumber} onChange={(e) => { setFormData({...formData, nationalNumber: e.target.value}); setErrors({...errors, nationalNumber: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.nationalNumber ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.nationalNumberPlaceholder')} />
</div>
{errors.nationalNumber && <p className="text-red-500 text-sm mt-1">{errors.nationalNumber}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">نوع العميل <span className="text-red-500">*</span></label>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.customerType')} <span className="text-red-500">*</span></label>
<select value={formData.customerType} onChange={(e) => setFormData({...formData, customerType: e.target.value})} className="w-full py-3 px-4 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white appearance-none cursor-pointer">
{Object.entries(CustomerTypeLabels).map(([value, label]) => (<option key={value} value={value} className="bg-gray-900 text-white">{label}</option>))}
</select>
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">كلمة المرور <span className="text-red-500">*</span></label>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.password')} <span className="text-red-500">*</span></label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className={`w-5 h-5 ${errors.password ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'}`} />
</div>
<input type={showPassword ? "text" : "password"} value={formData.password} onChange={(e) => { setFormData({...formData, password: e.target.value}); setErrors({...errors, password: null}); }} className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.password ? 'border-red-500' : 'border-gray-700'}`} placeholder="أدخل كلمة المرور" />
<input type={showPassword ? "text" : "password"} value={formData.password} onChange={(e) => { setFormData({...formData, password: e.target.value}); setErrors({...errors, password: null}); }} className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.password ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.passwordPlaceholder')} />
<button type="button" onClick={() => setShowPassword(!showPassword)} className="absolute inset-y-0 left-0 pl-3 flex items-center">
{showPassword ? <EyeOff className="w-5 h-5 text-gray-400" /> : <Eye className="w-5 h-5 text-gray-400" />}
</button>
@ -333,12 +334,12 @@ export default function TenantRegisterPage() {
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">تأكيد كلمة المرور <span className="text-red-500">*</span></label>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.confirmPassword')} <span className="text-red-500">*</span></label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className={`w-5 h-5 ${errors.confirmPassword ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'}`} />
</div>
<input type={showConfirmPassword ? "text" : "password"} value={formData.confirmPassword} onChange={(e) => { setFormData({...formData, confirmPassword: e.target.value}); setErrors({...errors, confirmPassword: null}); }} className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.confirmPassword ? 'border-red-500' : 'border-gray-700'}`} placeholder="أعد إدخال كلمة المرور" />
<input type={showConfirmPassword ? "text" : "password"} value={formData.confirmPassword} onChange={(e) => { setFormData({...formData, confirmPassword: e.target.value}); setErrors({...errors, confirmPassword: null}); }} className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.confirmPassword ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.confirmPasswordPlaceholder')} />
<button type="button" onClick={() => setShowConfirmPassword(!showConfirmPassword)} className="absolute inset-y-0 left-0 pl-3 flex items-center">
{showConfirmPassword ? <EyeOff className="w-5 h-5 text-gray-400" /> : <Eye className="w-5 h-5 text-gray-400" />}
</button>
@ -354,7 +355,7 @@ export default function TenantRegisterPage() {
<motion.div variants={fadeInUp} className="flex items-center gap-2">
<input type="checkbox" id="terms" checked={formData.agreeTerms} onChange={(e) => setFormData({...formData, agreeTerms: e.target.checked})} className="w-4 h-4 rounded border-gray-600 bg-white/5 text-blue-500 focus:ring-blue-500" required />
<label htmlFor="terms" className="text-sm text-gray-300">
أوافق على <Link href="/terms" className="text-blue-400 hover:text-blue-300">شروط الاستخدام</Link> و <Link href="/privacy" className="text-blue-400 hover:text-blue-300">سياسة الخصوصية</Link>
{t('register.agreeTerms')} <Link href="/terms" className="text-blue-400 hover:text-blue-300">{t('register.termsOfUse')}</Link> {t('register.and')} <Link href="/privacy" className="text-blue-400 hover:text-blue-300">{t('register.privacyPolicy')}</Link>
</label>
</motion.div>
</>
@ -363,14 +364,14 @@ export default function TenantRegisterPage() {
<motion.div variants={fadeInUp} className="flex gap-3 pt-4">
{step === 1 ? (
<>
<button type="button" onClick={() => router.push('/auth/choose-role')} className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors">إلغاء</button>
<button type="submit" className="flex-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white py-3 px-4 rounded-xl font-medium hover:from-blue-600 hover:to-blue-700 transition-all">التالي</button>
<button type="button" onClick={() => router.push('/auth/choose-role')} className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors">{t('register.cancel')}</button>
<button type="submit" className="flex-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white py-3 px-4 rounded-xl font-medium hover:from-blue-600 hover:to-blue-700 transition-all">{t('register.next')}</button>
</>
) : (
<>
<button type="button" onClick={() => setStep(1)} className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors">السابق</button>
<button type="button" onClick={() => setStep(1)} className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors">{t('register.previous')}</button>
<button type="submit" disabled={isLoading || !formData.agreeTerms} className="flex-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white py-3 px-4 rounded-xl font-medium hover:from-blue-600 hover:to-blue-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed">
{isLoading ? (<div className="flex items-center justify-center gap-2"><Loader2 className="w-5 h-5 animate-spin" /><span>جاري التسجيل...</span></div>) : 'إنشاء حساب'}
{isLoading ? (<div className="flex items-center justify-center gap-2"><Loader2 className="w-5 h-5 animate-spin" /><span>{t('register.registering')}</span></div>) : t('register.createAccount')}
</button>
</>
)}
@ -378,8 +379,7 @@ export default function TenantRegisterPage() {
{step === 1 && (
<motion.p variants={fadeInUp} className="text-center text-gray-400 mt-4">
لديك حساب بالفعل؟{' '}
<Link href="/login" className="text-blue-400 hover:text-blue-300 font-medium transition-colors">تسجيل الدخول</Link>
{t('register.haveAccount')} <Link href="/login" className="text-blue-400 hover:text-blue-300 font-medium transition-colors">{t('register.loginLink')}</Link>
</motion.p>
)}
</motion.form>
@ -393,12 +393,12 @@ export default function TenantRegisterPage() {
<motion.div initial={{ scale: 0.9, y: 20 }} animate={{ scale: 1, y: 0 }} exit={{ scale: 0.9, y: 20 }} className="bg-gray-900 border border-white/10 rounded-2xl w-full max-w-md p-6 shadow-2xl">
<div className="text-center mb-6">
<div className="w-16 h-16 bg-blue-500/20 rounded-full flex items-center justify-center mx-auto mb-3"><Shield className="w-8 h-8 text-blue-500" /></div>
<h2 className="text-xl font-bold text-white">التحقق من البريد</h2>
<p className="text-gray-400 text-sm mt-1">تم إرسال رمز التحقق إلى</p>
<h2 className="text-xl font-bold text-white">{t('register.otpTitle')}</h2>
<p className="text-gray-400 text-sm mt-1">{t('register.otpSentTo')}</p>
<p className="text-blue-400 font-medium text-sm">{formData.email}</p>
</div>
<div className="mb-6">
<label className="block text-sm font-medium text-gray-300 mb-2">رمز التحقق</label>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.otpLabel')}</label>
<div className="relative">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"><KeyRound className="w-5 h-5 text-gray-400" /></div>
<input type="text" value={otpCode} maxLength={6} onChange={(e) => setOtpCode(e.target.value)} className="w-full pr-12 pl-4 py-3 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 text-white text-center tracking-[0.5em] text-xl" placeholder="------" />
@ -406,14 +406,14 @@ export default function TenantRegisterPage() {
</div>
<div className="flex gap-3">
<button onClick={handleVerifyOTP} disabled={isLoading || !otpCode} className="flex-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white py-3 rounded-xl font-medium hover:from-blue-600 hover:to-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2">
{isLoading ? <><Loader2 className="w-5 h-5 animate-spin" /><span>جاري التحقق...</span></> : 'تحقق'}
{isLoading ? <><Loader2 className="w-5 h-5 animate-spin" /><span>{t('register.verifying')}</span></> : t('register.verify')}
</button>
</div>
<button onClick={handleResendOTP} disabled={isLoading} className="w-full text-center text-blue-400 hover:text-blue-300 text-sm mt-3 disabled:opacity-50">إعادة إرسال الرمز</button>
<button onClick={handleResendOTP} disabled={isLoading} className="w-full text-center text-blue-400 hover:text-blue-300 text-sm mt-3 disabled:opacity-50">{t('register.resendOtp')}</button>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
}

View File

@ -3,6 +3,7 @@
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useRouter } from 'next/navigation';
import { useTranslation } from 'react-i18next';
import toast, { Toaster } from 'react-hot-toast';
import {
FileText,
@ -19,13 +20,8 @@ import {
import { submitReport, submitReservationReport, submitSaleReport } from '../utils/api';
import AuthService from '../services/AuthService';
const tabs = [
{ id: 'general', label: 'تقرير عام', icon: FileText },
{ id: 'reservation', label: 'تقرير حجز', icon: Calendar },
{ id: 'sale', label: 'تقرير بيع', icon: DollarSign },
];
export default function ReportsPage() {
const { t } = useTranslation();
const router = useRouter();
const [activeTab, setActiveTab] = useState('general');
const [isLoading, setIsLoading] = useState(false);
@ -37,6 +33,12 @@ export default function ReportsPage() {
}
}, [router]);
const tabs = [
{ id: 'general', label: t('reports.generalReport'), icon: FileText },
{ id: 'reservation', label: t('reports.reservationReport'), icon: Calendar },
{ id: 'sale', label: t('reports.saleReport'), icon: DollarSign },
];
const [generalForm, setGeneralForm] = useState({ subject: '', body: '' });
const [reservationForm, setReservationForm] = useState({ reservationId: '', message: '', reporter: 'customer' });
const [saleForm, setSaleForm] = useState({ saleId: '', message: '', reporter: 'buyer' });
@ -45,24 +47,24 @@ export default function ReportsPage() {
const validateGeneral = () => {
const e = {};
if (!generalForm.subject.trim()) e.subject = 'الموضوع مطلوب';
if (!generalForm.body.trim()) e.body = 'الوصف مطلوب';
if (!generalForm.subject.trim()) e.subject = t('reports.subjectRequired');
if (!generalForm.body.trim()) e.body = t('reports.descriptionRequired');
setErrors(e);
return Object.keys(e).length === 0;
};
const validateReservation = () => {
const e = {};
if (!reservationForm.reservationId.trim()) e.reservationId = 'رقم الحجز مطلوب';
if (!reservationForm.message.trim()) e.message = 'الرسالة مطلوبة';
if (!reservationForm.reservationId.trim()) e.reservationId = t('reports.reservationIdRequired');
if (!reservationForm.message.trim()) e.message = t('reports.messageRequired');
setErrors(e);
return Object.keys(e).length === 0;
};
const validateSale = () => {
const e = {};
if (!saleForm.saleId.trim()) e.saleId = 'رقم البيع مطلوب';
if (!saleForm.message.trim()) e.message = 'الرسالة مطلوبة';
if (!saleForm.saleId.trim()) e.saleId = t('reports.saleIdRequired');
if (!saleForm.message.trim()) e.message = t('reports.messageRequired');
setErrors(e);
return Object.keys(e).length === 0;
};
@ -74,13 +76,13 @@ export default function ReportsPage() {
try {
await submitReport(generalForm.subject, generalForm.body);
setIsSuccess(true);
toast.success('تم إرسال التقرير بنجاح!', {
toast.success(t('reports.generalSuccess'), {
style: { background: '#dcfce7', color: '#166534' },
});
setGeneralForm({ subject: '', body: '' });
setTimeout(() => setIsSuccess(false), 2000);
} catch (err) {
toast.error(err.message || 'فشل إرسال التقرير', {
toast.error(err.message || t('reports.generalFailed'), {
style: { background: '#fee2e2', color: '#991b1b' },
});
} finally {
@ -95,13 +97,13 @@ export default function ReportsPage() {
try {
await submitReservationReport(reservationForm);
setIsSuccess(true);
toast.success('تم إرسال تقرير الحجز!', {
toast.success(t('reports.reservationSuccess'), {
style: { background: '#dcfce7', color: '#166534' },
});
setReservationForm({ reservationId: '', message: '', reporter: 'customer' });
setTimeout(() => setIsSuccess(false), 2000);
} catch (err) {
toast.error(err.message || 'فشل إرسال التقرير', {
toast.error(err.message || t('reports.reservationFailed'), {
style: { background: '#fee2e2', color: '#991b1b' },
});
} finally {
@ -116,13 +118,13 @@ export default function ReportsPage() {
try {
await submitSaleReport(saleForm);
setIsSuccess(true);
toast.success('تم إرسال تقرير البيع!', {
toast.success(t('reports.saleSuccess'), {
style: { background: '#dcfce7', color: '#166534' },
});
setSaleForm({ saleId: '', message: '', reporter: 'buyer' });
setTimeout(() => setIsSuccess(false), 2000);
} catch (err) {
toast.error(err.message || 'فشل إرسال التقرير', {
toast.error(err.message || t('reports.saleFailed'), {
style: { background: '#fee2e2', color: '#991b1b' },
});
} finally {
@ -134,7 +136,7 @@ export default function ReportsPage() {
general: (
<form onSubmit={handleGeneralSubmit} className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">الموضوع</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('reports.subjectLabel')}</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<FileText className={`w-5 h-5 ${errors.subject ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
@ -144,19 +146,19 @@ export default function ReportsPage() {
value={generalForm.subject}
onChange={(e) => { setGeneralForm({ ...generalForm, subject: e.target.value }); if (errors.subject) setErrors({ ...errors, subject: null }); }}
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all ${errors.subject ? 'border-red-500' : 'border-gray-300'}`}
placeholder="أدخل موضوع التقرير"
placeholder={t('reports.subjectPlaceholder')}
/>
</div>
{errors.subject && <p className="text-red-500 text-sm mt-1">{errors.subject}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">الوصف</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('reports.descriptionLabel')}</label>
<textarea
value={generalForm.body}
onChange={(e) => { setGeneralForm({ ...generalForm, body: e.target.value }); if (errors.body) setErrors({ ...errors, body: null }); }}
rows={5}
className={`w-full px-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all resize-none ${errors.body ? 'border-red-500' : 'border-gray-300'}`}
placeholder="اشرح التفاصيل..."
placeholder={t('reports.descriptionPlaceholder')}
/>
{errors.body && <p className="text-red-500 text-sm mt-1">{errors.body}</p>}
</div>
@ -168,11 +170,11 @@ export default function ReportsPage() {
whileTap={{ scale: 0.98 }}
>
{isLoading ? (
<><Loader2 className="w-5 h-5 animate-spin" /> جاري الإرسال...</>
<><Loader2 className="w-5 h-5 animate-spin" /> {t('reports.sending')}</>
) : isSuccess ? (
<><CheckCircle className="w-5 h-5" /> تم الإرسال!</>
<><CheckCircle className="w-5 h-5" /> {t('reports.sentSuccess')}</>
) : (
<><Send className="w-5 h-5" /> إرسال التقرير</>
<><Send className="w-5 h-5" /> {t('reports.submitReport')}</>
)}
</motion.button>
</form>
@ -180,7 +182,7 @@ export default function ReportsPage() {
reservation: (
<form onSubmit={handleReservationSubmit} className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">رقم الحجز</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('reports.reservationIdLabel')}</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Hash className={`w-5 h-5 ${errors.reservationId ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
@ -190,24 +192,24 @@ export default function ReportsPage() {
value={reservationForm.reservationId}
onChange={(e) => { setReservationForm({ ...reservationForm, reservationId: e.target.value }); if (errors.reservationId) setErrors({ ...errors, reservationId: null }); }}
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all ${errors.reservationId ? 'border-red-500' : 'border-gray-300'}`}
placeholder="أدخل رقم الحجز"
placeholder={t('reports.reservationIdPlaceholder')}
/>
</div>
{errors.reservationId && <p className="text-red-500 text-sm mt-1">{errors.reservationId}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">الرسالة</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('message')}</label>
<textarea
value={reservationForm.message}
onChange={(e) => { setReservationForm({ ...reservationForm, message: e.target.value }); if (errors.message) setErrors({ ...errors, message: null }); }}
rows={4}
className={`w-full px-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all resize-none ${errors.message ? 'border-red-500' : 'border-gray-300'}`}
placeholder="اشرح المشكلة..."
placeholder={t('reports.messagePlaceholder')}
/>
{errors.message && <p className="text-red-500 text-sm mt-1">{errors.message}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">المبلغ</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('reports.reporterLabel')}</label>
<div className="flex gap-3">
{['customer', 'owner'].map((val) => (
<button
@ -221,7 +223,7 @@ export default function ReportsPage() {
}`}
>
<User className="w-4 h-4" />
{val === 'customer' ? 'مستأجر' : 'مالك'}
{val === 'customer' ? t('customer') : t('owner')}
</button>
))}
</div>
@ -234,11 +236,11 @@ export default function ReportsPage() {
whileTap={{ scale: 0.98 }}
>
{isLoading ? (
<><Loader2 className="w-5 h-5 animate-spin" /> جاري الإرسال...</>
<><Loader2 className="w-5 h-5 animate-spin" /> {t('reports.sending')}</>
) : isSuccess ? (
<><CheckCircle className="w-5 h-5" /> تم الإرسال!</>
<><CheckCircle className="w-5 h-5" /> {t('reports.sentSuccess')}</>
) : (
<><Send className="w-5 h-5" /> إرسال التقرير</>
<><Send className="w-5 h-5" /> {t('reports.submitReport')}</>
)}
</motion.button>
</form>
@ -246,7 +248,7 @@ export default function ReportsPage() {
sale: (
<form onSubmit={handleSaleSubmit} className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">رقم البيع</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('reports.saleIdLabel')}</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Hash className={`w-5 h-5 ${errors.saleId ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
@ -256,24 +258,24 @@ export default function ReportsPage() {
value={saleForm.saleId}
onChange={(e) => { setSaleForm({ ...saleForm, saleId: e.target.value }); if (errors.saleId) setErrors({ ...errors, saleId: null }); }}
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all ${errors.saleId ? 'border-red-500' : 'border-gray-300'}`}
placeholder="أدخل رقم البيع"
placeholder={t('reports.saleIdPlaceholder')}
/>
</div>
{errors.saleId && <p className="text-red-500 text-sm mt-1">{errors.saleId}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">الرسالة</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('message')}</label>
<textarea
value={saleForm.message}
onChange={(e) => { setSaleForm({ ...saleForm, message: e.target.value }); if (errors.message) setErrors({ ...errors, message: null }); }}
rows={4}
className={`w-full px-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all resize-none ${errors.message ? 'border-red-500' : 'border-gray-300'}`}
placeholder="اشرح المشكلة..."
placeholder={t('reports.messagePlaceholder')}
/>
{errors.message && <p className="text-red-500 text-sm mt-1">{errors.message}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">المبلغ</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('reports.reporterLabel')}</label>
<div className="flex gap-3">
{['buyer', 'seller'].map((val) => (
<button
@ -287,7 +289,7 @@ export default function ReportsPage() {
}`}
>
<User className="w-4 h-4" />
{val === 'buyer' ? 'مشتري' : 'بائع'}
{val === 'buyer' ? t('buyer') : t('seller')}
</button>
))}
</div>
@ -300,11 +302,11 @@ export default function ReportsPage() {
whileTap={{ scale: 0.98 }}
>
{isLoading ? (
<><Loader2 className="w-5 h-5 animate-spin" /> جاري الإرسال...</>
<><Loader2 className="w-5 h-5 animate-spin" /> {t('reports.sending')}</>
) : isSuccess ? (
<><CheckCircle className="w-5 h-5" /> تم الإرسال!</>
<><CheckCircle className="w-5 h-5" /> {t('reports.sentSuccess')}</>
) : (
<><Send className="w-5 h-5" /> إرسال التقرير</>
<><Send className="w-5 h-5" /> {t('reports.submitReport')}</>
)}
</motion.button>
</form>
@ -320,8 +322,8 @@ export default function ReportsPage() {
animate={{ opacity: 1, y: 0 }}
className="mb-8"
>
<h1 className="text-3xl font-bold text-gray-900 mb-2">الإبلاغ عن مشكلة</h1>
<p className="text-gray-600">أخبرنا عن المشكلة التي تواجهها وسنقوم بمعالجتها</p>
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t('reports.pageTitle')}</h1>
<p className="text-gray-600">{t('reports.pageDescription')}</p>
</motion.div>
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">

View File

@ -396,6 +396,7 @@ import { useState, useEffect, useCallback } from 'react';
import Link from 'next/link';
import { motion } from 'framer-motion';
import { useRouter } from 'next/navigation';
import { useTranslation } from 'react-i18next';
import {
Calendar,
Clock,
@ -432,50 +433,49 @@ const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io
const STATUS_MAP = ['pending','ownerConfirmed','depositPaid','depositConfirmed','completed','cancelled'];
const STATUS_UI = {
pending: { label: 'قيد الانتظار', color: 'bg-yellow-100 text-yellow-800', icon: Clock },
ownerConfirmed: { label: 'مؤكد من المالك', color: 'bg-blue-100 text-blue-800', icon: CheckCircle },
depositPaid: { label: 'تم دفع السلفة', color: 'bg-indigo-100 text-indigo-800', icon: DollarSign },
depositConfirmed: { label: 'مؤكد', color: 'bg-green-100 text-green-800', icon: CheckCircle },
completed: { label: 'منتهي', color: 'bg-green-100 text-green-800', icon: CheckCircle },
cancelled: { label: 'ملغي', color: 'bg-gray-100 text-gray-800', icon: XCircle },
pending: { color: 'bg-yellow-100 text-yellow-800', icon: Clock },
ownerConfirmed: { color: 'bg-blue-100 text-blue-800', icon: CheckCircle },
depositPaid: { color: 'bg-indigo-100 text-indigo-800', icon: DollarSign },
depositConfirmed: { color: 'bg-green-100 text-green-800', icon: CheckCircle },
completed: { color: 'bg-green-100 text-green-800', icon: CheckCircle },
cancelled: { color: 'bg-gray-100 text-gray-800', icon: XCircle },
};
const PAYMENT_METHODS = [
{
id: 'cash',
label: 'الدفع النقدي',
desc: 'ادفع الآن عبر شام كاش مع تأكيد تلقائي للحجز',
labelKey: 'payment.cash',
descKey: 'payment.cashDesc',
icon: Banknote,
active: true,
},
{
id: 'office',
label: 'نقداً',
desc: 'ادفع في مكتب المنصة. يتم التأكيد لاحقاً',
labelKey: 'payment.office',
descKey: 'payment.officeDesc',
icon: Building,
active: true,
},
{
id: 'transfer',
label: 'تحويل داخلي',
desc: 'حول من حساب داخلي ثم أرسل المرجع',
labelKey: 'payment.transfer',
descKey: 'payment.transferDesc',
icon: ArrowLeftRight,
active: false,
},
{
id: 'electronic',
label: 'دفع إلكتروني',
desc: 'سيتم تفعيل الدفع الإلكتروني والتحويل لاحقاً',
labelKey: 'payment.electronic',
descKey: 'payment.electronicDesc',
icon: CreditCard,
active: false,
},
];
function statusLabel(code) { return STATUS_UI[STATUS_MAP[code]]?.label ?? String(code); }
function statusColor(code) { return STATUS_UI[STATUS_MAP[code]]?.color ?? 'bg-gray-100 text-gray-700'; }
function statusIcon(code) { return STATUS_UI[STATUS_MAP[code]]?.icon ?? Clock; }
function formatCurrency(v, sign = 'ل.س') {
function formatCurrency(v, sign = '') {
return `${sign} ${Number(v ?? 0).toLocaleString()}`;
}
@ -487,10 +487,12 @@ function formatDate(date) {
}
function StatusBadge({ code }) {
const { t } = useTranslation();
const Icon = statusIcon(code);
const key = STATUS_MAP[code] || 'pending';
return (
<span className={`inline-flex items-center gap-1 px-2 py-1 rounded-lg text-xs font-medium ${statusColor(code)}`}>
<Icon className="w-3 h-3" /> {statusLabel(code)}
<Icon className="w-3 h-3" /> {t(`bookingStatus.${key}`)}
</span>
);
}
@ -563,10 +565,10 @@ async function reportReservation(reservationId, message) {
const rid = Number(reservationId);
if (!Number.isInteger(rid)) {
throw new Error('رقم الحجز غير صالح');
throw new Error('Invalid reservation ID');
}
if (!Number.isInteger(reporter)) {
throw new Error('تعذر تحديد المستخدم الحالي');
throw new Error('Could not identify current user');
}
const token = getAuthToken();
@ -584,7 +586,7 @@ async function reportReservation(reservationId, message) {
});
if (!res.ok) {
let errorMessage = 'فشل إرسال البلاغ';
let errorMessage = 'Failed to submit report';
try {
const data = await res.json();
errorMessage = data?.message || data?.title || errorMessage;
@ -640,6 +642,7 @@ function formatWindowDuration(str) {
}
function CountdownTimer({ deadline }) {
const { t } = useTranslation();
const [remaining, setRemaining] = useState(deadline ? Math.max(0, deadline - Date.now()) : 0);
useEffect(() => {
if (!deadline) return;
@ -648,7 +651,7 @@ function CountdownTimer({ deadline }) {
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, [deadline]);
if (remaining <= 0) return <span className="text-red-500 text-sm font-medium">انتهت المهلة</span>;
if (remaining <= 0) return <span className="text-red-500 text-sm font-medium">{t('timeExpired')}</span>;
const h = Math.floor(remaining / 3600000);
const m = Math.floor((remaining % 3600000) / 60000);
const s = Math.floor((remaining % 60000) / 1000);
@ -665,6 +668,7 @@ function PaymentDialog({
onConfirmPay,
payingId,
}) {
const { t } = useTranslation();
const [showCashDialog, setShowCashDialog] = useState(false);
useEffect(() => {
@ -674,7 +678,7 @@ function PaymentDialog({
if (!isOpen || !reservation) return null;
const amount = reservation.depositAmount || reservation.totalPrice || 0;
const currencySign = reservation.currencySign || 'ل.س';
const currencySign = reservation.currencySign || t('sypSymbol');
return (
<>
@ -720,12 +724,12 @@ function PaymentDialog({
<div className="px-6 py-5 border-b border-gray-100">
<div className="text-center">
<p className="text-sm text-gray-500 mb-1">مبلغ التأمين (الرعبون)</p>
<p className="text-sm text-gray-500 mb-1">{t('depositAmount')}</p>
<p className="text-4xl font-bold text-amber-600">
{formatCurrency(amount, currencySign)}
</p>
<p className="text-xs text-gray-400 mt-2">
يتم دفع التأمين إلى المنصة وليس إلى المالك
{t('depositPaidToPlatform')}
</p>
</div>
</div>
@ -734,13 +738,13 @@ function PaymentDialog({
<div className="flex items-start gap-3">
<Info className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" />
<p className="text-sm text-amber-800 leading-relaxed">
الدفع النقدي فقط هو المتاح. سيتم تفعيل الدفع الإلكتروني والتحويل لاحقاً.
{t('cashOnlyNotice')}
</p>
</div>
</div>
<div className="px-6 py-5 border-b border-gray-100">
<p className="text-sm font-bold text-gray-700 mb-3">طريقة الدفع</p>
<p className="text-sm font-bold text-gray-700 mb-3">{t('paymentMethod')}</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{PAYMENT_METHODS.map((method) => {
const isSelected = selectedPayment === method.id;
@ -767,14 +771,14 @@ function PaymentDialog({
</div>
<div className="min-w-0 flex-1">
<p className={`text-sm font-bold ${isSelected ? 'text-amber-900' : 'text-gray-800'}`}>
{method.label}
{t(method.labelKey)}
</p>
<p className="text-xs text-gray-500 mt-0.5 leading-relaxed">
{method.desc}
{t(method.descKey)}
</p>
{!method.active && (
<span className="inline-block mt-1 text-[10px] font-medium text-gray-400 bg-gray-200 px-2 py-0.5 rounded-full">
غير متاح
{t('notAvailable')}
</span>
)}
</div>
@ -801,12 +805,12 @@ function PaymentDialog({
{payingId === reservation.id ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
جاري الدفع...
{t('processingPayment')}
</>
) : (
<>
<Banknote className="w-5 h-5" />
دفع التأمين ({formatCurrency(amount, currencySign)})
{t('payDeposit', { amount: formatCurrency(amount, currencySign) })}
</>
)}
</motion.button>
@ -819,7 +823,7 @@ function PaymentDialog({
className="w-full bg-white border-2 border-amber-200 hover:border-amber-400 text-amber-700 font-bold py-3 px-6 rounded-2xl text-base transition-all flex items-center justify-center gap-2"
>
<Building className="w-5 h-5" />
سأدفع نقداً
{t('payCash')}
</button>
</div>
@ -827,9 +831,9 @@ function PaymentDialog({
<div className="flex items-start gap-3">
<Landmark className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" />
<div>
<p className="text-sm font-bold text-gray-800 mb-1">موقع الدفع النقدي</p>
<p className="text-sm font-bold text-gray-800 mb-1">{t('cashPaymentLocation')}</p>
<p className="text-sm text-gray-600 leading-relaxed">
مكتب المنصة: أبو رمانة، شارع المالكي، دمشق
{t('platformOfficeAddress')}
</p>
<p className="text-sm text-gray-600 flex items-center gap-1 mt-1">
<Phone className="w-3.5 h-3.5" />
@ -845,7 +849,7 @@ function PaymentDialog({
onClick={onClose}
className="px-5 py-2.5 rounded-xl border border-gray-300 text-gray-700 hover:bg-gray-100 transition-colors font-medium"
>
إغلاق
{t('close')}
</button>
</div>
</motion.div>
@ -861,15 +865,13 @@ function PaymentDialog({
<div className="w-16 h-16 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-5">
<Clock className="w-8 h-8 text-amber-600" />
</div>
<h3 className="text-xl font-bold text-gray-900 mb-3">الدفع النقدي قيد الانتظار</h3>
<h3 className="text-xl font-bold text-gray-900 mb-3">{t('cashPaymentPending')}</h3>
<p className="text-gray-600 leading-relaxed mb-6">
اذهب إلى موقع المنصة وادفع العربون.
<br />
ستقوم المنصة بتأكيد الدفع بعد استلام المبلغ.
{t('cashPaymentInstructions')}
</p>
<div className="bg-gray-50 rounded-2xl p-4 mb-6 text-right">
<p className="text-sm font-bold text-gray-800 mb-1">موقع المنصة</p>
<p className="text-sm text-gray-600">مكتب المنصة: أبو رمانة، شارع المالكي، دمشق</p>
<p className="text-sm font-bold text-gray-800 mb-1">{t('platformLocation')}</p>
<p className="text-sm text-gray-600">{t('platformOfficeAddress')}</p>
<p className="text-sm text-gray-600 flex items-center gap-1 mt-1" dir="ltr">
<Phone className="w-3.5 h-3.5" />
+963567823411
@ -881,13 +883,13 @@ function PaymentDialog({
onClick={() => setShowCashDialog(false)}
className="w-full px-5 py-3 rounded-xl border border-gray-300 text-gray-700 hover:bg-gray-100 transition-colors font-medium"
>
إغلاق
{t('close')}
</button>
<Link
href="/reservations"
className="w-full px-5 py-3 rounded-xl bg-amber-500 text-white font-semibold text-center hover:bg-amber-600 transition-colors"
>
حجوزاتي
{t('myBookings')}
</Link>
</div>
</motion.div>
@ -898,6 +900,7 @@ function PaymentDialog({
}
function ReportDialog({ isOpen, reservation, onClose, onSubmit, submitting }) {
const { t } = useTranslation();
const [message, setMessage] = useState('');
useEffect(() => {
@ -924,8 +927,8 @@ function ReportDialog({ isOpen, reservation, onClose, onSubmit, submitting }) {
<div className="bg-gradient-to-r from-red-500 to-red-600 p-6 text-white">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold">الإبلاغ عن الحجز</h2>
<p className="text-red-100 text-sm mt-1">رقم الحجز: #{reservation.id}</p>
<h2 className="text-xl font-bold">{t('reportReservation')}</h2>
<p className="text-red-100 text-sm mt-1">{t('reservationNumber', { id: reservation.id })}</p>
</div>
<button onClick={onClose} className="rounded-full p-1 hover:bg-white/20">
<XCircle className="h-6 w-6" />
@ -935,13 +938,13 @@ function ReportDialog({ isOpen, reservation, onClose, onSubmit, submitting }) {
<div className="p-6">
<p className="text-gray-700 mb-4 leading-7">
اخبر فريق الدعم بما حدث التفاصيل الواضحة تساعدنا على مراجعة هذا الحجز بشكل اسرع
{t('reportDescription')}
</p>
<textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="اكتب تفاصيل البلاغ هنا..."
placeholder={t('reportPlaceholder')}
rows={5}
className="w-full resize-none rounded-xl border border-gray-300 p-3 text-sm focus:outline-none focus:ring-2 focus:ring-red-500"
/>
@ -955,14 +958,14 @@ function ReportDialog({ isOpen, reservation, onClose, onSubmit, submitting }) {
}`}
>
{submitting ? <Loader2 className="h-4 w-4 animate-spin" /> : <Flag className="h-4 w-4" />}
{submitting ? 'جاري الإرسال...' : 'إرسال البلاغ'}
{submitting ? t('sending') : t('sendReport')}
</button>
<button
onClick={onClose}
disabled={submitting}
className="rounded-xl bg-gray-200 px-4 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-300 transition-colors disabled:cursor-not-allowed"
>
إلغاء
{t('cancel')}
</button>
</div>
</div>
@ -972,6 +975,7 @@ function ReportDialog({ isOpen, reservation, onClose, onSubmit, submitting }) {
}
function ReservationCard({ r, onViewDetails, onPay, onReport, payingId, reportingId }) {
const { t } = useTranslation();
const p = r._prop;
const imgs = propImages(p, r);
const img = imgs.length > 0 ? `${API_BASE}${imgs[0]}` : null;
@ -1004,54 +1008,54 @@ function ReservationCard({ r, onViewDetails, onPay, onReport, payingId, reportin
</div>
<div className="text-left">
<div className="text-lg font-bold text-amber-600">{r.totalPrice?.toLocaleString() ?? '—'}</div>
<div className="text-xs text-gray-500">السعر الإجمالي</div>
<div className="text-xs text-gray-500">{t('totalPrice')}</div>
</div>
</div>
{(beds||baths) && <div className="flex gap-3 mb-3 text-sm text-gray-600">{beds>0&&<span>{beds} غرف</span>}{baths>0&&<span>{baths} حمامات</span>}</div>}
{(beds||baths) && <div className="flex gap-3 mb-3 text-sm text-gray-600">{beds>0&&<span>{beds} {t('rooms')}</span>}{baths>0&&<span>{baths} {t('bathrooms')}</span>}</div>}
<div className="grid grid-cols-2 gap-3 mb-4 text-center">
<div className="bg-gray-50 p-2 rounded-lg">
<Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1"/><div className="text-xs text-gray-500">من</div>
<Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1"/><div className="text-xs text-gray-500">{t('from')}</div>
<div className="text-sm font-medium">{new Date(r.startDate).toLocaleDateString('ar')}</div>
</div>
<div className="bg-gray-50 p-2 rounded-lg">
<Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1"/><div className="text-xs text-gray-500">إلى</div>
<Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1"/><div className="text-xs text-gray-500">{t('to')}</div>
<div className="text-sm font-medium">{new Date(r.endDate).toLocaleDateString('ar')}</div>
</div>
</div>
{isOwnerConfirmed && hasTimeWindow && <div className="bg-blue-50 p-3 rounded-xl mb-3">
<div className="flex items-center justify-between mb-1">
<span className="text-sm text-blue-800 font-medium flex items-center gap-1"><Timer className="w-4 h-4"/> متبقي للدفع:</span>
<span className="text-sm text-blue-800 font-medium flex items-center gap-1"><Timer className="w-4 h-4"/> {t('remainingForPayment')}</span>
<CountdownTimer deadline={deadline} />
</div>
<div className="text-xs text-blue-600">مدة الدفع: {formatWindowDuration(p.allowedPaymentPeriod)}</div>
<div className="text-xs text-blue-600">{t('paymentDuration', { duration: formatWindowDuration(p.allowedPaymentPeriod) })}</div>
</div>}
<div className="flex gap-3 pt-3 border-t border-gray-100">
<button onClick={() => onViewDetails(r)}
className="flex-1 bg-gray-100 text-gray-700 py-2 rounded-xl text-sm font-medium hover:bg-gray-200 transition-colors flex items-center justify-center gap-2">
<Eye className="w-4 h-4"/> التفاصيل
<Eye className="w-4 h-4"/> {t('details')}
</button>
{isOwnerConfirmed && !isExpired && <button onClick={() => onPay(r)} disabled={isPaying}
className={`flex-1 py-2 rounded-xl text-sm font-medium transition-colors flex items-center justify-center gap-2 ${isPaying ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-amber-500 text-white hover:bg-amber-600'}`}>
{isPaying ? <Loader2 className="w-4 h-4 animate-spin"/> : <CreditCard className="w-4 h-4"/>} {isPaying ? 'جاري الدفع...' : 'ادفع الآن'}
{isPaying ? <Loader2 className="w-4 h-4 animate-spin"/> : <CreditCard className="w-4 h-4"/>} {isPaying ? t('processingPayment') : t('payNow')}
</button>}
</div>
<button onClick={() => onReport(r)} disabled={isReporting}
className={`w-full mt-3 py-2 rounded-xl text-sm font-medium transition-colors flex items-center justify-center gap-2 ${isReporting ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-red-50 text-red-700 hover:bg-red-100'}`}>
{isReporting ? <Loader2 className="w-4 h-4 animate-spin"/> : <Flag className="w-4 h-4"/>} {isReporting ? 'جاري الإبلاغ...' : 'إبلاغ'}
{isReporting ? <Loader2 className="w-4 h-4 animate-spin"/> : <Flag className="w-4 h-4"/>} {isReporting ? t('reportingAction') : t('report')}
</button>
{canRate && !showRating && <button onClick={() => setShowRating(true)}
className="w-full mt-3 bg-amber-50 text-amber-700 py-2 rounded-xl text-sm font-medium hover:bg-amber-100 transition-colors flex items-center justify-center gap-2">
<Star className="w-4 h-4"/> قيّم هذا العقار
<Star className="w-4 h-4"/> {t('rateThisProperty')}
</button>}
{canRate && showRating && <div className="mt-3 bg-amber-50 p-3 rounded-xl">
<div className="space-y-2 mb-3">
{[
{ key: 'clean', label: 'النظافة' },
{ key: 'services', label: 'الخدمات' },
{ key: 'ownerBehavior', label: 'تعامل المالك' },
{ key: 'experience', label: 'التجربة العامة' },
{ key: 'clean', label: 'ratingCategory.clean' },
{ key: 'services', label: 'ratingCategory.services' },
{ key: 'ownerBehavior', label: 'ratingCategory.ownerBehavior' },
{ key: 'experience', label: 'ratingCategory.experience' },
].map(cat => <div key={cat.key} className="flex items-center justify-between">
<span className="text-sm text-gray-700">{cat.label}</span>
<span className="text-sm text-gray-700">{t(cat.label)}</span>
<div className="flex gap-0.5">
{[1,2,3,4,5].map(n => (
<button key={n} onClick={() => setRatings(p => ({...p, [cat.key]: n}))}
@ -1063,26 +1067,26 @@ function ReservationCard({ r, onViewDetails, onPay, onReport, payingId, reportin
</div>)}
</div>
<textarea value={ratingComment} onChange={e => setRatingComment(e.target.value)}
placeholder="أكتب تعليقك (اختياري)"
placeholder={t('writeCommentOptional')}
className="w-full p-2 text-sm border border-amber-200 rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-amber-500 mb-2" rows={2} />
<div className="flex gap-2">
<button onClick={async () => {
if (!ratings.clean || !ratings.services || !ratings.ownerBehavior || !ratings.experience) return toast.error('قيّم جميع الفئات');
if (!ratings.clean || !ratings.services || !ratings.ownerBehavior || !ratings.experience) return toast.error(t('rateAllCategories'));
setSubmittingRating(true);
try {
await addPropertyRating({ reservationId: r.id, cleanRating: ratings.clean, servicesRating: ratings.services, ownerBehaviorRating: ratings.ownerBehavior, experienceRating: ratings.experience, comment: ratingComment || null });
toast.success('تم إرسال التقييم');
toast.success(t('ratingSubmittedToast'));
setShowRating(false);
setRatings({ clean: 0, services: 0, ownerBehavior: 0, experience: 0 });
setRatingComment('');
} catch (e) { toast.error(e?.message || 'فشل إرسال التقييم'); }
} catch (e) { toast.error(e?.message || t('ratingFailed')); }
finally { setSubmittingRating(false); }
}} disabled={submittingRating}
className="flex-1 bg-amber-500 text-white py-1.5 rounded-lg text-sm font-medium hover:bg-amber-600 transition-colors disabled:bg-gray-300">
{submittingRating ? 'جاري الإرسال...' : 'إرسال التقييم'}
{submittingRating ? t('sending') : t('submitRating')}
</button>
<button onClick={() => { setShowRating(false); setRatings({ clean: 0, services: 0, ownerBehavior: 0, experience: 0 }); setRatingComment(''); }}
className="px-4 py-1.5 bg-gray-200 text-gray-700 rounded-lg text-sm hover:bg-gray-300 transition-colors">إلغاء</button>
className="px-4 py-1.5 bg-gray-200 text-gray-700 rounded-lg text-sm hover:bg-gray-300 transition-colors">{t('cancel')}</button>
</div>
</div>}
</div>
@ -1091,6 +1095,7 @@ function ReservationCard({ r, onViewDetails, onPay, onReport, payingId, reportin
}
function DetailsModal({ r, isOpen, onClose, onPay, onReport, payingId, reportingId }) {
const { t } = useTranslation();
if (!isOpen || !r) return null;
const p = r._prop;
const isOwnerConfirmed = STATUS_MAP[r.status] === 'ownerConfirmed';
@ -1109,47 +1114,47 @@ function DetailsModal({ r, isOpen, onClose, onPay, onReport, payingId, reporting
className="bg-white rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto shadow-2xl" onClick={e=>e.stopPropagation()}>
<div className="sticky top-0 bg-gradient-to-r from-amber-500 to-amber-600 p-6 text-white">
<div className="flex justify-between items-center">
<h2 className="text-xl font-bold">تفاصيل الحجز</h2>
<h2 className="text-xl font-bold">{t('bookingDetails')}</h2>
<button onClick={onClose} className="p-1 hover:bg-white/20 rounded-full"><XCircle className="w-6 h-6"/></button>
</div>
<p className="text-amber-100 text-sm mt-1">رقم الحجز: #{r.id}</p>
<p className="text-amber-100 text-sm mt-1">{t('reservationNumber', { id: r.id })}</p>
</div>
<div className="p-6 space-y-6">
{p && <div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2"><Home className="w-5 h-5 text-amber-500"/> معلومات العقار</h3>
<p><span className="text-gray-500">العنوان:</span> {propAddr(p, r)||''}</p>
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2"><Home className="w-5 h-5 text-amber-500"/> {t('propertyInfo')}</h3>
<p><span className="text-gray-500">{t('addressLabel')}</span> {propAddr(p, r)||''}</p>
{(propBeds(p, r)||propBaths(p, r)) && <div className="flex gap-3 mt-2">
{propBeds(p, r)>0&&<span className="text-sm bg-white px-2 py-1 rounded-lg">{propBeds(p, r)} غرف</span>}
{propBaths(p, r)>0&&<span className="text-sm bg-white px-2 py-1 rounded-lg">{propBaths(p, r)} حمامات</span>}
{propBeds(p, r)>0&&<span className="text-sm bg-white px-2 py-1 rounded-lg">{propBeds(p, r)} {t('rooms')}</span>}
{propBaths(p, r)>0&&<span className="text-sm bg-white px-2 py-1 rounded-lg">{propBaths(p, r)} {t('bathrooms')}</span>}
</div>}
</div>}
<div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2"><Calendar className="w-5 h-5 text-amber-500"/> تفاصيل الحجز</h3>
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2"><Calendar className="w-5 h-5 text-amber-500"/> {t('bookingDetails')}</h3>
<div className="grid grid-cols-2 gap-4">
<div><p className="text-gray-500">تاريخ البداية</p><p className="font-medium">{new Date(r.startDate).toLocaleDateString('ar')}</p></div>
<div><p className="text-gray-500">تاريخ النهاية</p><p className="font-medium">{new Date(r.endDate).toLocaleDateString('ar')}</p></div>
<div><p className="text-gray-500">الحالة</p><StatusBadge code={r.status}/></div>
<div><p className="text-gray-500">تاريخ الإنشاء</p><p className="font-medium">{new Date(r.createdAt).toLocaleDateString('ar')}</p></div>
<div><p className="text-gray-500">{t('startDateLabel')}</p><p className="font-medium">{new Date(r.startDate).toLocaleDateString('ar')}</p></div>
<div><p className="text-gray-500">{t('endDateLabel')}</p><p className="font-medium">{new Date(r.endDate).toLocaleDateString('ar')}</p></div>
<div><p className="text-gray-500">{t('statusLabel')}</p><StatusBadge code={r.status}/></div>
<div><p className="text-gray-500">{t('createdAtLabel')}</p><p className="font-medium">{new Date(r.createdAt).toLocaleDateString('ar')}</p></div>
</div>
</div>
<div className="bg-amber-50 p-4 rounded-xl">
<h3 className="font-bold text-amber-700 mb-3 flex items-center gap-2"><DollarSign className="w-5 h-5"/> المعلومات المالية</h3>
<div className="flex justify-between font-bold"><span className="text-gray-900">الإجمالي</span><span className="text-amber-600 text-lg">{r.totalPrice?.toLocaleString()??''}</span></div>
<h3 className="font-bold text-amber-700 mb-3 flex items-center gap-2"><DollarSign className="w-5 h-5"/> {t('financialInfo')}</h3>
<div className="flex justify-between font-bold"><span className="text-gray-900">{t('total')}</span><span className="text-amber-600 text-lg">{r.totalPrice?.toLocaleString()??''}</span></div>
</div>
{isOwnerConfirmed && hasTimeWindow && <div className="bg-blue-50 p-4 rounded-xl">
<div className="flex items-center justify-between mb-2">
<span className="text-blue-800 font-medium flex items-center gap-2"><Timer className="w-5 h-5"/> متبقي للدفع:</span>
<span className="text-blue-800 font-medium flex items-center gap-2"><Timer className="w-5 h-5"/> {t('remainingForPayment')}</span>
<CountdownTimer deadline={deadline} />
</div>
<div className="text-xs text-blue-600 mb-3">مدة الدفع: {formatWindowDuration(p.allowedPaymentPeriod)}</div>
<div className="text-xs text-blue-600 mb-3">{t('paymentDuration', { duration: formatWindowDuration(p.allowedPaymentPeriod) })}</div>
{!isExpired && <button onClick={() => { onPay(r); onClose(); }} disabled={isPaying}
className={`w-full py-2 rounded-xl font-medium transition-colors flex items-center justify-center gap-2 ${isPaying ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-amber-500 text-white hover:bg-amber-600'}`}>
{isPaying ? <Loader2 className="w-5 h-5 animate-spin"/> : <CreditCard className="w-5 h-5"/>} {isPaying ? 'جاري الدفع...' : 'ادفع الآن'}
{isPaying ? <Loader2 className="w-5 h-5 animate-spin"/> : <CreditCard className="w-5 h-5"/>} {isPaying ? t('processingPayment') : t('payNow')}
</button>}
</div>}
<button onClick={() => { onReport(r); onClose(); }} disabled={isReporting}
className={`w-full py-2 rounded-xl font-medium transition-colors flex items-center justify-center gap-2 ${isReporting ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-red-50 text-red-700 hover:bg-red-100'}`}>
{isReporting ? <Loader2 className="w-5 h-5 animate-spin"/> : <Flag className="w-5 h-5"/>} {isReporting ? 'جاري الإبلاغ...' : 'إبلاغ'}
{isReporting ? <Loader2 className="w-5 h-5 animate-spin"/> : <Flag className="w-5 h-5"/>} {isReporting ? t('reportingAction') : t('report')}
</button>
</div>
</motion.div>
@ -1158,6 +1163,7 @@ function DetailsModal({ r, isOpen, onClose, onPay, onReport, payingId, reporting
}
export default function UserReservationsPage() {
const { t } = useTranslation();
const router = useRouter();
const [reservations, setReservations] = useState([]);
const [filtered, setFiltered] = useState([]);
@ -1196,7 +1202,7 @@ export default function UserReservationsPage() {
setFiltered(enriched);
} catch (err) {
console.error(err);
toast.error('فشل تحميل الحجوزات');
toast.error(t('failedToLoadBookings'));
setReservations([]);
setFiltered([]);
}
@ -1233,11 +1239,11 @@ export default function UserReservationsPage() {
transactionType: 1,
comment: null,
});
toast.success('تم دفع السلفة بنجاح!');
toast.success(t('depositPaidSuccess'));
closePaymentDialog();
loadReservations();
} catch (err) {
toast.error(err?.message || 'فشل عملية الدفع');
toast.error(err?.message || t('paymentFailed'));
} finally {
setPayingId(null);
}
@ -1257,10 +1263,10 @@ export default function UserReservationsPage() {
setReportingId(reportDialog.reservation.id);
try {
await reportReservation(reportDialog.reservation.id, message.trim() || null);
toast.success('تم إرسال البلاغ بنجاح');
toast.success(t('reportSubmittedSuccess'));
closeReportDialog();
} catch (err) {
toast.error(err?.message || 'فشل إرسال البلاغ');
toast.error(err?.message || t('reportFailed'));
} finally {
setReportingId(null);
}
@ -1298,9 +1304,9 @@ export default function UserReservationsPage() {
/>
<div className="container mx-auto px-4">
<motion.div initial={{opacity:0,y:-20}} animate={{opacity:1,y:0}} className="mb-8">
<button onClick={() => router.back()} className="flex items-center gap-2 text-gray-600 hover:text-amber-600 mb-4"><ArrowLeft className="w-5 h-5"/> الرجوع</button>
<h1 className="text-3xl font-bold text-gray-900 mb-2">حجوزاتي</h1>
<p className="text-gray-600">لديك {reservations.length} حجز</p>
<button onClick={() => router.back()} className="flex items-center gap-2 text-gray-600 hover:text-amber-600 mb-4"><ArrowLeft className="w-5 h-5"/> {t('back')}</button>
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t('myBookings')}</h1>
<p className="text-gray-600">{t('youHaveBookings', { count: reservations.length })}</p>
</motion.div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
{Object.entries(counts).map(([s, c]) => (
@ -1308,20 +1314,20 @@ export default function UserReservationsPage() {
className={`bg-white rounded-xl shadow-sm p-4 text-center border cursor-pointer hover:shadow-md transition-all ${filterStatus===s?'border-amber-500 bg-amber-50':'border-gray-200'}`}
onClick={() => setFilterStatus(s)}>
<div className="text-2xl font-bold text-amber-600">{c}</div>
<div className="text-sm text-gray-600">{s==='all'?'الكل':(STATUS_UI[s]?.label||s)}</div>
<div className="text-sm text-gray-600">{s==='all'?t('all'):t(`bookingStatus.${s}`)}</div>
</motion.div>
))}
</div>
<div className="mb-6 relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400"/>
<input type="text" placeholder="ابحث بعنوان العقار أو رقم الحجز..." value={searchTerm} onChange={e=>setSearchTerm(e.target.value)}
<input type="text" placeholder={t('searchPlaceholder')} value={searchTerm} onChange={e=>setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"/>
</div>
{filtered.length === 0 ? (
<div className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
<Calendar className="w-12 h-12 text-amber-600 mx-auto mb-4"/>
<h3 className="text-xl font-bold text-gray-900 mb-2">لا توجد حجوزات</h3>
<p className="text-gray-600">لم تقم بأي حجز حتى الآن</p>
<h3 className="text-xl font-bold text-gray-900 mb-2">{t('noBookings')}</h3>
<p className="text-gray-600">{t('noReservationsDesc')}</p>
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">

View File

@ -1,257 +1,3 @@
// 'use client';
// import { useState } from 'react';
// import { useRouter } from 'next/navigation';
// import Link from 'next/link';
// import { motion } from 'framer-motion';
// import {
// User,
// Shield,
// Trash2,
// LogOut,
// ChevronLeft,
// Bell,
// Lock,
// Eye,
// FileText,
// HelpCircle,
// MessageCircle,
// Loader2,
// AlertTriangle,
// X
// } from 'lucide-react';
// import toast, { Toaster } from 'react-hot-toast';
// import AuthService from '../services/AuthService';
// import { changePassword, deleteMyAccount } from '../utils/api';
// export default function SettingsPage() {
// const router = useRouter();
// const [showDeleteDialog, setShowDeleteDialog] = useState(false);
// const [deletePassword, setDeletePassword] = useState('');
// const [isDeleting, setIsDeleting] = useState(false);
// const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
// const handleSignOut = () => {
// AuthService.deleteToken();
// toast.success('تم تسجيل الخروج بنجاح');
// router.push('/');
// };
// const handleDeleteAccount = async () => {
// if (!deletePassword) {
// toast.error('الرجاء إدخال كلمة المرور');
// return;
// }
// setIsDeleting(true);
// try {
// await deleteMyAccount(deletePassword);
// AuthService.deleteToken();
// toast.success('تم حذف الحساب بنجاح');
// router.push('/');
// } catch (err) {
// toast.error(err.message || 'فشل حذف الحساب');
// } finally {
// setIsDeleting(false);
// setShowDeleteDialog(false);
// setDeletePassword('');
// }
// };
// const sections = [
// {
// title: 'الحساب',
// items: [
// { icon: User, label: 'الملف الشخصي', href: '/profile', desc: 'عرض وتعديل معلوماتك الشخصية' },
// { icon: Lock, label: 'تغيير كلمة المرور', href: '/change-password', desc: 'تحديث كلمة المرور الخاصة بك' },
// { icon: Shield, label: 'التحقق من الحساب', href: '/account-verification', desc: 'تأكيد البريد الإلكتروني ورقم الهاتف' },
// ]
// },
// {
// title: 'الإشعارات',
// items: [
// { icon: Bell, label: 'الإشعارات', href: '/notifications', desc: 'إدارة تفضيلات الإشعارات' },
// ]
// },
// {
// title: 'الدعم',
// items: [
// { icon: HelpCircle, label: 'الأسئلة الشائعة', href: '/faq', desc: 'إجابات للأسئلة المتكررة' },
// { icon: MessageCircle, label: 'تواصل معنا', href: '/support', desc: 'الحصول على المساعدة والدعم' },
// { icon: FileText, label: 'الشروط والأحكام', href: '/terms', desc: 'سياسة الاستخدام والخصوصية' },
// { icon: Eye, label: 'سياسة الخصوصية', href: '/privacy', desc: 'كيف نحمي بياناتك' },
// ]
// },
// ];
// const containerVariants = {
// hidden: { opacity: 0 },
// visible: {
// opacity: 1,
// transition: { staggerChildren: 0.08 }
// }
// };
// const itemVariants = {
// hidden: { opacity: 0, y: 20 },
// visible: { opacity: 1, y: 0 }
// };
// return (
// <div className="min-h-screen bg-gray-50 py-8" dir="rtl">
// <Toaster position="top-center" reverseOrder={false} />
// <div className="container mx-auto px-4 max-w-2xl">
// <motion.div
// initial={{ opacity: 0, y: -20 }}
// animate={{ opacity: 1, y: 0 }}
// className="flex items-center gap-3 mb-8"
// >
// <Link
// href="/profile"
// className="p-2 rounded-xl hover:bg-gray-200 transition-colors text-gray-600"
// >
// <ChevronLeft className="w-5 h-5" />
// </Link>
// <h1 className="text-2xl font-bold text-gray-900">الإعدادات</h1>
// </motion.div>
// <motion.div
// variants={containerVariants}
// initial="hidden"
// animate="visible"
// className="space-y-6"
// >
// {sections.map((section) => (
// <motion.div
// key={section.title}
// variants={itemVariants}
// className="bg-white rounded-2xl shadow-sm overflow-hidden"
// >
// <div className="px-6 py-4 border-b border-gray-100">
// <h2 className="text-lg font-semibold text-gray-800">{section.title}</h2>
// </div>
// <div className="divide-y divide-gray-50">
// {section.items.map((item) => {
// const Icon = item.icon;
// return (
// <Link
// key={item.label}
// href={item.href}
// className="flex items-center gap-4 px-6 py-4 hover:bg-gray-50 transition-colors group"
// >
// <div className="w-10 h-10 rounded-xl bg-amber-50 flex items-center justify-center flex-shrink-0 group-hover:bg-amber-100 transition-colors">
// <Icon className="w-5 h-5 text-amber-600" />
// </div>
// <div className="flex-1 min-w-0">
// <p className="text-sm font-medium text-gray-900">{item.label}</p>
// <p className="text-xs text-gray-500 truncate">{item.desc}</p>
// </div>
// <ChevronLeft className="w-4 h-4 text-gray-400" />
// </Link>
// );
// })}
// </div>
// </motion.div>
// ))}
// <motion.div variants={itemVariants} className="space-y-4">
// <div className="bg-white rounded-2xl shadow-sm overflow-hidden">
// <div className="px-6 py-4 border-b border-gray-100">
// <h2 className="text-lg font-semibold text-gray-800">الأمان</h2>
// </div>
// <div className="p-6">
// <button
// onClick={() => setShowDeleteDialog(true)}
// className="w-full flex items-center gap-3 px-4 py-3 rounded-xl border border-red-200 text-red-600 hover:bg-red-50 transition-colors"
// >
// <Trash2 className="w-5 h-5" />
// <span className="text-sm font-medium">حذف الحساب</span>
// </button>
// <p className="text-xs text-gray-500 mt-2 pr-12">
// سيتم حذف جميع بياناتك بشكل دائم ولا يمكن التراجع عن هذا الإجراء
// </p>
// </div>
// </div>
// <div className="bg-white rounded-2xl shadow-sm overflow-hidden">
// <div className="p-6">
// <button
// onClick={handleSignOut}
// className="w-full flex items-center gap-3 px-4 py-3 rounded-xl border border-gray-200 text-gray-700 hover:bg-gray-50 transition-colors"
// >
// <LogOut className="w-5 h-5" />
// <span className="text-sm font-medium">تسجيل الخروج</span>
// </button>
// </div>
// </div>
// </motion.div>
// </motion.div>
// </div>
// {showDeleteDialog && (
// <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
// <motion.div
// initial={{ opacity: 0, scale: 0.95 }}
// animate={{ opacity: 1, scale: 1 }}
// className="bg-white rounded-2xl shadow-xl max-w-md w-full p-6"
// >
// <div className="flex items-center gap-3 mb-4">
// <div className="w-10 h-10 rounded-full bg-red-100 flex items-center justify-center">
// <AlertTriangle className="w-5 h-5 text-red-600" />
// </div>
// <div>
// <h3 className="text-lg font-semibold text-gray-900">حذف الحساب</h3>
// <p className="text-sm text-gray-500">هذا الإجراء لا يمكن التراجع عنه</p>
// </div>
// <button
// onClick={() => { setShowDeleteDialog(false); setDeletePassword(''); }}
// className="mr-auto p-1 hover:bg-gray-100 rounded-lg transition-colors"
// >
// <X className="w-5 h-5 text-gray-400" />
// </button>
// </div>
// <p className="text-sm text-gray-600 mb-4">
// أدخل كلمة المرور لتأكيد حذف حسابك نهائياً. سيتم حذف جميع بياناتك وملفاتك بشكل دائم.
// </p>
// <input
// type="password"
// value={deletePassword}
// onChange={(e) => setDeletePassword(e.target.value)}
// placeholder="كلمة المرور"
// className="w-full px-4 py-3 border border-gray-300 rounded-xl mb-4 focus:ring-2 focus:ring-red-500 focus:border-transparent outline-none"
// />
// <div className="flex gap-3">
// <button
// onClick={() => { setShowDeleteDialog(false); setDeletePassword(''); }}
// className="flex-1 px-4 py-3 rounded-xl border border-gray-200 text-gray-700 hover:bg-gray-50 transition-colors text-sm font-medium"
// >
// إلغاء
// </button>
// <button
// onClick={handleDeleteAccount}
// disabled={isDeleting}
// className="flex-1 px-4 py-3 rounded-xl bg-red-600 text-white hover:bg-red-700 transition-colors text-sm font-medium disabled:opacity-50 flex items-center justify-center gap-2"
// >
// {isDeleting ? <Loader2 className="w-4 h-4 animate-spin" /> : <Trash2 className="w-4 h-4" />}
// {isDeleting ? 'جاري الحذف...' : 'تأكيد الحذف'}
// </button>
// </div>
// </motion.div>
// </div>
// )}
// </div>
// );
// }
'use client';
import { useState } from 'react';
@ -266,19 +12,21 @@ import {
ChevronLeft,
Bell,
Lock,
Eye,
FileText,
HelpCircle,
MessageCircle,
FileText,
Eye,
Loader2,
AlertTriangle,
X
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
import toast, { Toaster } from 'react-hot-toast';
import AuthService from '../services/AuthService';
import { changePassword, deleteMyAccount, submitReport } from '../utils/api';
export default function SettingsPage() {
const { t, i18n } = useTranslation();
const router = useRouter();
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [deletePassword, setDeletePassword] = useState('');
@ -292,24 +40,24 @@ export default function SettingsPage() {
const handleSignOut = () => {
AuthService.deleteToken();
toast.success('تم تسجيل الخروج بنجاح');
toast.success(t('logout-success'));
router.push('/');
};
const handleDeleteAccount = async () => {
if (!deletePassword) {
toast.error('الرجاء إدخال كلمة المرور');
toast.error(t('enter-password-prompt'));
return;
}
setIsDeleting(true);
try {
await deleteMyAccount(deletePassword);
AuthService.deleteToken();
toast.success('تم حذف الحساب بنجاح');
toast.success(t('delete-account-success'));
router.push('/');
} catch (err) {
console.error('Delete account error:', err);
toast.error(err.message || 'فشل حذف الحساب');
toast.error(err.message || t('delete-account-failed'));
} finally {
setIsDeleting(false);
setShowDeleteDialog(false);
@ -319,19 +67,19 @@ export default function SettingsPage() {
const handleSendContact = async () => {
if (!contactSubject.trim() || !contactBody.trim()) {
toast.error('الرجاء تعبئة العنوان والرسالة');
toast.error(t('fill-subject-and-message'));
return;
}
setIsSendingContact(true);
try {
await submitReport(contactSubject.trim(), contactBody.trim());
toast.success('تم إرسال رسالتك بنجاح');
toast.success(t('send-success'));
setShowContactDialog(false);
setContactSubject('');
setContactBody('');
} catch (err) {
toast.error(err.message || 'حدث خطأ أثناء الإرسال');
toast.error(err.message || t('send-error'));
} finally {
setIsSendingContact(false);
}
@ -339,26 +87,26 @@ export default function SettingsPage() {
const sections = [
{
title: 'الحساب',
title: t('account'),
items: [
{ icon: User, label: 'الملف الشخصي', href: '/profile', desc: 'عرض وتعديل معلوماتك الشخصية' },
{ icon: Lock, label: 'تغيير كلمة المرور', href: '/change-password', desc: 'تحديث كلمة المرور الخاصة بك' },
{ icon: Shield, label: 'التحقق من الحساب', href: '/account-verification', desc: 'تأكيد البريد الإلكتروني ورقم الهاتف' },
{ icon: User, label: t('profile'), href: '/profile', desc: t('profileDesc') },
{ icon: Lock, label: t('change-password'), href: '/change-password', desc: t('change-password-desc') },
{ icon: Shield, label: t('account-verification'), href: '/account-verification', desc: t('account-verification-desc') },
]
},
{
title: 'الإشعارات',
title: t('notifications'),
items: [
{ icon: Bell, label: 'الإشعارات', href: '/notifications', desc: 'إدارة تفضيلات الإشعارات' },
{ icon: Bell, label: t('notifications'), href: '/notifications', desc: t('notification-preferences-desc') },
]
},
{
title: 'الدعم',
title: t('support'),
items: [
{ icon: HelpCircle, label: 'الأسئلة الشائعة', href: '/faq', desc: 'إجابات للأسئلة المتكررة' },
{ icon: MessageCircle, label: 'تواصل معنا', desc: 'الحصول على المساعدة والدعم', action: () => setShowContactDialog(true) },
{ icon: FileText, label: 'الشروط والأحكام', href: '/terms', desc: 'سياسة الاستخدام والخصوصية' },
{ icon: Eye, label: 'سياسة الخصوصية', href: '/privacy', desc: 'كيف نحمي بياناتك' },
{ icon: HelpCircle, label: t('faq'), href: '/faq', desc: t('faq-desc') },
{ icon: MessageCircle, label: t('contactUs'), desc: t('contact-us-desc'), action: () => setShowContactDialog(true) },
{ icon: FileText, label: t('terms-and-conditions'), href: '/terms', desc: t('terms-desc') },
{ icon: Eye, label: t('privacy'), href: '/privacy', desc: t('privacy-desc') },
]
},
];
@ -377,7 +125,7 @@ export default function SettingsPage() {
};
return (
<div className="min-h-screen bg-gray-50 py-8 pb-28" dir="rtl">
<div className="min-h-screen bg-gray-50 py-8 pb-28" dir={i18n.language === 'ar' ? 'rtl' : 'ltr'}>
<Toaster position="top-center" reverseOrder={false} />
<div className="container mx-auto px-4 max-w-2xl">
@ -392,7 +140,7 @@ export default function SettingsPage() {
>
<ChevronLeft className="w-5 h-5" />
</Link>
<h1 className="text-2xl font-bold text-gray-900">الإعدادات</h1>
<h1 className="text-2xl font-bold text-gray-900">{t('settings')}</h1>
</motion.div>
<motion.div
@ -456,7 +204,7 @@ export default function SettingsPage() {
<motion.div variants={itemVariants} className="space-y-4">
<div className="bg-white rounded-2xl shadow-sm overflow-hidden">
<div className="px-6 py-4 border-b border-gray-100">
<h2 className="text-lg font-semibold text-gray-800">الأمان</h2>
<h2 className="text-lg font-semibold text-gray-800">{t('security')}</h2>
</div>
<div className="p-6">
<button
@ -464,10 +212,10 @@ export default function SettingsPage() {
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl border border-red-200 text-red-600 hover:bg-red-50 transition-colors"
>
<Trash2 className="w-5 h-5" />
<span className="text-sm font-medium">حذف الحساب</span>
<span className="text-sm font-medium">{t('deleteAccount')}</span>
</button>
<p className="text-xs text-gray-500 mt-2 pr-12">
سيتم حذف جميع بياناتك بشكل دائم ولا يمكن التراجع عن هذا الإجراء
{t('delete-account-warning')}
</p>
</div>
</div>
@ -479,7 +227,7 @@ export default function SettingsPage() {
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl border border-gray-200 text-gray-700 hover:bg-gray-50 transition-colors"
>
<LogOut className="w-5 h-5" />
<span className="text-sm font-medium">تسجيل الخروج</span>
<span className="text-sm font-medium">{t('signOut')}</span>
</button>
</div>
</div>
@ -499,8 +247,8 @@ export default function SettingsPage() {
<AlertTriangle className="w-5 h-5 text-red-600" />
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900">حذف الحساب</h3>
<p className="text-sm text-gray-500">هذا الإجراء لا يمكن التراجع عنه</p>
<h3 className="text-lg font-semibold text-gray-900">{t('deleteDialogTitle')}</h3>
<p className="text-sm text-gray-500">{t('deleteDialogWarning')}</p>
</div>
<button
onClick={() => { setShowDeleteDialog(false); setDeletePassword(''); }}
@ -511,14 +259,14 @@ export default function SettingsPage() {
</div>
<p className="text-sm text-gray-600 mb-4">
أدخل كلمة المرور لتأكيد حذف حسابك نهائياً. سيتم حذف جميع بياناتك وملفاتك بشكل دائم.
{t('delete-account-confirm-desc')}
</p>
<input
type="password"
value={deletePassword}
onChange={(e) => setDeletePassword(e.target.value)}
placeholder="كلمة المرور"
placeholder={t('enter-password-prompt')}
className="w-full px-4 py-3 border border-gray-300 rounded-xl mb-4 focus:ring-2 focus:ring-red-500 focus:border-transparent outline-none"
/>
@ -527,7 +275,7 @@ export default function SettingsPage() {
onClick={() => { setShowDeleteDialog(false); setDeletePassword(''); }}
className="flex-1 px-4 py-3 rounded-xl border border-gray-200 text-gray-700 hover:bg-gray-50 transition-colors text-sm font-medium"
>
إلغاء
{t('cancel')}
</button>
<button
onClick={handleDeleteAccount}
@ -535,7 +283,7 @@ export default function SettingsPage() {
className="flex-1 px-4 py-3 rounded-xl bg-red-600 text-white hover:bg-red-700 transition-colors text-sm font-medium disabled:opacity-50 flex items-center justify-center gap-2"
>
{isDeleting ? <Loader2 className="w-4 h-4 animate-spin" /> : <Trash2 className="w-4 h-4" />}
{isDeleting ? 'جاري الحذف...' : 'تأكيد الحذف'}
{isDeleting ? t('deleting') : t('confirm-delete')}
</button>
</div>
</motion.div>
@ -554,8 +302,8 @@ export default function SettingsPage() {
<MessageCircle className="w-5 h-5 text-amber-600" />
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900">تواصل معنا</h3>
<p className="text-sm text-gray-500">سنقوم بالرد عليك في أقرب وقت</p>
<h3 className="text-lg font-semibold text-gray-900">{t('contactUs')}</h3>
<p className="text-sm text-gray-500">{t('contact-reply-prompt')}</p>
</div>
<button
onClick={() => {
@ -573,7 +321,7 @@ export default function SettingsPage() {
type="text"
value={contactSubject}
onChange={(e) => setContactSubject(e.target.value)}
placeholder="الموضوع"
placeholder={t('subject')}
maxLength={300}
className="w-full px-4 py-3 border border-gray-300 rounded-xl mb-4 focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none"
/>
@ -581,7 +329,7 @@ export default function SettingsPage() {
<textarea
value={contactBody}
onChange={(e) => setContactBody(e.target.value)}
placeholder="اكتب رسالتك هنا..."
placeholder={t('write-message-placeholder')}
rows={5}
className="w-full px-4 py-3 border border-gray-300 rounded-xl mb-4 focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none resize-none"
/>
@ -595,7 +343,7 @@ export default function SettingsPage() {
}}
className="flex-1 px-4 py-3 rounded-xl border border-gray-200 text-gray-700 hover:bg-gray-50 transition-colors text-sm font-medium"
>
إلغاء
{t('cancel')}
</button>
<button
onClick={handleSendContact}
@ -603,7 +351,7 @@ export default function SettingsPage() {
className="flex-1 px-4 py-3 rounded-xl bg-amber-600 text-white hover:bg-amber-700 transition-colors text-sm font-medium disabled:opacity-50 flex items-center justify-center gap-2"
>
{isSendingContact ? <Loader2 className="w-4 h-4 animate-spin" /> : <MessageCircle className="w-4 h-4" />}
{isSendingContact ? 'جاري الإرسال...' : 'إرسال'}
{isSendingContact ? t('sending') : t('send')}
</button>
</div>
</motion.div>
@ -611,4 +359,4 @@ export default function SettingsPage() {
)}
</div>
);
}
}

View File

@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
import { useTranslation } from 'react-i18next';
import { motion } from "framer-motion";
import {
MessageCircle,
@ -15,6 +16,7 @@ import { submitReport } from "../utils/api";
import AuthService from "../services/AuthService";
export default function SupportPage() {
const { t, i18n } = useTranslation();
const [subject, setSubject] = useState("");
const [body, setBody] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
@ -22,23 +24,23 @@ export default function SupportPage() {
const handleSubmit = async (e) => {
e.preventDefault();
if (!subject.trim() || !body.trim()) {
toast.error("يرجى تعبئة جميع الحقول");
toast.error(t('support.fillAllFields'));
return;
}
if (!AuthService.isAuthenticated()) {
toast.error("يرجى تسجيل الدخول أولاً لإرسال طلب دعم");
toast.error(t('auth.loginRequired'));
return;
}
setIsSubmitting(true);
try {
await submitReport(subject, body);
toast.success("تم إرسال طلب الدعم بنجاح");
toast.success(t('support.submittedSuccess'));
setSubject("");
setBody("");
} catch (error) {
toast.error("حدث خطأ أثناء إرسال الطلب. حاول مرة أخرى");
toast.error(t('support.submitError'));
} finally {
setIsSubmitting(false);
}
@ -47,7 +49,7 @@ export default function SupportPage() {
return (
<div
className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12"
dir="rtl"
dir={i18n.language === 'ar' ? 'rtl' : 'ltr'}
>
<Toaster position="top-center" reverseOrder={false} />
<div className="container mx-auto px-4 max-w-5xl">
@ -60,11 +62,10 @@ export default function SupportPage() {
<MessageCircle className="w-10 h-10 text-amber-600" />
</div>
<h1 className="text-4xl font-bold text-gray-900 mb-4">
خدمة العملاء
{t('support.customerService')}
</h1>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
نحن هنا لمساعدتك. تواصل معنا عبر النموذج أدناه أو من خلال معلومات
الاتصال المباشرة
{t('support.hereToHelp')}
</p>
</motion.div>
@ -77,30 +78,30 @@ export default function SupportPage() {
>
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-8">
<h2 className="text-2xl font-bold text-gray-900 mb-6">
أرسل لنا رسالة
{t('support.sendMessage')}
</h2>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
الموضوع
{t('support.subjectLabel')}
</label>
<input
type="text"
value={subject}
onChange={(e) => setSubject(e.target.value)}
placeholder="اكتب موضوع الرسالة..."
placeholder={t('support.subjectPlaceholder')}
className="w-full px-4 py-3 bg-white border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
الرسالة
{t('support.messageLabel')}
</label>
<textarea
value={body}
onChange={(e) => setBody(e.target.value)}
rows={6}
placeholder="اكتب تفاصيل طلبك..."
placeholder={t('support.messagePlaceholder')}
className="w-full px-4 py-3 bg-white border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400 resize-none"
/>
</div>
@ -114,7 +115,7 @@ export default function SupportPage() {
) : (
<Send className="w-5 h-5" />
)}
{isSubmitting ? "جاري الإرسال..." : "إرسال"}
{isSubmitting ? t('support.sending') : t('support.send')}
</button>
</form>
</div>
@ -128,7 +129,7 @@ export default function SupportPage() {
>
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-bold text-gray-900 mb-4">
معلومات الاتصال
{t('support.contactInfo')}
</h3>
<div className="space-y-4">
<div className="flex items-center gap-3">
@ -136,7 +137,7 @@ export default function SupportPage() {
<Mail className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="text-sm text-gray-500">البريد الإلكتروني</p>
<p className="text-sm text-gray-500">{t('support.email')}</p>
<p className="font-medium text-gray-900">
support@sweethome.com
</p>
@ -147,7 +148,7 @@ export default function SupportPage() {
<Phone className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-sm text-gray-500">رقم الهاتف</p>
<p className="text-sm text-gray-500">{t('support.phone')}</p>
<p className="font-medium text-gray-900" dir="ltr">
+963 11 234 5678
</p>
@ -158,8 +159,8 @@ export default function SupportPage() {
<MapPin className="w-5 h-5 text-amber-600" />
</div>
<div>
<p className="text-sm text-gray-500">العنوان</p>
<p className="font-medium text-gray-900">دمشق، سورية</p>
<p className="text-sm text-gray-500">{t('support.address')}</p>
<p className="font-medium text-gray-900">{t('support.addressValue')}</p>
</div>
</div>
</div>
@ -167,16 +168,16 @@ export default function SupportPage() {
<div className="bg-amber-50 rounded-2xl border border-amber-200 p-6">
<h3 className="text-lg font-bold text-amber-800 mb-2">
ساعات العمل
{t('support.workingHours')}
</h3>
<div className="space-y-2 text-amber-700">
<div className="flex justify-between">
<span>السبت - الخميس</span>
<span>{t('support.weekdays')}</span>
<span>9:00 - 18:00</span>
</div>
<div className="flex justify-between">
<span>الجمعة</span>
<span>مغلق</span>
<span>{t('support.friday')}</span>
<span>{t('support.closed')}</span>
</div>
</div>
</div>