Compare commits
80 Commits
5d593d593f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5592d9fbd2 | |||
| 4b53669a35 | |||
| 00dde588cb | |||
| 4924e7c2f3 | |||
| 9d3d02c88a | |||
| 1fa2fabb38 | |||
| 793ec51c9a | |||
| e3e9aa81a8 | |||
| 989a7f580f | |||
| f7afef03d4 | |||
| f09561ef00 | |||
| e42b0421bd | |||
| 9602154ba4 | |||
| 9b4f9979b6 | |||
| b9a4c6157a | |||
| 4d1b744516 | |||
| 7da4bf9f2b | |||
| 7973c15e3f | |||
| 4ebb75ddd9 | |||
| 30693c6495 | |||
| 491f10a1e0 | |||
| f91dfaacad | |||
| b4bb3ec279 | |||
| 3ee4a93365 | |||
| 689170ebce | |||
| 46ecf74572 | |||
| 66db009562 | |||
| 83a06e8007 | |||
| 4166a5d652 | |||
| ca6b12bfb9 | |||
| 2374e80380 | |||
| 747e42dc00 | |||
| 038df6397d | |||
| b5f5fabfc8 | |||
| 8ed623bbe2 | |||
| 47550dbfac | |||
| 7176031485 | |||
| 9a7e7fb5f8 | |||
| c01ada480d | |||
| c365503382 | |||
| bb47b05858 | |||
| 3bef951d5c | |||
| 2d2e2427a2 | |||
| 3b66bb53f1 | |||
| c7698741c7 | |||
| dab5b62fb7 | |||
| 3052209df8 | |||
| 409cb41dd6 | |||
| 3a3fa5f3fd | |||
| 03b39317c9 | |||
| dc64178f94 | |||
| 253e1b7542 | |||
| d7ef0977ad | |||
| 4f58519bac | |||
| df5801ee25 | |||
| 9dcbb940c6 | |||
| 12cb804b58 | |||
| 5738d0369e | |||
| c0fdc7ef03 | |||
| 389dca56d2 | |||
| 6e4d629cbe | |||
| 13b83e9aea | |||
| 4a9380313b | |||
| 8d3a745cc3 | |||
| 199478f237 | |||
| 61e527fab3 | |||
| d0b626ac8c | |||
| 792bed6250 | |||
| bf45a48504 | |||
| eaa4206b0b | |||
| c5d9ad7b70 | |||
| 46945464ae | |||
| 64d422ddd8 | |||
| 224c29bc19 | |||
| f892fc4a4d | |||
| 01ac4f8d6c | |||
| f2724a5cd2 | |||
| bef133ad5b | |||
| a9eb1cc684 | |||
| 13b563e35e |
@ -10,7 +10,7 @@ jobs:
|
|||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
repository: Rahaf/SweetHome
|
repository: amer2004/SweetHome
|
||||||
github-server-url: http://45.93.137.91:3000
|
github-server-url: http://45.93.137.91:3000
|
||||||
|
|
||||||
- name: Stopping server
|
- name: Stopping server
|
||||||
|
|||||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"i18n-ally-next.localesPaths": ["app/i18n"]
|
||||||
|
}
|
||||||
@ -5,9 +5,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { NavLink, MobileNavLink } from "./components/NavLinks";
|
import { NavLink, MobileNavLink } from "./components/NavLinks";
|
||||||
import { FavoritesProvider } from '@/app/contexts/FavoritesContext';
|
import BottomNav from "./components/BottomNav";
|
||||||
import { NotificationsProvider } from '@/app/contexts/NotificationsContext';
|
|
||||||
import BottomNav from './components/BottomNav';
|
|
||||||
import {
|
import {
|
||||||
Globe,
|
Globe,
|
||||||
LogIn,
|
LogIn,
|
||||||
@ -18,21 +16,14 @@ import {
|
|||||||
Building,
|
Building,
|
||||||
PlusCircle,
|
PlusCircle,
|
||||||
Heart,
|
Heart,
|
||||||
MessageCircle,
|
|
||||||
Settings,
|
Settings,
|
||||||
Edit,
|
|
||||||
Phone,
|
Phone,
|
||||||
Mail,
|
Mail,
|
||||||
MapPin,
|
|
||||||
Camera,
|
|
||||||
Bell,
|
Bell,
|
||||||
Home,
|
|
||||||
ChevronDown,
|
|
||||||
Menu,
|
Menu,
|
||||||
X,
|
X,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
CalendarDays,
|
CalendarDays,
|
||||||
Clock,
|
|
||||||
DollarSign,
|
DollarSign,
|
||||||
Star,
|
Star,
|
||||||
FileText,
|
FileText,
|
||||||
@ -44,6 +35,9 @@ import { UserRole, UserRoleLabels } from "./enums/UserRole";
|
|||||||
import "./i18n/config";
|
import "./i18n/config";
|
||||||
import NotificationHandler from "./components/NotificationHandler";
|
import NotificationHandler from "./components/NotificationHandler";
|
||||||
|
|
||||||
|
import { useNotifications } from "@/app/contexts/NotificationsContext";
|
||||||
|
import Loading from "./loading";
|
||||||
|
|
||||||
export default function ClientLayout({ children }) {
|
export default function ClientLayout({ children }) {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@ -55,6 +49,8 @@ export default function ClientLayout({ children }) {
|
|||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
const menuRef = useRef(null);
|
const menuRef = useRef(null);
|
||||||
|
|
||||||
|
const { unreadCount } = useNotifications();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsMounted(true);
|
setIsMounted(true);
|
||||||
const savedLanguage = localStorage.getItem("language") || "ar";
|
const savedLanguage = localStorage.getItem("language") || "ar";
|
||||||
@ -78,7 +74,7 @@ export default function ClientLayout({ children }) {
|
|||||||
name: authUser.name || authUser.email,
|
name: authUser.name || authUser.email,
|
||||||
email: authUser.email,
|
email: authUser.email,
|
||||||
phone: authUser.phone,
|
phone: authUser.phone,
|
||||||
role: AuthService.isOwner() ? UserRole.OWNER : UserRole.CUSTOMER,
|
role: AuthService.isOwner() ? UserRole.OWNER : AuthService.isAgent() ? UserRole.AGENT : UserRole.CUSTOMER,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setUser(null);
|
setUser(null);
|
||||||
@ -124,17 +120,14 @@ export default function ClientLayout({ children }) {
|
|||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
};
|
};
|
||||||
|
|
||||||
const isAuthPage = [
|
const isAuthPage = ["/login", "/blocked", "/register", "/forgot-password", "/auth/choose-role"].includes(pathname);
|
||||||
"/login",
|
|
||||||
"/blocked",
|
|
||||||
"/register",
|
|
||||||
"/forgot-password",
|
|
||||||
"/auth/choose-role",
|
|
||||||
].includes(pathname);
|
|
||||||
|
|
||||||
const isProfilePage = pathname === "/profile";
|
const isProfilePage = pathname === "/profile";
|
||||||
|
const isHomePage = pathname === "/";
|
||||||
|
|
||||||
const isOwner = user?.role === UserRole.OWNER;
|
const isOwner = user?.role === UserRole.OWNER;
|
||||||
|
const isAgent = user?.role === UserRole.AGENT;
|
||||||
|
const isOwnerOrAgent = isOwner || isAgent;
|
||||||
const isCustomer = user?.role === UserRole.CUSTOMER;
|
const isCustomer = user?.role === UserRole.CUSTOMER;
|
||||||
const isAuthenticated = !!user;
|
const isAuthenticated = !!user;
|
||||||
|
|
||||||
@ -147,114 +140,99 @@ export default function ClientLayout({ children }) {
|
|||||||
|
|
||||||
if (!isMounted) {
|
if (!isMounted) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
<Loading/>
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="min-h-screen flex flex-col">
|
||||||
{!isAuthPage && !isAuthenticated && (
|
{!isAuthPage && !isAuthenticated && (
|
||||||
<nav className="fixed top-0 left-0 right-0 bg-white/95 backdrop-blur-sm border-b border-gray-200 z-50 transition-all duration-300 shadow-sm">
|
<nav
|
||||||
|
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${isHomePage ? "bg-transparent backdrop-blur-sm" : "bg-white/95 backdrop-blur-sm border-b border-gray-200 shadow-sm"}`}
|
||||||
|
>
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div
|
<div className={`flex justify-between items-center h-20 ${currentLanguage === "ar" ? "flex-row-reverse" : ""}`}>
|
||||||
className={`flex justify-between items-center h-20 ${currentLanguage === "ar" ? "flex-row-reverse" : ""}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Link href="/" className="flex items-center space-x-3 group">
|
<Link href="/" className="flex items-center space-x-3 group">
|
||||||
<div className="relative w-[150px] h-[60px]">
|
<div className="relative w-[150px] h-[60px]">
|
||||||
<Image
|
<Image src="/logo.png" alt={t("logoAlt")} fill className="object-contain" priority />
|
||||||
src="/logo.png"
|
|
||||||
alt={t("logoAlt")}
|
|
||||||
fill
|
|
||||||
className="object-contain"
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<span className="text-3xl font-bold text-gray-800 hidden md:block">
|
<span className={`text-3xl font-bold hidden md:block ${isHomePage ? "text-white" : "text-gray-800"}`}>
|
||||||
{t("brandNamePart1")}
|
{t("brandNamePart1")}
|
||||||
<span className="text-amber-600">
|
<span className={isHomePage ? "text-amber-400" : "text-amber-600"}>{t("brandNamePart2")}</span>
|
||||||
{t("brandNamePart2")}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hidden md:flex items-center space-x-4">
|
<div className="hidden md:flex items-center space-x-4">
|
||||||
<div
|
<div className={`flex items-center space-x-1 ${currentLanguage === "ar" ? "flex-row-reverse space-x-reverse" : ""}`}>
|
||||||
className={`flex items-center space-x-1 ${currentLanguage === "ar" ? "flex-row-reverse space-x-reverse" : ""}`}
|
|
||||||
>
|
|
||||||
{/* Download App Dropdown */}
|
{/* Download App Dropdown */}
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<button className="flex items-center gap-2 px-3 py-2 text-gray-700 hover:text-amber-600 hover:bg-amber-50 rounded-lg transition-all">
|
<button
|
||||||
|
className={`flex items-center gap-2 px-3 py-2 rounded-lg transition-all ${isHomePage ? "text-white/80 hover:text-amber-300 hover:bg-white/10" : "text-gray-700 hover:text-amber-600 hover:bg-amber-50"}`}
|
||||||
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
|
||||||
<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="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"/>
|
<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">{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>
|
</svg>
|
||||||
<span className="text-sm font-semibold">تحميل التطبيق</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>
|
</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">
|
<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">
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
<a href="/files/SweetHome.apk" download
|
<a href="/files/SweetHome.apk" download className="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-green-50 transition-colors">
|
||||||
className="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-green-50 transition-colors">
|
|
||||||
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center">
|
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="#16a34a" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="#16a34a" 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"/>
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-semibold text-gray-900 text-sm">Android</p>
|
<p className="font-semibold text-gray-900 text-sm">{t("android")}</p>
|
||||||
<p className="text-xs text-green-600">تحميل APK</p>
|
<p className="text-xs text-green-600">{t("downloadAPK")}</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<div className="flex items-center gap-3 px-4 py-3 rounded-lg opacity-50 cursor-not-allowed">
|
<div className="flex items-center gap-3 px-4 py-3 rounded-lg opacity-50 cursor-not-allowed">
|
||||||
<div className="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center">
|
<div className="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#6b7280" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#6b7280" 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"/>
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-semibold text-gray-400 text-sm">iOS</p>
|
<p className="font-semibold text-gray-400 text-sm">{t("ios")}</p>
|
||||||
<p className="text-xs text-gray-400">قريباً</p>
|
<p className="text-xs text-gray-400">{t("comingSoon")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NavLink href="/">الرئيسية</NavLink>
|
<NavLink light={isHomePage} href="/">
|
||||||
<NavLink href="/properties">عقاراتنا</NavLink>
|
{t("home")}
|
||||||
|
</NavLink>
|
||||||
|
<NavLink light={isHomePage} href="/properties">
|
||||||
|
{t("ourProperties")}
|
||||||
|
</NavLink>
|
||||||
|
|
||||||
{isOwner && (
|
{isOwner && (
|
||||||
<>
|
<>
|
||||||
<NavLink href="/owner/properties">
|
<NavLink light={isHomePage} href="/owner/properties">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<Building className="w-4 h-4" />
|
<Building className="w-4 h-4" />
|
||||||
عقاراتي
|
{t("myProperties")}
|
||||||
</span>
|
</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<NavLink href="/owner/reservations">
|
<NavLink light={isHomePage} href="/owner/reservations">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<Calendar className="w-4 h-4" />
|
<Calendar className="w-4 h-4" />
|
||||||
الحجوزات
|
{t("reservations")}
|
||||||
</span>
|
</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
{/* <NavLink href="/owner/calendar">
|
<NavLink light={isHomePage} href="/owner/profits">
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
<CalendarDays className="w-4 h-4" />
|
|
||||||
التقويم
|
|
||||||
</span>
|
|
||||||
</NavLink> */}
|
|
||||||
<NavLink href="/owner/profits">
|
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<TrendingUp className="w-4 h-4" />
|
<TrendingUp className="w-4 h-4" />
|
||||||
الأرباح
|
{t("profits")}
|
||||||
</span>
|
</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</>
|
</>
|
||||||
@ -262,16 +240,16 @@ export default function ClientLayout({ children }) {
|
|||||||
|
|
||||||
{!user && !isAuthPage && (
|
{!user && !isAuthPage && (
|
||||||
<>
|
<>
|
||||||
<NavLink href="/login">
|
<NavLink light={isHomePage} href="/login">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<LogIn className="w-4 h-4" />
|
<LogIn className="w-4 h-4" />
|
||||||
تسجيل الدخول
|
{t("login")}
|
||||||
</span>
|
</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<NavLink href="/auth/choose-role">
|
<NavLink light={isHomePage} href="/auth/choose-role">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<UserPlus className="w-4 h-4" />
|
<UserPlus className="w-4 h-4" />
|
||||||
إنشاء حساب
|
{t("createAccount")}
|
||||||
</span>
|
</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</>
|
</>
|
||||||
@ -310,49 +288,29 @@ export default function ClientLayout({ children }) {
|
|||||||
>
|
>
|
||||||
<div className="bg-gradient-to-r from-amber-500 to-amber-600 p-4 text-white">
|
<div className="bg-gradient-to-r from-amber-500 to-amber-600 p-4 text-white">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-12 h-12 bg-white/20 rounded-full flex items-center justify-center text-xl font-bold">
|
<div className="w-12 h-12 bg-white/20 rounded-full flex items-center justify-center text-xl font-bold">{getUserInitial()}</div>
|
||||||
{getUserInitial()}
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<p className="font-bold">
|
<p className="font-bold">{user?.name || t("userLabel")}</p>
|
||||||
{user?.name || "مستخدم"}
|
<p className="text-xs text-amber-100">{user?.email || ""}</p>
|
||||||
</p>
|
<p className="text-xs text-amber-100 mt-1">{t(UserRoleLabels[user?.role] ? `userRole.${user?.role.toLowerCase()}` : "visitor")}</p>
|
||||||
<p className="text-xs text-amber-100">
|
|
||||||
{user?.email || ""}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-amber-100 mt-1">
|
|
||||||
{UserRoleLabels[user?.role] || 'زائر'}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
<Link
|
<Link href="/profile" className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors" onClick={() => setShowUserMenu(false)}>
|
||||||
href="/profile"
|
|
||||||
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
|
|
||||||
onClick={() => setShowUserMenu(false)}
|
|
||||||
>
|
|
||||||
<UserCircle className="w-5 h-5 text-amber-500" />
|
<UserCircle className="w-5 h-5 text-amber-500" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">الملف الشخصي</p>
|
<p className="font-medium">{t("profile")}</p>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">{t("viewAndEditInfo")}</p>
|
||||||
عرض وتعديل معلوماتك
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link href="/favorites" className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors" onClick={() => setShowUserMenu(false)}>
|
||||||
href="/favorites"
|
|
||||||
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
|
|
||||||
onClick={() => setShowUserMenu(false)}
|
|
||||||
>
|
|
||||||
<Heart className="w-5 h-5 text-amber-500" />
|
<Heart className="w-5 h-5 text-amber-500" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">المفضلة</p>
|
<p className="font-medium">{t("favorites")}</p>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">{t("savedProperties")}</p>
|
||||||
العقارات المحفوظة
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
@ -363,38 +321,24 @@ export default function ClientLayout({ children }) {
|
|||||||
>
|
>
|
||||||
<CalendarDays className="w-5 h-5 text-amber-500" />
|
<CalendarDays className="w-5 h-5 text-amber-500" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">عقاراتي المحجوزة</p>
|
<p className="font-medium">{t("myBookedProperties")}</p>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">{t("viewAndRateBooked")}</p>
|
||||||
عرض وتقييم العقارات المحجوزة
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link href="/my-rates" className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors" onClick={() => setShowUserMenu(false)}>
|
||||||
href="/my-rates"
|
|
||||||
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
|
|
||||||
onClick={() => setShowUserMenu(false)}
|
|
||||||
>
|
|
||||||
<Star className="w-5 h-5 text-amber-500" />
|
<Star className="w-5 h-5 text-amber-500" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">تقييماتي</p>
|
<p className="font-medium">{t("myRatings")}</p>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">{t("ratingsReceived")}</p>
|
||||||
التقييمات التي تلقيتها
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link href="/settings" className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors" onClick={() => setShowUserMenu(false)}>
|
||||||
href="/settings"
|
|
||||||
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
|
|
||||||
onClick={() => setShowUserMenu(false)}
|
|
||||||
>
|
|
||||||
<Settings className="w-5 h-5 text-amber-500" />
|
<Settings className="w-5 h-5 text-amber-500" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">الإعدادات</p>
|
<p className="font-medium">{t("settings")}</p>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">{t("accountAndSecurity")}</p>
|
||||||
إعدادات الحساب والأمان
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
@ -409,10 +353,8 @@ export default function ClientLayout({ children }) {
|
|||||||
>
|
>
|
||||||
<Building className="w-5 h-5 text-amber-500" />
|
<Building className="w-5 h-5 text-amber-500" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">عقاراتي</p>
|
<p className="font-medium">{t("myProperties")}</p>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">{t("manageYourProperties")}</p>
|
||||||
إدارة عقاراتك
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
@ -423,10 +365,8 @@ export default function ClientLayout({ children }) {
|
|||||||
>
|
>
|
||||||
<PlusCircle className="w-5 h-5 text-amber-500" />
|
<PlusCircle className="w-5 h-5 text-amber-500" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">إضافة عقار</p>
|
<p className="font-medium">{t("addProperty")}</p>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">{t("addNewProperty")}</p>
|
||||||
أضف عقاراً جديداً
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
@ -437,10 +377,8 @@ export default function ClientLayout({ children }) {
|
|||||||
>
|
>
|
||||||
<Calendar className="w-5 h-5 text-amber-500" />
|
<Calendar className="w-5 h-5 text-amber-500" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">الحجوزات</p>
|
<p className="font-medium">{t("reservations")}</p>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">{t("manageReservations")}</p>
|
||||||
إدارة حجوزاتك
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
@ -451,10 +389,8 @@ export default function ClientLayout({ children }) {
|
|||||||
>
|
>
|
||||||
<CalendarDays className="w-5 h-5 text-amber-500" />
|
<CalendarDays className="w-5 h-5 text-amber-500" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">التقويم</p>
|
<p className="font-medium">{t("calendar")}</p>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">{t("propertyAvailability")}</p>
|
||||||
جدول توفر العقارات
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
@ -465,10 +401,8 @@ export default function ClientLayout({ children }) {
|
|||||||
>
|
>
|
||||||
<TrendingUp className="w-5 h-5 text-amber-500" />
|
<TrendingUp className="w-5 h-5 text-amber-500" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">الأرباح</p>
|
<p className="font-medium">{t("profits")}</p>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">{t("statisticsAndProfits")}</p>
|
||||||
إحصائيات وأرباح
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
@ -479,10 +413,8 @@ export default function ClientLayout({ children }) {
|
|||||||
>
|
>
|
||||||
<DollarSign className="w-5 h-5 text-amber-500" />
|
<DollarSign className="w-5 h-5 text-amber-500" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">دفتر الحسابات</p>
|
<p className="font-medium">{t("accountBook")}</p>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">{t("financialRecords")}</p>
|
||||||
سجل المعاملات المالية
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
@ -499,10 +431,8 @@ export default function ClientLayout({ children }) {
|
|||||||
>
|
>
|
||||||
<Calendar className="w-5 h-5 text-amber-500" />
|
<Calendar className="w-5 h-5 text-amber-500" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">حجوزاتي</p>
|
<p className="font-medium">{t("myBookings")}</p>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">{t("viewBookings")}</p>
|
||||||
عرض حجوزاتك
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
@ -513,10 +443,8 @@ export default function ClientLayout({ children }) {
|
|||||||
>
|
>
|
||||||
<CalendarDays className="w-5 h-5 text-amber-500" />
|
<CalendarDays className="w-5 h-5 text-amber-500" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">عقاراتي المحجوزة</p>
|
<p className="font-medium">{t("myBookedProperties")}</p>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">{t("viewAndRateBooked")}</p>
|
||||||
عرض وتقييم العقارات المحجوزة
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
@ -527,24 +455,16 @@ export default function ClientLayout({ children }) {
|
|||||||
>
|
>
|
||||||
<Star className="w-5 h-5 text-amber-500" />
|
<Star className="w-5 h-5 text-amber-500" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">تقييماتي</p>
|
<p className="font-medium">{t("myRatings")}</p>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">{t("ratingsReceived")}</p>
|
||||||
التقييمات التي تلقيتها
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link href="/reports" className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors" onClick={() => setShowUserMenu(false)}>
|
||||||
href="/reports"
|
|
||||||
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
|
|
||||||
onClick={() => setShowUserMenu(false)}
|
|
||||||
>
|
|
||||||
<FileText className="w-5 h-5 text-amber-500" />
|
<FileText className="w-5 h-5 text-amber-500" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">البلاغات</p>
|
<p className="font-medium">{t("reports")}</p>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">{t("submitAndTrackReports")}</p>
|
||||||
تقديم ومتابعة البلاغات
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
@ -552,16 +472,11 @@ export default function ClientLayout({ children }) {
|
|||||||
|
|
||||||
<div className="border-t border-gray-100 my-2"></div>
|
<div className="border-t border-gray-100 my-2"></div>
|
||||||
|
|
||||||
<button
|
<button onClick={logout} className="w-full flex items-center gap-3 px-4 py-3 text-red-600 hover:bg-red-50 rounded-lg transition-colors">
|
||||||
onClick={logout}
|
|
||||||
className="w-full flex items-center gap-3 px-4 py-3 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
<LogOut className="w-5 h-5" />
|
<LogOut className="w-5 h-5" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">تسجيل الخروج</p>
|
<p className="font-medium">{t("logout")}</p>
|
||||||
<p className="text-xs text-red-400">
|
<p className="text-xs text-red-400">{t("endSession")}</p>
|
||||||
إنهاء الجلسة الحالية
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -576,9 +491,7 @@ export default function ClientLayout({ children }) {
|
|||||||
<motion.button
|
<motion.button
|
||||||
whileHover={{ scale: 1.1, rotate: 360 }}
|
whileHover={{ scale: 1.1, rotate: 360 }}
|
||||||
whileTap={{ scale: 0.9 }}
|
whileTap={{ scale: 0.9 }}
|
||||||
onClick={() =>
|
onClick={() => changeLanguage(currentLanguage === "en" ? "ar" : "en")}
|
||||||
changeLanguage(currentLanguage === "en" ? "ar" : "en")
|
|
||||||
}
|
|
||||||
className="flex items-center justify-center w-10 h-10 bg-gray-100 hover:bg-gray-200 rounded-full transition-colors"
|
className="flex items-center justify-center w-10 h-10 bg-gray-100 hover:bg-gray-200 rounded-full transition-colors"
|
||||||
>
|
>
|
||||||
<Globe className="w-5 h-5 text-gray-700" />
|
<Globe className="w-5 h-5 text-gray-700" />
|
||||||
@ -599,11 +512,7 @@ export default function ClientLayout({ children }) {
|
|||||||
className="inline-flex items-center justify-center p-2 rounded-md text-gray-700 hover:text-amber-600 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-amber-500"
|
className="inline-flex items-center justify-center p-2 rounded-md text-gray-700 hover:text-amber-600 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-amber-500"
|
||||||
>
|
>
|
||||||
<span className="sr-only">{t("openMainMenu")}</span>
|
<span className="sr-only">{t("openMainMenu")}</span>
|
||||||
{isMobileMenuOpen ? (
|
{isMobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
|
||||||
<X className="w-6 h-6" />
|
|
||||||
) : (
|
|
||||||
<Menu className="w-6 h-6" />
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -614,70 +523,66 @@ export default function ClientLayout({ children }) {
|
|||||||
initial={{ opacity: 0, y: -10 }}
|
initial={{ opacity: 0, y: -10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, y: -10 }}
|
exit={{ opacity: 0, y: -10 }}
|
||||||
className="md:hidden bg-white border-t border-gray-200 shadow-lg"
|
className={`md:hidden border-t shadow-lg ${isHomePage ? "bg-black/60 backdrop-blur-md border-white/10" : "bg-white border-gray-200"}`}
|
||||||
>
|
>
|
||||||
<div className="px-2 pt-2 pb-3 space-y-1">
|
<div className="px-2 pt-2 pb-3 space-y-1">
|
||||||
<MobileNavLink href="/" onClick={closeMobileMenu}>
|
<MobileNavLink light={isHomePage} href="/" onClick={closeMobileMenu}>
|
||||||
{t("home")}
|
{t("home")}
|
||||||
</MobileNavLink>
|
</MobileNavLink>
|
||||||
<MobileNavLink href="/properties" onClick={closeMobileMenu}>
|
<MobileNavLink light={isHomePage} href="/properties" onClick={closeMobileMenu}>
|
||||||
{t("ourProducts")}
|
{t("ourProducts")}
|
||||||
</MobileNavLink>
|
</MobileNavLink>
|
||||||
|
|
||||||
{/* Download App - Mobile */}
|
{/* Download App - Mobile */}
|
||||||
<div className="border-t border-gray-200 my-2"></div>
|
<div className={`border-t my-2 ${isHomePage ? "border-white/10" : "border-gray-200"}`}></div>
|
||||||
<p className="px-3 py-1 text-xs text-gray-400 font-medium">تحميل التطبيق</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}
|
<a
|
||||||
className="flex items-center gap-2 px-3 py-2 rounded-md text-green-600 hover:bg-green-50 transition-colors">
|
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">
|
<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"/>
|
<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>
|
</svg>
|
||||||
<span className="text-sm font-medium">Android - تحميل APK</span>
|
<span className="text-sm font-medium">
|
||||||
|
{t("android")} - {t("downloadAPK")}
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<div className="flex items-center gap-2 px-3 py-2 rounded-md text-gray-400 cursor-not-allowed opacity-50">
|
<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">
|
<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"/>
|
<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>
|
</svg>
|
||||||
<span className="text-sm">iOS - قريباً</span>
|
<span className="text-sm">
|
||||||
|
{t("ios")} - {t("comingSoon")}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t border-gray-200 my-2"></div>
|
<div className={`border-t my-2 ${isHomePage ? "border-white/10" : "border-gray-200"}`}></div>
|
||||||
|
|
||||||
{isOwner && (
|
{isOwner && (
|
||||||
<>
|
<>
|
||||||
<MobileNavLink
|
<MobileNavLink light={isHomePage} href="/owner/properties" onClick={closeMobileMenu}>
|
||||||
href="/owner/properties"
|
|
||||||
onClick={closeMobileMenu}
|
|
||||||
>
|
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<Building className="w-4 h-4" />
|
<Building className="w-4 h-4" />
|
||||||
عقاراتي
|
{t("myProperties")}
|
||||||
</span>
|
</span>
|
||||||
</MobileNavLink>
|
</MobileNavLink>
|
||||||
<MobileNavLink
|
<MobileNavLink light={isHomePage} href="/owner/reservations" onClick={closeMobileMenu}>
|
||||||
href="/owner/reservations"
|
|
||||||
onClick={closeMobileMenu}
|
|
||||||
>
|
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<Calendar className="w-4 h-4" />
|
<Calendar className="w-4 h-4" />
|
||||||
الحجوزات
|
{t("reservations")}
|
||||||
</span>
|
</span>
|
||||||
</MobileNavLink>
|
</MobileNavLink>
|
||||||
<MobileNavLink
|
<MobileNavLink light={isHomePage} href="/owner/calendar" onClick={closeMobileMenu}>
|
||||||
href="/owner/calendar"
|
|
||||||
onClick={closeMobileMenu}
|
|
||||||
>
|
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<CalendarDays className="w-4 h-4" />
|
<CalendarDays className="w-4 h-4" />
|
||||||
التقويم
|
{t("calendar")}
|
||||||
</span>
|
</span>
|
||||||
</MobileNavLink>
|
</MobileNavLink>
|
||||||
<MobileNavLink
|
<MobileNavLink light={isHomePage} href="/owner/profits" onClick={closeMobileMenu}>
|
||||||
href="/owner/profits"
|
|
||||||
onClick={closeMobileMenu}
|
|
||||||
>
|
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<TrendingUp className="w-4 h-4" />
|
<TrendingUp className="w-4 h-4" />
|
||||||
الأرباح
|
{t("profits")}
|
||||||
</span>
|
</span>
|
||||||
</MobileNavLink>
|
</MobileNavLink>
|
||||||
</>
|
</>
|
||||||
@ -685,20 +590,17 @@ export default function ClientLayout({ children }) {
|
|||||||
|
|
||||||
{!user && (
|
{!user && (
|
||||||
<>
|
<>
|
||||||
<div className="border-t border-gray-200 my-2"></div>
|
<div className={`border-t my-2 ${isHomePage ? "border-white/10" : "border-gray-200"}`}></div>
|
||||||
<MobileNavLink href="/login" onClick={closeMobileMenu}>
|
<MobileNavLink light={isHomePage} href="/login" onClick={closeMobileMenu}>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<LogIn className="w-4 h-4" />
|
<LogIn className="w-4 h-4" />
|
||||||
تسجيل الدخول
|
{t("login")}
|
||||||
</span>
|
</span>
|
||||||
</MobileNavLink>
|
</MobileNavLink>
|
||||||
<MobileNavLink
|
<MobileNavLink light={isHomePage} href="/auth/choose-role" onClick={closeMobileMenu}>
|
||||||
href="/auth/choose-role"
|
|
||||||
onClick={closeMobileMenu}
|
|
||||||
>
|
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<UserPlus className="w-4 h-4" />
|
<UserPlus className="w-4 h-4" />
|
||||||
إنشاء حساب
|
{t("createAccount")}
|
||||||
</span>
|
</span>
|
||||||
</MobileNavLink>
|
</MobileNavLink>
|
||||||
</>
|
</>
|
||||||
@ -709,68 +611,105 @@ export default function ClientLayout({ children }) {
|
|||||||
</nav>
|
</nav>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<NotificationsProvider>
|
{isHomePage && (
|
||||||
<FavoritesProvider>
|
<div className="fixed inset-0 -z-10">
|
||||||
<main
|
<motion.div
|
||||||
className={`${!isAuthPage && !isProfilePage && !isAuthenticated ? "pt-20" : ""} min-h-screen bg-gradient-to-b from-gray-50 to-white ${currentLanguage === "ar" ? "text-right" : "text-left"}`}
|
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||||
>
|
style={{ backgroundImage: "url(/hero.jpg)" }}
|
||||||
{children}
|
initial={{ scale: 1.1 }}
|
||||||
</main>
|
animate={{ scale: 1 }}
|
||||||
|
transition={{ duration: 1.5, ease: "easeOut" }}
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-black/70 via-black/60 to-black/50" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{isAuthenticated && !isAuthPage && (
|
<main
|
||||||
<BottomNav isOwner={isOwner} />
|
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" : ""}`}
|
||||||
)}
|
>
|
||||||
</FavoritesProvider>
|
{!isAuthPage && isAuthenticated && (
|
||||||
</NotificationsProvider>
|
<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 ? 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"}`}
|
||||||
|
>
|
||||||
|
<Bell className="w-5 h-5" />
|
||||||
|
{isMounted && unreadCount > 0 && (
|
||||||
|
<div className="absolute -top-1 -right-1 bg-red-500 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center">{unreadCount > 9 ? "9+" : unreadCount}</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={() => 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>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex-1">{children}</div>
|
||||||
|
{isAuthenticated && !isAuthPage && <BottomNav isOwner={isOwner} isOwnerOrAgent={isOwnerOrAgent} />}
|
||||||
|
</main>
|
||||||
|
|
||||||
{pathname === "/" && (
|
{pathname === "/" && (
|
||||||
<footer className="bg-gray-900 text-white py-12">
|
<footer className="py-20" style={{backgroundColor:"#0f172a"}}>
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div
|
<div className={`grid grid-cols-1 md:grid-cols-4 gap-8 ${currentLanguage === "ar" ? "text-right" : "text-left"}`}>
|
||||||
className={`grid grid-cols-1 md:grid-cols-4 gap-8 ${currentLanguage === "ar" ? "text-right" : "text-left"}`}
|
|
||||||
>
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div
|
<div className={`flex items-center ${currentLanguage === "ar" ? "flex-row-reverse" : "space-x-3"}`}>
|
||||||
className={`flex items-center ${currentLanguage === "ar" ? "flex-row-reverse" : "space-x-3"}`}
|
|
||||||
>
|
|
||||||
<div className="relative w-10 h-10">
|
<div className="relative w-10 h-10">
|
||||||
<Image
|
<Image src="/logo.png" alt={t("logoAlt")} fill className="object-contain" sizes="40px" />
|
||||||
src="/logo.png"
|
|
||||||
alt={t("logoAlt")}
|
|
||||||
fill
|
|
||||||
className="object-contain"
|
|
||||||
sizes="40px"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<span className="text-3xl font-bold">
|
<span className="text-3xl font-bold">
|
||||||
{t("brandNamePart1")}
|
{t("brandNamePart1")}
|
||||||
<span className="text-amber-400">
|
<span className="text-amber-400">{t("brandNamePart2")}</span>
|
||||||
{t("brandNamePart2")}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-400 text-sm">
|
<p className="text-gray-400 text-sm">{t("footerDescription")}</p>
|
||||||
{t("footerDescription")}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold mb-4">
|
<h3 className="text-lg font-semibold mb-4">{t("quickLinks")}</h3>
|
||||||
{t("quickLinks")}
|
|
||||||
</h3>
|
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
<li>
|
<li>
|
||||||
<Link
|
<Link href="/" className="text-gray-400 hover:text-white transition-colors block py-1">
|
||||||
href="/"
|
|
||||||
className="text-gray-400 hover:text-white transition-colors block py-1"
|
|
||||||
>
|
|
||||||
{t("home")}
|
{t("home")}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link
|
<Link href="/properties" className="text-gray-400 hover:text-white transition-colors block py-1">
|
||||||
href="/properties"
|
|
||||||
className="text-gray-400 hover:text-white transition-colors block py-1"
|
|
||||||
>
|
|
||||||
{t("ourProducts")}
|
{t("ourProducts")}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
@ -781,7 +720,9 @@ export default function ClientLayout({ children }) {
|
|||||||
<ul className="space-y-3 text-gray-400">
|
<ul className="space-y-3 text-gray-400">
|
||||||
<li className="flex items-center gap-2">
|
<li className="flex items-center gap-2">
|
||||||
<Phone className="w-5 h-5" />
|
<Phone className="w-5 h-5" />
|
||||||
<span dir="ltr" className="text-right">{t("phone")}</span>
|
<span dir="ltr" className="text-right">
|
||||||
|
{t("phone")}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-center gap-2">
|
<li className="flex items-center gap-2">
|
||||||
<Mail className="w-5 h-5" />
|
<Mail className="w-5 h-5" />
|
||||||
@ -799,6 +740,6 @@ export default function ClientLayout({ children }) {
|
|||||||
</footer>
|
</footer>
|
||||||
)}
|
)}
|
||||||
<NotificationHandler />
|
<NotificationHandler />
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
57
app/HelperFunction/ApiProperty.js
Normal file
57
app/HelperFunction/ApiProperty.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from "../enums";
|
||||||
|
export function mapApiProperty(t, item, index) {
|
||||||
|
const info = item.propertyInformation || {};
|
||||||
|
const dailyPrice = item.dailyRent ?? 0;
|
||||||
|
const monthlyPrice = item.monthlyRent ?? 0;
|
||||||
|
const salePrice = item.price ?? 0;
|
||||||
|
const isRentListing = Boolean(item.dailyRent != null || item.monthlyRent != null);
|
||||||
|
const price = isRentListing ? dailyPrice || monthlyPrice || 0 : salePrice;
|
||||||
|
const priceUnit = isRentListing ? (monthlyPrice ? "monthly" : "daily") : "sale";
|
||||||
|
const propType = BuildingTypeKeys[info.buildingType] ?? BuildingTypeKeys[item.type] ?? (item.type || "apartment");
|
||||||
|
const status = PropertyStatusKeys[info.status] ?? PropertyStatusKeys[item.status] ?? "available";
|
||||||
|
const features = [];
|
||||||
|
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 : [];
|
||||||
|
const images = rawImages.length > 0 ? rawImages.map((img) => (img.startsWith("http") ? img : `${apiBase}${img.startsWith("/") ? "" : "/Pictures/"}${img}`)) : ["/property-placeholder.jpg"];
|
||||||
|
const ownerSource = info.ownerType == null && item.ownerType == null ? "all" : [info.ownerType, item.ownerType].find((value) => value != null) === 1 ? "agency" : "owner";
|
||||||
|
return {
|
||||||
|
id: item.id ?? index + 1,
|
||||||
|
title: info.address || t("propertyWithId", { id: item.id || index + 1 }),
|
||||||
|
description: info.description || "",
|
||||||
|
type: propType,
|
||||||
|
price: price,
|
||||||
|
priceUSD: price,
|
||||||
|
priceUnit,
|
||||||
|
listingType: isRentListing ? "rent" : "sale",
|
||||||
|
location: {
|
||||||
|
city: extractCity(info.address) || "damascus",
|
||||||
|
district: info.address || "",
|
||||||
|
address: info.address || "",
|
||||||
|
lat: parseFloat(info.cordsX) || 0,
|
||||||
|
lng: parseFloat(info.cordsY) || 0,
|
||||||
|
},
|
||||||
|
bedrooms: info.numberOfBedRooms || 0,
|
||||||
|
bathrooms: info.numberOfBathRooms || 0,
|
||||||
|
area: info.space || 0,
|
||||||
|
features,
|
||||||
|
images,
|
||||||
|
status,
|
||||||
|
rating: item.rating || 4.5,
|
||||||
|
isNew: false,
|
||||||
|
allowedIdentities: ["syrian", "passport"],
|
||||||
|
priceDisplay: {
|
||||||
|
daily: dailyPrice,
|
||||||
|
monthly: monthlyPrice,
|
||||||
|
sale: salePrice,
|
||||||
|
},
|
||||||
|
ownerSource,
|
||||||
|
bookings: [],
|
||||||
|
_raw: item,
|
||||||
|
};
|
||||||
|
}
|
||||||
20
app/HelperFunction/UploadImage.js
Normal file
20
app/HelperFunction/UploadImage.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
|
export const handleImageUpload = (side, file,t,setIdImagePreviews,setIdImages) => {
|
||||||
|
if (!file) return;
|
||||||
|
if (!file.type.startsWith("image/")) {
|
||||||
|
toast.error(t("register.selectValidImage"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
|
toast.error(t("register.imageSizeLimit"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = () => {
|
||||||
|
setIdImagePreviews((prev) => ({ ...prev, [side]: reader.result }));
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
setIdImages((prev) => ({ ...prev, [side]: file }));
|
||||||
|
toast.success(t("register.imageUploadSuccess"), { style: { background: "#dcfce7", color: "#166534" } });
|
||||||
|
};
|
||||||
@ -1,31 +1,24 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from "next/navigation";
|
||||||
import Link from 'next/link';
|
import Link from "next/link";
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from "framer-motion";
|
||||||
import {
|
import { useTranslation } from "react-i18next";
|
||||||
Mail,
|
import { Mail, Phone, Shield, ChevronLeft, Loader2, CheckCircle, XCircle, Send, Key } from "lucide-react";
|
||||||
Phone,
|
import toast, { Toaster } from "react-hot-toast";
|
||||||
Shield,
|
import AuthService from "../services/AuthService";
|
||||||
ChevronLeft,
|
import { sendEmailOTP, sendPhoneOTP, verifyEmail, verifyPhone } from "../utils/api";
|
||||||
Loader2,
|
import InteractiveBackground from "../components/Animation/Background";
|
||||||
CheckCircle,
|
|
||||||
XCircle,
|
|
||||||
Send,
|
|
||||||
Key
|
|
||||||
} from 'lucide-react';
|
|
||||||
import toast, { Toaster } from 'react-hot-toast';
|
|
||||||
import AuthService from '../services/AuthService';
|
|
||||||
import { sendEmailOTP, sendPhoneOTP, verifyEmail, verifyPhone } from '../utils/api';
|
|
||||||
|
|
||||||
export default function AccountVerificationPage() {
|
export default function AccountVerificationPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { t } = useTranslation();
|
||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
const [emailCode, setEmailCode] = useState('');
|
const [emailCode, setEmailCode] = useState("");
|
||||||
const [phoneCode, setPhoneCode] = useState('');
|
const [phoneCode, setPhoneCode] = useState("");
|
||||||
const [sendingEmailOTP, setSendingEmailOTP] = useState(false);
|
const [sendingEmailOTP, setSendingEmailOTP] = useState(false);
|
||||||
const [sendingPhoneOTP, setSendingPhoneOTP] = useState(false);
|
const [sendingPhoneOTP, setSendingPhoneOTP] = useState(false);
|
||||||
const [verifyingEmail, setVerifyingEmail] = useState(false);
|
const [verifyingEmail, setVerifyingEmail] = useState(false);
|
||||||
@ -41,7 +34,7 @@ export default function AccountVerificationPage() {
|
|||||||
setUser(authUser);
|
setUser(authUser);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
} else {
|
} else {
|
||||||
router.push('/login');
|
router.push("/login");
|
||||||
}
|
}
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
@ -49,10 +42,10 @@ export default function AccountVerificationPage() {
|
|||||||
setSendingEmailOTP(true);
|
setSendingEmailOTP(true);
|
||||||
try {
|
try {
|
||||||
await sendEmailOTP();
|
await sendEmailOTP();
|
||||||
toast.success('تم إرسال رمز التحقق إلى بريدك الإلكتروني');
|
toast.success(t("accountVerification.emailOtpSent"));
|
||||||
setShowEmailInput(true);
|
setShowEmailInput(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err.message || 'فشل إرسال رمز التحقق');
|
toast.error(err.message || t("accountVerification.otpSendFailed"));
|
||||||
} finally {
|
} finally {
|
||||||
setSendingEmailOTP(false);
|
setSendingEmailOTP(false);
|
||||||
}
|
}
|
||||||
@ -60,7 +53,7 @@ export default function AccountVerificationPage() {
|
|||||||
|
|
||||||
const handleVerifyEmail = async () => {
|
const handleVerifyEmail = async () => {
|
||||||
if (!emailCode.trim()) {
|
if (!emailCode.trim()) {
|
||||||
toast.error('الرجاء إدخال رمز التحقق');
|
toast.error(t("accountVerification.otpRequired"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setVerifyingEmail(true);
|
setVerifyingEmail(true);
|
||||||
@ -68,9 +61,9 @@ export default function AccountVerificationPage() {
|
|||||||
await verifyEmail(emailCode.trim());
|
await verifyEmail(emailCode.trim());
|
||||||
setEmailVerified(true);
|
setEmailVerified(true);
|
||||||
setShowEmailInput(false);
|
setShowEmailInput(false);
|
||||||
toast.success('تم التحقق من البريد الإلكتروني بنجاح');
|
toast.success(t("accountVerification.emailVerifiedSuccess"));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err.message || 'رمز التحقق غير صحيح');
|
toast.error(err.message || t("accountVerification.otpInvalid"));
|
||||||
} finally {
|
} finally {
|
||||||
setVerifyingEmail(false);
|
setVerifyingEmail(false);
|
||||||
}
|
}
|
||||||
@ -80,10 +73,10 @@ export default function AccountVerificationPage() {
|
|||||||
setSendingPhoneOTP(true);
|
setSendingPhoneOTP(true);
|
||||||
try {
|
try {
|
||||||
await sendPhoneOTP();
|
await sendPhoneOTP();
|
||||||
toast.success('تم إرسال رمز التحقق إلى هاتفك');
|
toast.success(t("accountVerification.phoneOtpSent"));
|
||||||
setShowPhoneInput(true);
|
setShowPhoneInput(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err.message || 'فشل إرسال رمز التحقق');
|
toast.error(err.message || t("accountVerification.otpSendFailed"));
|
||||||
} finally {
|
} finally {
|
||||||
setSendingPhoneOTP(false);
|
setSendingPhoneOTP(false);
|
||||||
}
|
}
|
||||||
@ -91,7 +84,7 @@ export default function AccountVerificationPage() {
|
|||||||
|
|
||||||
const handleVerifyPhone = async () => {
|
const handleVerifyPhone = async () => {
|
||||||
if (!phoneCode.trim()) {
|
if (!phoneCode.trim()) {
|
||||||
toast.error('الرجاء إدخال رمز التحقق');
|
toast.error(t("accountVerification.otpRequired"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setVerifyingPhone(true);
|
setVerifyingPhone(true);
|
||||||
@ -99,80 +92,56 @@ export default function AccountVerificationPage() {
|
|||||||
await verifyPhone(phoneCode.trim());
|
await verifyPhone(phoneCode.trim());
|
||||||
setPhoneVerified(true);
|
setPhoneVerified(true);
|
||||||
setShowPhoneInput(false);
|
setShowPhoneInput(false);
|
||||||
toast.success('تم التحقق من رقم الهاتف بنجاح');
|
toast.success(t("accountVerification.phoneVerifiedSuccess"));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err.message || 'رمز التحقق غير صحيح');
|
toast.error(err.message || t("accountVerification.otpInvalid"));
|
||||||
} finally {
|
} finally {
|
||||||
setVerifyingPhone(false);
|
setVerifyingPhone(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
|
||||||
<Loader2 className="w-12 h-12 text-amber-500 animate-spin" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const containerVariants = {
|
const containerVariants = {
|
||||||
hidden: { opacity: 0 },
|
hidden: { opacity: 0 },
|
||||||
visible: {
|
visible: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
transition: { staggerChildren: 0.1 }
|
transition: { staggerChildren: 0.1 },
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const itemVariants = {
|
const itemVariants = {
|
||||||
hidden: { opacity: 0, y: 20 },
|
hidden: { opacity: 0, y: 20 },
|
||||||
visible: { opacity: 1, y: 0 }
|
visible: { opacity: 1, y: 0 },
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">
|
<div className="min-h-screen py-8" dir="rtl">
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
<InteractiveBackground />
|
||||||
<div className="container mx-auto px-4 max-w-2xl">
|
<div className="relative z-10 container mx-auto px-4 max-w-2xl">
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="flex items-center gap-3 mb-8">
|
||||||
initial={{ opacity: 0, y: -20 }}
|
<Link href="/settings" className="p-2 rounded-xl hover:bg-gray-200 transition-colors text-gray-600">
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="flex items-center gap-3 mb-8"
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href="/settings"
|
|
||||||
className="p-2 rounded-xl hover:bg-gray-200 transition-colors text-gray-600"
|
|
||||||
>
|
|
||||||
<ChevronLeft className="w-5 h-5" />
|
<ChevronLeft className="w-5 h-5" />
|
||||||
</Link>
|
</Link>
|
||||||
<h1 className="text-2xl font-bold text-gray-900">التحقق من الحساب</h1>
|
<h1 className="text-2xl font-bold text-gray-900">{t("accountVerification.title")}</h1>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div variants={containerVariants} initial="hidden" animate="visible" className="space-y-6">
|
||||||
variants={containerVariants}
|
|
||||||
initial="hidden"
|
|
||||||
animate="visible"
|
|
||||||
className="space-y-6"
|
|
||||||
>
|
|
||||||
<motion.div variants={itemVariants} className="bg-white rounded-2xl shadow-sm overflow-hidden">
|
<motion.div variants={itemVariants} className="bg-white rounded-2xl shadow-sm overflow-hidden">
|
||||||
<div className="px-6 py-4 border-b border-gray-100 flex items-center gap-3">
|
<div className="px-6 py-4 border-b border-gray-100 flex items-center gap-3">
|
||||||
<Mail className="w-5 h-5 text-amber-600" />
|
<Mail className="w-5 h-5 text-amber-600" />
|
||||||
<h2 className="text-lg font-semibold text-gray-800">البريد الإلكتروني</h2>
|
<h2 className="text-lg font-semibold text-gray-800">{t("accountVerification.emailSection")}</h2>
|
||||||
{emailVerified && (
|
{emailVerified && (
|
||||||
<span className="flex items-center gap-1 text-xs text-green-600 bg-green-50 px-2 py-0.5 rounded-full">
|
<span className="flex items-center gap-1 text-xs text-green-600 bg-green-50 px-2 py-0.5 rounded-full">
|
||||||
<CheckCircle className="w-3 h-3" />
|
<CheckCircle className="w-3 h-3" />
|
||||||
موثق
|
{t("accountVerification.verified")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600">{user?.email || 'بريد إلكتروني غير مسجل'}</p>
|
<p className="text-sm text-gray-600">{user?.email || t("accountVerification.emailNotRegistered")}</p>
|
||||||
<p className="text-xs text-gray-400 mt-1">
|
<p className="text-xs text-gray-400 mt-1">{emailVerified ? t("accountVerification.emailVerifiedDesc") : t("accountVerification.emailVerifyDesc")}</p>
|
||||||
{emailVerified
|
|
||||||
? 'بريدك الإلكتروني موثق ويمكنك استخدامه لتسجيل الدخول'
|
|
||||||
: 'يرجى توثيق بريدك الإلكتروني لحماية حسابك'}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
{!emailVerified && !showEmailInput && (
|
{!emailVerified && !showEmailInput && (
|
||||||
<button
|
<button
|
||||||
@ -180,29 +149,21 @@ export default function AccountVerificationPage() {
|
|||||||
disabled={sendingEmailOTP}
|
disabled={sendingEmailOTP}
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-amber-500 text-white rounded-xl hover:bg-amber-600 transition-colors text-sm disabled:opacity-50"
|
className="flex items-center gap-2 px-4 py-2 bg-amber-500 text-white rounded-xl hover:bg-amber-600 transition-colors text-sm disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{sendingEmailOTP ? (
|
{sendingEmailOTP ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />}
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
{sendingEmailOTP ? t("accountVerification.sending") : t("accountVerification.sendOtp")}
|
||||||
) : (
|
|
||||||
<Send className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
{sendingEmailOTP ? 'جاري الإرسال...' : 'إرسال رمز التحقق'}
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showEmailInput && (
|
{showEmailInput && (
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} className="space-y-3">
|
||||||
initial={{ opacity: 0, height: 0 }}
|
<p className="text-xs text-gray-500">{t("accountVerification.enter6DigitCode")}</p>
|
||||||
animate={{ opacity: 1, height: 'auto' }}
|
|
||||||
className="space-y-3"
|
|
||||||
>
|
|
||||||
<p className="text-xs text-gray-500">أدخل الرمز المكون من 6 أرقام الذي تم إرساله إلى بريدك الإلكتروني</p>
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={emailCode}
|
value={emailCode}
|
||||||
onChange={(e) => setEmailCode(e.target.value)}
|
onChange={(e) => setEmailCode(e.target.value)}
|
||||||
placeholder="رمز التحقق"
|
placeholder={t("accountVerification.otpPlaceholder")}
|
||||||
maxLength={6}
|
maxLength={6}
|
||||||
className="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none text-center text-lg tracking-widest"
|
className="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none text-center text-lg tracking-widest"
|
||||||
/>
|
/>
|
||||||
@ -211,20 +172,12 @@ export default function AccountVerificationPage() {
|
|||||||
disabled={verifyingEmail}
|
disabled={verifyingEmail}
|
||||||
className="px-6 py-3 bg-green-500 text-white rounded-xl hover:bg-green-600 transition-colors text-sm font-medium disabled:opacity-50 flex items-center gap-2"
|
className="px-6 py-3 bg-green-500 text-white rounded-xl hover:bg-green-600 transition-colors text-sm font-medium disabled:opacity-50 flex items-center gap-2"
|
||||||
>
|
>
|
||||||
{verifyingEmail ? (
|
{verifyingEmail ? <Loader2 className="w-4 h-4 animate-spin" /> : <Key className="w-4 h-4" />}
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
{t("accountVerification.verify")}
|
||||||
) : (
|
|
||||||
<Key className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
تحقق
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button onClick={handleSendEmailOTP} disabled={sendingEmailOTP} className="text-xs text-amber-600 hover:text-amber-700">
|
||||||
onClick={handleSendEmailOTP}
|
{t("accountVerification.resendCode")}
|
||||||
disabled={sendingEmailOTP}
|
|
||||||
className="text-xs text-amber-600 hover:text-amber-700"
|
|
||||||
>
|
|
||||||
إعادة إرسال الرمز
|
|
||||||
</button>
|
</button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
@ -232,7 +185,7 @@ export default function AccountVerificationPage() {
|
|||||||
{emailVerified && (
|
{emailVerified && (
|
||||||
<div className="flex items-center gap-2 text-green-600 bg-green-50 px-4 py-3 rounded-xl">
|
<div className="flex items-center gap-2 text-green-600 bg-green-50 px-4 py-3 rounded-xl">
|
||||||
<CheckCircle className="w-5 h-5" />
|
<CheckCircle className="w-5 h-5" />
|
||||||
<span className="text-sm font-medium">تم توثيق البريد الإلكتروني بنجاح</span>
|
<span className="text-sm font-medium">{t("accountVerification.emailVerifiedSuccess")}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -241,23 +194,19 @@ export default function AccountVerificationPage() {
|
|||||||
<motion.div variants={itemVariants} className="bg-white rounded-2xl shadow-sm overflow-hidden">
|
<motion.div variants={itemVariants} className="bg-white rounded-2xl shadow-sm overflow-hidden">
|
||||||
<div className="px-6 py-4 border-b border-gray-100 flex items-center gap-3">
|
<div className="px-6 py-4 border-b border-gray-100 flex items-center gap-3">
|
||||||
<Phone className="w-5 h-5 text-amber-600" />
|
<Phone className="w-5 h-5 text-amber-600" />
|
||||||
<h2 className="text-lg font-semibold text-gray-800">رقم الهاتف</h2>
|
<h2 className="text-lg font-semibold text-gray-800">{t("accountVerification.phoneSection")}</h2>
|
||||||
{phoneVerified && (
|
{phoneVerified && (
|
||||||
<span className="flex items-center gap-1 text-xs text-green-600 bg-green-50 px-2 py-0.5 rounded-full">
|
<span className="flex items-center gap-1 text-xs text-green-600 bg-green-50 px-2 py-0.5 rounded-full">
|
||||||
<CheckCircle className="w-3 h-3" />
|
<CheckCircle className="w-3 h-3" />
|
||||||
موثق
|
{t("accountVerification.verified")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600">{user?.phone || 'رقم هاتف غير مسجل'}</p>
|
<p className="text-sm text-gray-600">{user?.phone || t("accountVerification.phoneNotRegistered")}</p>
|
||||||
<p className="text-xs text-gray-400 mt-1">
|
<p className="text-xs text-gray-400 mt-1">{phoneVerified ? t("accountVerification.phoneVerifiedDesc") : t("accountVerification.phoneVerifyDesc")}</p>
|
||||||
{phoneVerified
|
|
||||||
? 'رقم هاتفك موثق ويمكنك استخدامه لتسجيل الدخول'
|
|
||||||
: 'يرجى توثيق رقم هاتفك لحماية حسابك'}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
{!phoneVerified && !showPhoneInput && (
|
{!phoneVerified && !showPhoneInput && (
|
||||||
<button
|
<button
|
||||||
@ -265,29 +214,21 @@ export default function AccountVerificationPage() {
|
|||||||
disabled={sendingPhoneOTP}
|
disabled={sendingPhoneOTP}
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-amber-500 text-white rounded-xl hover:bg-amber-600 transition-colors text-sm disabled:opacity-50"
|
className="flex items-center gap-2 px-4 py-2 bg-amber-500 text-white rounded-xl hover:bg-amber-600 transition-colors text-sm disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{sendingPhoneOTP ? (
|
{sendingPhoneOTP ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />}
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
{sendingPhoneOTP ? t("accountVerification.sending") : t("accountVerification.sendOtp")}
|
||||||
) : (
|
|
||||||
<Send className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
{sendingPhoneOTP ? 'جاري الإرسال...' : 'إرسال رمز التحقق'}
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showPhoneInput && (
|
{showPhoneInput && (
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} className="space-y-3">
|
||||||
initial={{ opacity: 0, height: 0 }}
|
<p className="text-xs text-gray-500">{t("accountVerification.enter6DigitCodePhone")}</p>
|
||||||
animate={{ opacity: 1, height: 'auto' }}
|
|
||||||
className="space-y-3"
|
|
||||||
>
|
|
||||||
<p className="text-xs text-gray-500">أدخل الرمز المكون من 6 أرقام الذي تم إرساله إلى هاتفك</p>
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={phoneCode}
|
value={phoneCode}
|
||||||
onChange={(e) => setPhoneCode(e.target.value)}
|
onChange={(e) => setPhoneCode(e.target.value)}
|
||||||
placeholder="رمز التحقق"
|
placeholder={t("accountVerification.otpPlaceholder")}
|
||||||
maxLength={6}
|
maxLength={6}
|
||||||
className="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none text-center text-lg tracking-widest"
|
className="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none text-center text-lg tracking-widest"
|
||||||
/>
|
/>
|
||||||
@ -296,20 +237,12 @@ export default function AccountVerificationPage() {
|
|||||||
disabled={verifyingPhone}
|
disabled={verifyingPhone}
|
||||||
className="px-6 py-3 bg-green-500 text-white rounded-xl hover:bg-green-600 transition-colors text-sm font-medium disabled:opacity-50 flex items-center gap-2"
|
className="px-6 py-3 bg-green-500 text-white rounded-xl hover:bg-green-600 transition-colors text-sm font-medium disabled:opacity-50 flex items-center gap-2"
|
||||||
>
|
>
|
||||||
{verifyingPhone ? (
|
{verifyingPhone ? <Loader2 className="w-4 h-4 animate-spin" /> : <Key className="w-4 h-4" />}
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
{t("accountVerification.verify")}
|
||||||
) : (
|
|
||||||
<Key className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
تحقق
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button onClick={handleSendPhoneOTP} disabled={sendingPhoneOTP} className="text-xs text-amber-600 hover:text-amber-700">
|
||||||
onClick={handleSendPhoneOTP}
|
{t("accountVerification.resendCode")}
|
||||||
disabled={sendingPhoneOTP}
|
|
||||||
className="text-xs text-amber-600 hover:text-amber-700"
|
|
||||||
>
|
|
||||||
إعادة إرسال الرمز
|
|
||||||
</button>
|
</button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
@ -317,7 +250,7 @@ export default function AccountVerificationPage() {
|
|||||||
{phoneVerified && (
|
{phoneVerified && (
|
||||||
<div className="flex items-center gap-2 text-green-600 bg-green-50 px-4 py-3 rounded-xl">
|
<div className="flex items-center gap-2 text-green-600 bg-green-50 px-4 py-3 rounded-xl">
|
||||||
<CheckCircle className="w-5 h-5" />
|
<CheckCircle className="w-5 h-5" />
|
||||||
<span className="text-sm font-medium">تم توثيق رقم الهاتف بنجاح</span>
|
<span className="text-sm font-medium">{t("accountVerification.phoneVerifiedSuccess")}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -326,20 +259,20 @@ export default function AccountVerificationPage() {
|
|||||||
<motion.div variants={itemVariants} className="bg-gradient-to-br from-amber-500 to-amber-600 rounded-2xl p-6 text-white">
|
<motion.div variants={itemVariants} className="bg-gradient-to-br from-amber-500 to-amber-600 rounded-2xl p-6 text-white">
|
||||||
<div className="flex items-center gap-3 mb-3">
|
<div className="flex items-center gap-3 mb-3">
|
||||||
<Shield className="w-6 h-6" />
|
<Shield className="w-6 h-6" />
|
||||||
<h3 className="text-lg font-semibold">لماذا يجب توثيق حسابك؟</h3>
|
<h3 className="text-lg font-semibold">{t("accountVerification.whyVerify")}</h3>
|
||||||
</div>
|
</div>
|
||||||
<ul className="space-y-2 text-sm text-amber-50">
|
<ul className="space-y-2 text-sm " style={{ color: "#0f172a", fontWeight: "400" }}>
|
||||||
<li className="flex items-center gap-2">
|
<li className="flex items-center gap-2">
|
||||||
<CheckCircle className="w-4 h-4 flex-shrink-0" />
|
<CheckCircle className="w-4 h-4 flex-shrink-0" />
|
||||||
حماية حسابك من الوصول غير المصرح به
|
{t("accountVerification.reason1")}
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-center gap-2">
|
<li className="flex items-center gap-2">
|
||||||
<CheckCircle className="w-4 h-4 flex-shrink-0" />
|
<CheckCircle className="w-4 h-4 flex-shrink-0" />
|
||||||
استعادة كلمة المرور بسهولة في حال فقدانها
|
{t("accountVerification.reason2")}
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-center gap-2">
|
<li className="flex items-center gap-2">
|
||||||
<CheckCircle className="w-4 h-4 flex-shrink-0" />
|
<CheckCircle className="w-4 h-4 flex-shrink-0" />
|
||||||
زيادة الثقة مع مالكي العقارات والمستأجرين
|
{t("accountVerification.reason3")}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import Link from 'next/link';
|
|||||||
|
|
||||||
export default function Error({ error, reset }) {
|
export default function Error({ error, reset }) {
|
||||||
return (
|
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">
|
<div className="min-h-screen bg-gray-900 flex items-center justify-center p-4">
|
||||||
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="text-center max-w-md">
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="text-center max-w-md">
|
||||||
<div className="w-20 h-20 bg-red-500/20 rounded-full flex items-center justify-center mx-auto mb-6">
|
<div className="w-20 h-20 bg-red-500/20 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
<AlertTriangle className="w-10 h-10 text-red-500" />
|
<AlertTriangle className="w-10 h-10 text-red-500" />
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
|
|
||||||
export default function Loading() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center">
|
|
||||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-center">
|
|
||||||
<div className="w-14 h-14 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
|
||||||
<p className="text-gray-400 text-lg">جاري التحميل...</p>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -3,8 +3,11 @@
|
|||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Home, Building, Briefcase, ArrowLeft } from 'lucide-react';
|
import { Home, Building, Briefcase, ArrowLeft } from 'lucide-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import InteractiveBackground from '@/app/components/Animation/Background';
|
||||||
|
|
||||||
export default function ChooseRolePage() {
|
export default function ChooseRolePage() {
|
||||||
|
const { t, i18n } = useTranslation();
|
||||||
const containerVariants = {
|
const containerVariants = {
|
||||||
hidden: { opacity: 0 },
|
hidden: { opacity: 0 },
|
||||||
visible: {
|
visible: {
|
||||||
@ -35,7 +38,8 @@ export default function ChooseRolePage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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 flex items-center justify-center p-4 relative overflow-hidden">
|
||||||
|
<InteractiveBackground/>
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
{[...Array(20)].map((_, i) => (
|
{[...Array(20)].map((_, i) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
@ -77,7 +81,7 @@ export default function ChooseRolePage() {
|
|||||||
<motion.div whileHover={{ x: -5 }}>
|
<motion.div whileHover={{ x: -5 }}>
|
||||||
<ArrowLeft className="w-4 h-4" />
|
<ArrowLeft className="w-4 h-4" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<span>العودة للرئيسية</span>
|
<span>{t('backToHome')}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@ -86,13 +90,13 @@ export default function ChooseRolePage() {
|
|||||||
className="text-center mb-12"
|
className="text-center mb-12"
|
||||||
>
|
>
|
||||||
<motion.h1
|
<motion.h1
|
||||||
className="text-4xl md:text-5xl font-bold text-white mb-4"
|
className="text-4xl md:text-5xl font-bold text-amber-50 mb-4"
|
||||||
animate={floatingAnimation}
|
animate={floatingAnimation}
|
||||||
>
|
>
|
||||||
مرحباً بك في SweetHome
|
{t('chooseRole.welcomeToSweetHome')}
|
||||||
</motion.h1>
|
</motion.h1>
|
||||||
<p className="text-xl text-gray-400">
|
<p className="text-xl text-gray-400">
|
||||||
اختر نوع الحساب المناسب لك
|
{t('chooseRole.subtitle')}
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@ -123,23 +127,23 @@ export default function ChooseRolePage() {
|
|||||||
<Building className="w-10 h-10 text-white" />
|
<Building className="w-10 h-10 text-white" />
|
||||||
</motion.div>
|
</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">
|
<p className="text-amber-100 mb-6">
|
||||||
تريد عرض عقار للإيجار أو البيع؟ انضم كمالك عقار وابدأ الآن
|
{t('chooseRole.ownerDesc')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul className="space-y-2 text-sm text-amber-100">
|
<ul className="space-y-2 text-sm text-amber-100">
|
||||||
<li className="flex items-center gap-2">
|
<li className="flex items-center gap-2">
|
||||||
<span className="w-1.5 h-1.5 bg-white rounded-full" />
|
<span className="w-1.5 h-1.5 bg-white rounded-full" />
|
||||||
إضافة عقارات غير محدودة
|
{t('chooseRole.ownerFeature1')}
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-center gap-2">
|
<li className="flex items-center gap-2">
|
||||||
<span className="w-1.5 h-1.5 bg-white rounded-full" />
|
<span className="w-1.5 h-1.5 bg-white rounded-full" />
|
||||||
إدارة الحجوزات بسهولة
|
{t('chooseRole.ownerFeature2')}
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-center gap-2">
|
<li className="flex items-center gap-2">
|
||||||
<span className="w-1.5 h-1.5 bg-white rounded-full" />
|
<span className="w-1.5 h-1.5 bg-white rounded-full" />
|
||||||
تواصل مباشر مع المستأجرين
|
{t('chooseRole.ownerFeature3')}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@ -148,7 +152,7 @@ export default function ChooseRolePage() {
|
|||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
>
|
>
|
||||||
إنشاء حساب مالك
|
{t('chooseRole.createOwnerAccount')}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -181,23 +185,23 @@ export default function ChooseRolePage() {
|
|||||||
<Home className="w-10 h-10 text-white" />
|
<Home className="w-10 h-10 text-white" />
|
||||||
</motion.div>
|
</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">
|
<p className="text-blue-100 mb-6">
|
||||||
تبحث عن عقار للإيجار أو الشراء؟ انضم كعميل وابحث عن منزل أحلامك
|
{t('chooseRole.tenantDesc')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul className="space-y-2 text-sm text-blue-100">
|
<ul className="space-y-2 text-sm text-blue-100">
|
||||||
<li className="flex items-center gap-2">
|
<li className="flex items-center gap-2">
|
||||||
<span className="w-1.5 h-1.5 bg-white rounded-full" />
|
<span className="w-1.5 h-1.5 bg-white rounded-full" />
|
||||||
آلاف العقارات المتاحة
|
{t('chooseRole.tenantFeature1')}
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-center gap-2">
|
<li className="flex items-center gap-2">
|
||||||
<span className="w-1.5 h-1.5 bg-white rounded-full" />
|
<span className="w-1.5 h-1.5 bg-white rounded-full" />
|
||||||
بحث متقدم بفلاتر ذكية
|
{t('chooseRole.tenantFeature2')}
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-center gap-2">
|
<li className="flex items-center gap-2">
|
||||||
<span className="w-1.5 h-1.5 bg-white rounded-full" />
|
<span className="w-1.5 h-1.5 bg-white rounded-full" />
|
||||||
حجز وإستئجار فوري
|
{t('chooseRole.tenantFeature3')}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@ -206,7 +210,7 @@ export default function ChooseRolePage() {
|
|||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
>
|
>
|
||||||
إنشاء حساب مستأجر
|
{t('chooseRole.createTenantAccount')}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -239,23 +243,23 @@ export default function ChooseRolePage() {
|
|||||||
<Briefcase className="w-10 h-10 text-white" />
|
<Briefcase className="w-10 h-10 text-white" />
|
||||||
</motion.div>
|
</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">
|
<p className="text-purple-100 mb-6">
|
||||||
تدير عقارات للغير؟ انضم كوسيط عقاري واحصل على فرص مميزة
|
{t('chooseRole.agentDesc')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul className="space-y-2 text-sm text-purple-100">
|
<ul className="space-y-2 text-sm text-purple-100">
|
||||||
<li className="flex items-center gap-2">
|
<li className="flex items-center gap-2">
|
||||||
<span className="w-1.5 h-1.5 bg-white rounded-full" />
|
<span className="w-1.5 h-1.5 bg-white rounded-full" />
|
||||||
إدارة عقارات العملاء
|
{t('chooseRole.agentFeature1')}
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-center gap-2">
|
<li className="flex items-center gap-2">
|
||||||
<span className="w-1.5 h-1.5 bg-white rounded-full" />
|
<span className="w-1.5 h-1.5 bg-white rounded-full" />
|
||||||
عمولات وأرباح مضمونة
|
{t('chooseRole.agentFeature2')}
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-center gap-2">
|
<li className="flex items-center gap-2">
|
||||||
<span className="w-1.5 h-1.5 bg-white rounded-full" />
|
<span className="w-1.5 h-1.5 bg-white rounded-full" />
|
||||||
لوحة تحكم متكاملة
|
{t('chooseRole.agentFeature3')}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@ -264,7 +268,7 @@ export default function ChooseRolePage() {
|
|||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
>
|
>
|
||||||
إنشاء حساب وكيل
|
{t('chooseRole.createAgentAccount')}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -276,12 +280,12 @@ export default function ChooseRolePage() {
|
|||||||
variants={itemVariants}
|
variants={itemVariants}
|
||||||
className="text-center text-gray-400 mt-8"
|
className="text-center text-gray-400 mt-8"
|
||||||
>
|
>
|
||||||
لديك حساب بالفعل؟{' '}
|
{t('chooseRole.alreadyHaveAccount')}{' '}
|
||||||
<Link
|
<Link
|
||||||
href="/login"
|
href="/login"
|
||||||
className="text-amber-400 hover:text-amber-300 font-medium transition-colors"
|
className="text-amber-400 hover:text-amber-300 font-medium transition-colors"
|
||||||
>
|
>
|
||||||
تسجيل الدخول
|
{t('login')}
|
||||||
</Link>
|
</Link>
|
||||||
</motion.p>
|
</motion.p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@ -1,22 +1,25 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from "next/navigation";
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from "framer-motion";
|
||||||
import { ShieldAlert, LogOut, MessageSquare, Send, Loader2 } from 'lucide-react';
|
import { useTranslation } from "react-i18next";
|
||||||
import toast, { Toaster } from 'react-hot-toast';
|
import { ShieldAlert, LogOut, MessageSquare, Send, Loader2 } from "lucide-react";
|
||||||
import AuthService from '../services/AuthService';
|
import toast, { Toaster } from "react-hot-toast";
|
||||||
import { sendGeneralReport } from '../utils/api';
|
import AuthService from "../services/AuthService";
|
||||||
|
import { sendGeneralReport } from "../utils/api";
|
||||||
|
import InteractiveBackground from "../components/Animation/Background";
|
||||||
|
|
||||||
export default function BlockedPage() {
|
export default function BlockedPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [form, setForm] = useState({ subject: '', body: '' });
|
const [form, setForm] = useState({ subject: "", body: "" });
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [isSent, setIsSent] = useState(false);
|
const [isSent, setIsSent] = useState(false);
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
AuthService.deleteToken();
|
AuthService.deleteToken();
|
||||||
router.replace('/');
|
router.replace("/");
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateField = (field, value) => {
|
const updateField = (field, value) => {
|
||||||
@ -28,7 +31,7 @@ export default function BlockedPage() {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (!form.subject.trim() || !form.body.trim()) {
|
if (!form.subject.trim() || !form.body.trim()) {
|
||||||
toast.error('يرجى تعبئة الموضوع والرسالة');
|
toast.error(t("blocked.fillAllFields"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,39 +40,29 @@ export default function BlockedPage() {
|
|||||||
try {
|
try {
|
||||||
await sendGeneralReport(form.subject.trim(), form.body.trim());
|
await sendGeneralReport(form.subject.trim(), form.body.trim());
|
||||||
setIsSent(true);
|
setIsSent(true);
|
||||||
setForm({ subject: '', body: '' });
|
setForm({ subject: "", body: "" });
|
||||||
toast.success('تم إرسال طلب الدعم بنجاح');
|
toast.success(t("blocked.reportSent"));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('حدث خطأ أثناء إرسال طلب الدعم. حاول مرة أخرى');
|
toast.error(t("blocked.sendError"));
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-red-50 via-white to-amber-50 flex items-center justify-center p-4" dir="rtl">
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4" dir="rtl">
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
<InteractiveBackground />
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, y: 24 }} animate={{ opacity: 1, y: 0 }} className="w-full max-w-5xl relative z-10">
|
||||||
initial={{ opacity: 0, y: 24 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="w-full max-w-5xl"
|
|
||||||
>
|
|
||||||
<div className="text-center mb-10">
|
<div className="text-center mb-10">
|
||||||
<motion.div
|
<motion.div initial={{ scale: 0.9 }} animate={{ scale: 1 }} className="w-24 h-24 bg-red-100 rounded-3xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-red-100">
|
||||||
initial={{ scale: 0.9 }}
|
|
||||||
animate={{ scale: 1 }}
|
|
||||||
className="w-24 h-24 bg-red-100 rounded-3xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-red-100"
|
|
||||||
>
|
|
||||||
<ShieldAlert className="w-12 h-12 text-red-600" />
|
<ShieldAlert className="w-12 h-12 text-red-600" />
|
||||||
</motion.div>
|
</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">
|
<p className="text-gray-600 text-lg max-w-2xl mx-auto">{t("blocked.description")}</p>
|
||||||
تم تقييد وصولك إلى التطبيق. يمكنك تسجيل الخروج أو مراسلة دعم العملاء للمساعدة في حل المشكلة.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-6">
|
<div className="grid md:grid-cols-2 gap-6 relative z-10 ">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, x: -24 }}
|
initial={{ opacity: 0, x: -24 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
@ -80,57 +73,50 @@ export default function BlockedPage() {
|
|||||||
<div className="w-14 h-14 bg-red-50 rounded-2xl flex items-center justify-center mb-6">
|
<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" />
|
<LogOut className="w-7 h-7 text-red-600" />
|
||||||
</div>
|
</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">
|
<p className="text-gray-600 leading-7">{t("blocked.logoutDesc")}</p>
|
||||||
إنهاء الجلسة الحالية وإزالة بيانات الدخول من هذا الجهاز.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
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"
|
className="mt-8 w-full cursor-pointer 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" />
|
<LogOut className="w-5 h-5" />
|
||||||
تسجيل الخروج
|
{t("blocked.logoutButton")}
|
||||||
</button>
|
</button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, x: 24 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.2 }} className="bg-white rounded-3xl shadow-sm border border-gray-200 p-8">
|
||||||
initial={{ opacity: 0, x: 24 }}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
transition={{ delay: 0.2 }}
|
|
||||||
className="bg-white rounded-3xl shadow-sm border border-gray-200 p-8"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-4 mb-6">
|
<div className="flex items-center gap-4 mb-6">
|
||||||
<div className="w-14 h-14 bg-amber-50 rounded-2xl flex items-center justify-center">
|
<div className="w-14 h-14 bg-amber-50 rounded-2xl flex items-center justify-center">
|
||||||
<MessageSquare className="w-7 h-7 text-amber-600" />
|
<MessageSquare className="w-7 h-7 text-amber-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold text-gray-900">مراسلة دعم العملاء</h2>
|
<h2 className="text-2xl font-bold text-gray-900">{t("blocked.contactSupportTitle")}</h2>
|
||||||
<p className="text-gray-600 mt-1">أرسل تفاصيل المشكلة وسنقوم بمراجعتها.</p>
|
<p className="text-gray-600 mt-1">{t("blocked.contactSupportDesc")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-5">
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
<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("subject")}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={form.subject}
|
value={form.subject}
|
||||||
onChange={(event) => updateField('subject', event.target.value)}
|
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"
|
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>
|
||||||
|
|
||||||
<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
|
<textarea
|
||||||
value={form.body}
|
value={form.body}
|
||||||
onChange={(event) => updateField('body', event.target.value)}
|
onChange={(event) => updateField("body", event.target.value)}
|
||||||
rows={6}
|
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"
|
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>
|
</div>
|
||||||
@ -143,17 +129,17 @@ export default function BlockedPage() {
|
|||||||
{isSubmitting ? (
|
{isSubmitting ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="w-5 h-5 animate-spin" />
|
<Loader2 className="w-5 h-5 animate-spin" />
|
||||||
جاري الإرسال...
|
{t("blocked.sending")}
|
||||||
</>
|
</>
|
||||||
) : isSent ? (
|
) : isSent ? (
|
||||||
<>
|
<>
|
||||||
<Send className="w-5 h-5" />
|
<Send className="w-5 h-5" />
|
||||||
تم الإرسال
|
{t("blocked.sentButton")}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Send className="w-5 h-5" />
|
<Send className="w-5 h-5" />
|
||||||
إرسال الرسالة
|
{t("blocked.sendButton")}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
Home, Star, MapPin, Calendar, Clock, Check, X, Loader2,
|
Home, Star, MapPin, Calendar, Clock, Check, X, Loader2,
|
||||||
User, MessageCircle, ChevronDown, Image as ImageIcon,
|
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_MAP = ['pending', 'ownerConfirmed', 'depositPaid', 'depositConfirmed', 'completed', 'cancelled'];
|
||||||
|
|
||||||
const STATUS_CONFIG = {
|
const STATUS_CONFIG = {
|
||||||
pending: { label: 'قيد الانتظار', color: 'bg-yellow-100 text-yellow-800 border-yellow-300' },
|
pending: { color: 'bg-yellow-100 text-yellow-800 border-yellow-300' },
|
||||||
ownerConfirmed: { label: 'مؤكد من المالك', color: 'bg-blue-100 text-blue-800 border-blue-300' },
|
ownerConfirmed: { color: 'bg-blue-100 text-blue-800 border-blue-300' },
|
||||||
depositPaid: { label: 'تم دفع السلفة', color: 'bg-orange-100 text-orange-800 border-orange-300' },
|
depositPaid: { color: 'bg-orange-100 text-orange-800 border-orange-300' },
|
||||||
depositConfirmed: { label: 'مؤكد', color: 'bg-green-100 text-green-800 border-green-300' },
|
depositConfirmed: { color: 'bg-green-100 text-green-800 border-green-300' },
|
||||||
completed: { label: 'منتهي', color: 'bg-teal-100 text-teal-800 border-teal-300' },
|
completed: { color: 'bg-teal-100 text-teal-800 border-teal-300' },
|
||||||
cancelled: { label: 'ملغي', color: 'bg-red-100 text-red-800 border-red-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';
|
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api';
|
||||||
|
|
||||||
function StatusBadge({ code }) {
|
function StatusBadge({ code }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const key = STATUS_MAP[code] || 'pending';
|
const key = STATUS_MAP[code] || 'pending';
|
||||||
const cfg = STATUS_CONFIG[key];
|
const cfg = STATUS_CONFIG[key];
|
||||||
return (
|
return (
|
||||||
<span className={`inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium border ${cfg.color}`}>
|
<span className={`inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium border ${cfg.color}`}>
|
||||||
<Clock className="w-3 h-3" /> {cfg.label}
|
<Clock className="w-3 h-3" /> {t(`bookingStatus.${key}`)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ReservationCard({ reservation: r, onRate }) {
|
function ReservationCard({ reservation: r, onRate }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const isCompleted = STATUS_MAP[r.status] === 'completed';
|
const isCompleted = STATUS_MAP[r.status] === 'completed';
|
||||||
const imgSrc = r.propertyImage || r._prop?.images?.[0] || (r.propertyInfo?.images?.[0]);
|
const imgSrc = r.propertyImage || r._prop?.images?.[0] || (r.propertyInfo?.images?.[0]);
|
||||||
const imageUrl = imgSrc ? `${API_BASE}${imgSrc}` : null;
|
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 justify-between items-start">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Home className="w-5 h-5 text-amber-500" />
|
<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>
|
</div>
|
||||||
<StatusBadge code={r.status} />
|
<StatusBadge code={r.status} />
|
||||||
</div>
|
</div>
|
||||||
@ -73,38 +76,38 @@ function ReservationCard({ reservation: r, onRate }) {
|
|||||||
|
|
||||||
{(beds > 0 || baths > 0) && (
|
{(beds > 0 || baths > 0) && (
|
||||||
<div className="flex gap-3 text-sm text-gray-600">
|
<div className="flex gap-3 text-sm text-gray-600">
|
||||||
{beds > 0 && <span>{beds} غرف</span>}
|
{beds > 0 && <span>{beds} {t('rooms')}</span>}
|
||||||
{baths > 0 && <span>{baths} حمامات</span>}
|
{baths > 0 && <span>{baths} {t('bathrooms')}</span>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="bg-gray-50 p-3 rounded-xl text-center">
|
<div className="bg-gray-50 p-3 rounded-xl text-center">
|
||||||
<Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1" />
|
<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 className="text-sm font-medium">{new Date(r.startDate).toLocaleDateString('ar')}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-50 p-3 rounded-xl text-center">
|
<div className="bg-gray-50 p-3 rounded-xl text-center">
|
||||||
<Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1" />
|
<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 className="text-sm font-medium">{new Date(r.endDate).toLocaleDateString('ar')}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-amber-50 p-3 rounded-xl text-center">
|
<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-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>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 text-xs text-gray-400">
|
<div className="flex items-center gap-2 text-xs text-gray-400">
|
||||||
<User className="w-3 h-3" />
|
<User className="w-3 h-3" />
|
||||||
<span>رقم الحجز: #{r.id}</span>
|
<span>{t('reservationNumber', { id: r.id })}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isCompleted && (
|
{isCompleted && (
|
||||||
<button onClick={onRate}
|
<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">
|
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>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -113,6 +116,7 @@ function ReservationCard({ reservation: r, onRate }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function BookedPropertiesPage() {
|
export default function BookedPropertiesPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [reservations, setReservations] = useState([]);
|
const [reservations, setReservations] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [ratingReservation, setRatingReservation] = useState(null);
|
const [ratingReservation, setRatingReservation] = useState(null);
|
||||||
@ -120,7 +124,7 @@ export default function BookedPropertiesPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!AuthService.getToken()) {
|
if (!AuthService.getToken()) {
|
||||||
toast.error('يرجى تسجيل الدخول أولاً');
|
toast.error(t('loginRequired'));
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -133,20 +137,14 @@ export default function BookedPropertiesPage() {
|
|||||||
setReservations(Array.isArray(data) ? data : []);
|
setReservations(Array.isArray(data) ? data : []);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
toast.error('فشل تحميل الحجوزات');
|
toast.error(t('failedToLoadBookings'));
|
||||||
setReservations([]);
|
setReservations([]);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center" dir="rtl">
|
|
||||||
<Loader2 className="w-12 h-12 text-amber-500 animate-spin" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">
|
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">
|
||||||
@ -155,14 +153,14 @@ export default function BookedPropertiesPage() {
|
|||||||
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="mb-8">
|
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="mb-8">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<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('bookedProperties')}</h1>
|
||||||
<p className="text-gray-600">لديك {reservations.length} حجز</p>
|
<p className="text-gray-600">{t('youHaveBookings', { count: reservations.length })}</p>
|
||||||
</div>
|
</div>
|
||||||
{reservations.length > 0 && (
|
{reservations.length > 0 && (
|
||||||
<button onClick={() => setExpandedId(expandedId ? null : 'all')}
|
<button onClick={() => setExpandedId(expandedId ? null : 'all')}
|
||||||
className="flex items-center gap-1 text-sm text-amber-600 hover:text-amber-700 transition">
|
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' : ''}`} />
|
<ChevronDown className={`w-4 h-4 transition-transform ${expandedId ? 'rotate-180' : ''}`} />
|
||||||
{expandedId ? 'طي الكل' : 'عرض الكل'}
|
{expandedId ? t('collapseAll') : t('showAll')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -172,8 +170,8 @@ export default function BookedPropertiesPage() {
|
|||||||
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 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">
|
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" />
|
<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>
|
<h3 className="text-xl font-bold text-gray-900 mb-2">{t('noBookings')}</h3>
|
||||||
<p className="text-gray-500">لم تقم بحجز أي عقار حتى الآن</p>
|
<p className="text-gray-500">{t('noBookingsDesc')}</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
@ -188,8 +186,8 @@ export default function BookedPropertiesPage() {
|
|||||||
className="mt-8 bg-amber-50 border border-amber-200 rounded-xl p-4 flex items-start gap-3">
|
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" />
|
<MessageCircle className="w-5 h-5 text-amber-600 flex-shrink-0 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-amber-800 font-medium">تقييم العقارات</p>
|
<p className="text-amber-800 font-medium">{t('rateProperties')}</p>
|
||||||
<p className="text-amber-600 text-sm">يمكنك تقييم العقارات المنتهية حجزها لمساعدة المستأجرين الآخرين</p>
|
<p className="text-amber-600 text-sm">{t('ratePropertiesDesc')}</p>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
@ -209,7 +207,7 @@ export default function BookedPropertiesPage() {
|
|||||||
</button>
|
</button>
|
||||||
<PropertyRatingForm
|
<PropertyRatingForm
|
||||||
reservationId={ratingReservation.id}
|
reservationId={ratingReservation.id}
|
||||||
onSuccess={() => { setRatingReservation(null); toast.success('تم إرسال التقييم بنجاح!'); }}
|
onSuccess={() => { setRatingReservation(null); toast.success(t('ratingSubmitted')); }}
|
||||||
onCancel={() => setRatingReservation(null)}
|
onCancel={() => setRatingReservation(null)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,16 +1,19 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from "framer-motion";
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from "next/navigation";
|
||||||
import toast, { Toaster } from 'react-hot-toast';
|
import toast, { Toaster } from "react-hot-toast";
|
||||||
import { Lock, Eye, EyeOff, ArrowLeft, Shield } from 'lucide-react';
|
import { Lock, Eye, EyeOff, ArrowLeft, Shield } from "lucide-react";
|
||||||
import { changePassword } from '../utils/api';
|
import { useTranslation } from "react-i18next";
|
||||||
import AuthService from '../services/AuthService';
|
import { changePassword } from "../utils/api";
|
||||||
|
import AuthService from "../services/AuthService";
|
||||||
|
import InteractiveBackground from "../components/Animation/Background";
|
||||||
|
|
||||||
export default function ChangePasswordPage() {
|
export default function ChangePasswordPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [form, setForm] = useState({ oldPassword: '', newPassword: '', confirmPassword: '' });
|
const { t } = useTranslation();
|
||||||
|
const [form, setForm] = useState({ oldPassword: "", newPassword: "", confirmPassword: "" });
|
||||||
const [show, setShow] = useState({ old: false, new: false, confirm: false });
|
const [show, setShow] = useState({ old: false, new: false, confirm: false });
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
@ -22,162 +25,124 @@ export default function ChangePasswordPage() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!AuthService.isAuthenticated()) {
|
if (!AuthService.isAuthenticated()) {
|
||||||
toast.error('يرجى تسجيل الدخول أولاً');
|
toast.error(t("changePassword.loginRequired"));
|
||||||
router.push('/login');
|
router.push("/login");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (form.newPassword !== form.confirmPassword) {
|
if (form.newPassword !== form.confirmPassword) {
|
||||||
toast.error('كلمة المرور الجديدة وتأكيدها غير متطابقتين');
|
toast.error(t("changePassword.passwordsDoNotMatch"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (form.newPassword.length < 6) {
|
if (form.newPassword.length < 6) {
|
||||||
toast.error('كلمة المرور الجديدة يجب أن تكون 6 أحرف على الأقل');
|
toast.error(t("validation.passwordMinLength"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
await changePassword(form.oldPassword, form.newPassword);
|
await changePassword(form.oldPassword, form.newPassword);
|
||||||
toast.success('تم تغيير كلمة المرور بنجاح');
|
toast.success(t("changePassword.changeSuccess"));
|
||||||
setTimeout(() => router.push('/profile'), 1200);
|
setTimeout(() => router.push("/profile"), 1200);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err?.message || 'فشل تغيير كلمة المرور');
|
toast.error(err?.message || t("changePassword.changeFailed"));
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const inputClass = "w-full pr-12 pl-4 py-3 bg-gray-50 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400 transition-all";
|
const inputClass =
|
||||||
|
"w-full pr-12 pl-4 py-3 bg-gray-50 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400 transition-all";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
<div className="min-h-screen relative z-10 flex items-center justify-center p-4" style={{ backgroundColor: "#0f172a" }}>
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
<InteractiveBackground />
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, y: 30 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }} className="w-full max-w-md relative z-10 ">
|
||||||
initial={{ opacity: 0, y: 30 }}
|
<motion.div initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} className="mb-6 relative z-10">
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<button onClick={() => router.push("/profile")} className="flex items-center relative z-10 gap-2 text-gray-600 hover:text-amber-600 transition-colors">
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
className="w-full max-w-md"
|
|
||||||
>
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, x: -20 }}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
className="mb-6"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
onClick={() => router.push('/profile')}
|
|
||||||
className="flex items-center gap-2 text-gray-600 hover:text-amber-600 transition-colors"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="w-5 h-5" />
|
<ArrowLeft className="w-5 h-5" />
|
||||||
<span>العودة للملف الشخصي</span>
|
<span>{t("changePassword.backToProfile")}</span>
|
||||||
</button>
|
</button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<div className="bg-white rounded-3xl shadow-xl overflow-hidden">
|
<div className=" rounded-3xl shadow-xl overflow-hidden" style={{ backgroundColor: "#1e293b" }}>
|
||||||
<div className="bg-gradient-to-l from-amber-500 to-amber-600 p-8 text-center">
|
<div className="bg-gradient-to-l from-amber-500 to-amber-600 p-8 text-center">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ scale: 0 }}
|
initial={{ scale: 0 }}
|
||||||
animate={{ scale: 1 }}
|
animate={{ scale: 1 }}
|
||||||
transition={{ type: 'spring', stiffness: 200 }}
|
transition={{ type: "spring", stiffness: 200 }}
|
||||||
className="w-16 h-16 bg-white/20 rounded-full flex items-center justify-center mx-auto mb-4"
|
className="w-16 h-16 bg-white/20 rounded-full flex items-center justify-center mx-auto mb-4"
|
||||||
>
|
>
|
||||||
<Shield className="w-8 h-8 text-white" />
|
<Shield className="w-8 h-8 text-white" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<motion.h1
|
<motion.h1 initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} className="text-3xl font-bold text-white mb-2">
|
||||||
initial={{ y: 20, opacity: 0 }}
|
{t("changePassword.title")}
|
||||||
animate={{ y: 0, opacity: 1 }}
|
|
||||||
className="text-3xl font-bold text-white mb-2"
|
|
||||||
>
|
|
||||||
تغيير كلمة المرور
|
|
||||||
</motion.h1>
|
</motion.h1>
|
||||||
<motion.p
|
<motion.p initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} transition={{ delay: 0.1 }} className="text-amber-100">
|
||||||
initial={{ y: 20, opacity: 0 }}
|
{t("changePassword.subtitle")}
|
||||||
animate={{ y: 0, opacity: 1 }}
|
|
||||||
transition={{ delay: 0.1 }}
|
|
||||||
className="text-amber-100"
|
|
||||||
>
|
|
||||||
أدخل كلمة المرور الحالية والجديدة
|
|
||||||
</motion.p>
|
</motion.p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="p-8 space-y-6">
|
<form onSubmit={handleSubmit} className="p-8 space-y-6 " style={{ backgroundColor: "#0f172a" }}>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">{t("changePassword.currentPasswordLabel")}</label>
|
||||||
كلمة المرور الحالية
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
<Lock className="w-5 h-5 text-gray-400" />
|
<Lock className="w-5 h-5 text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type={show.old ? 'text' : 'password'}
|
type={show.old ? "text" : "password"}
|
||||||
value={form.oldPassword}
|
value={form.oldPassword}
|
||||||
onChange={handleChange('oldPassword')}
|
onChange={handleChange("oldPassword")}
|
||||||
className={inputClass}
|
className={inputClass}
|
||||||
placeholder="أدخل كلمة المرور الحالية"
|
placeholder={t("changePassword.currentPasswordPlaceholder")}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<button
|
<button type="button" onClick={() => toggleShow("old")} className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600">
|
||||||
type="button"
|
|
||||||
onClick={() => toggleShow('old')}
|
|
||||||
className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600"
|
|
||||||
>
|
|
||||||
{show.old ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
{show.old ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">{t("changePassword.newPasswordLabel")}</label>
|
||||||
كلمة المرور الجديدة
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
<Lock className="w-5 h-5 text-gray-400" />
|
<Lock className="w-5 h-5 text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type={show.new ? 'text' : 'password'}
|
type={show.new ? "text" : "password"}
|
||||||
value={form.newPassword}
|
value={form.newPassword}
|
||||||
onChange={handleChange('newPassword')}
|
onChange={handleChange("newPassword")}
|
||||||
className={inputClass}
|
className={inputClass}
|
||||||
placeholder="أدخل كلمة المرور الجديدة"
|
placeholder={t("changePassword.newPasswordPlaceholder")}
|
||||||
required
|
required
|
||||||
minLength={6}
|
minLength={6}
|
||||||
/>
|
/>
|
||||||
<button
|
<button type="button" onClick={() => toggleShow("new")} className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600">
|
||||||
type="button"
|
|
||||||
onClick={() => toggleShow('new')}
|
|
||||||
className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600"
|
|
||||||
>
|
|
||||||
{show.new ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
{show.new ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">{t("changePassword.confirmNewPasswordLabel")}</label>
|
||||||
تأكيد كلمة المرور الجديدة
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
<Lock className="w-5 h-5 text-gray-400" />
|
<Lock className="w-5 h-5 text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type={show.confirm ? 'text' : 'password'}
|
type={show.confirm ? "text" : "password"}
|
||||||
value={form.confirmPassword}
|
value={form.confirmPassword}
|
||||||
onChange={handleChange('confirmPassword')}
|
onChange={handleChange("confirmPassword")}
|
||||||
className={inputClass}
|
className={inputClass}
|
||||||
placeholder="أعد إدخال كلمة المرور الجديدة"
|
placeholder={t("changePassword.confirmPasswordPlaceholder")}
|
||||||
required
|
required
|
||||||
minLength={6}
|
minLength={6}
|
||||||
/>
|
/>
|
||||||
<button
|
<button type="button" onClick={() => toggleShow("confirm")} className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600">
|
||||||
type="button"
|
|
||||||
onClick={() => toggleShow('confirm')}
|
|
||||||
className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600"
|
|
||||||
>
|
|
||||||
{show.confirm ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
{show.confirm ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -193,10 +158,10 @@ export default function ChangePasswordPage() {
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||||
<span>جاري الحفظ...</span>
|
<span>{t("changePassword.saving")}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
'تغيير كلمة المرور'
|
t("changePassword.changePassword")
|
||||||
)}
|
)}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
113
app/components/Animation/Background.js
Normal file
113
app/components/Animation/Background.js
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
"use client";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
export default function InteractiveBackground() {
|
||||||
|
const canvasRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
|
let animationFrameId;
|
||||||
|
let width = (canvas.width = window.innerWidth);
|
||||||
|
let height = (canvas.height = window.innerHeight);
|
||||||
|
|
||||||
|
// تتبع حركة الماوس
|
||||||
|
const mouse = { x: null, y: null, radius: 180 };
|
||||||
|
|
||||||
|
const handleMouseMove = (e) => {
|
||||||
|
mouse.x = e.clientX;
|
||||||
|
mouse.y = e.clientY;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
mouse.x = null;
|
||||||
|
mouse.y = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
width = canvas.width = window.innerWidth;
|
||||||
|
height = canvas.height = window.innerHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("mousemove", handleMouseMove);
|
||||||
|
window.addEventListener("mouseleave", handleMouseLeave);
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
|
||||||
|
const particles = Array.from({ length: 70 }, () => ({
|
||||||
|
x: Math.random() * width,
|
||||||
|
y: Math.random() * height,
|
||||||
|
vx: (Math.random() - 0.5) * 0.8,
|
||||||
|
vy: (Math.random() - 0.5) * 0.8,
|
||||||
|
radius: Math.random() * 2 + 1,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const draw = () => {
|
||||||
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
|
||||||
|
particles.forEach((p, i) => {
|
||||||
|
p.x += p.vx;
|
||||||
|
p.y += p.vy;
|
||||||
|
|
||||||
|
if (p.x < 0 || p.x > width) p.vx *= -1;
|
||||||
|
if (p.y < 0 || p.y > height) p.vy *= -1;
|
||||||
|
|
||||||
|
|
||||||
|
if (mouse.x !== null && mouse.y !== null) {
|
||||||
|
const dx = mouse.x - p.x;
|
||||||
|
const dy = mouse.y - p.y;
|
||||||
|
const dist = Math.hypot(dx, dy);
|
||||||
|
|
||||||
|
if (dist < mouse.radius) {
|
||||||
|
const angle = Math.atan2(dy, dx);
|
||||||
|
const force = (mouse.radius - dist) / mouse.radius;
|
||||||
|
p.x -= Math.cos(angle) * force * 2;
|
||||||
|
p.y -= Math.sin(angle) * force * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = "rgba(99, 102, 241, 0.7)";
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
|
||||||
|
for (let j = i + 1; j < particles.length; j++) {
|
||||||
|
const p2 = particles[j];
|
||||||
|
const dist = Math.hypot(p.x - p2.x, p.y - p2.y);
|
||||||
|
if (dist < 140) {
|
||||||
|
const opacity = 1 - dist / 140;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(p.x, p.y);
|
||||||
|
ctx.lineTo(p2.x, p2.y);
|
||||||
|
ctx.strokeStyle = `rgba(99, 102, 241, ${opacity * 0.25})`;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
animationFrameId = requestAnimationFrame(draw);
|
||||||
|
};
|
||||||
|
|
||||||
|
draw();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
window.removeEventListener("mouseleave", handleMouseLeave);
|
||||||
|
window.removeEventListener("resize", handleResize);
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 pointer-events-none z-0 bg-slate-950 overflow-hidden">
|
||||||
|
<div className="absolute top-1/4 -left-20 w-96 h-96 bg-indigo-600/20 rounded-full blur-3xl animate-pulse" />
|
||||||
|
<div className="absolute bottom-1/4 -right-20 w-96 h-96 bg-blue-600/20 rounded-full blur-3xl animate-pulse delay-1000" />
|
||||||
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] h-[500px] bg-purple-600/10 rounded-full blur-3xl" />
|
||||||
|
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
70
app/components/Animation/HeaderLoginAnimation.js
Normal file
70
app/components/Animation/HeaderLoginAnimation.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { motion } from "framer-motion";
|
||||||
|
export const particles = Array.from({ length: 20 }, (_, i) => ({
|
||||||
|
id: i,
|
||||||
|
x: Math.random() * 100,
|
||||||
|
y: Math.random() * 100,
|
||||||
|
size: Math.random() * 3 + 1,
|
||||||
|
duration: Math.random() * 15 + 10,
|
||||||
|
delay: Math.random() * 5,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const containerVariants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: { staggerChildren: 0.1, delayChildren: 0.2 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const itemVariants = {
|
||||||
|
hidden: { y: 20, opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
y: 0,
|
||||||
|
opacity: 1,
|
||||||
|
transition: { type: "spring", stiffness: 100 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function HeaderAnimation() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
|
{particles.map((p) => (
|
||||||
|
<motion.div
|
||||||
|
key={p.id}
|
||||||
|
className="absolute rounded-full bg-amber-500/20"
|
||||||
|
style={{
|
||||||
|
left: `${p.x}%`,
|
||||||
|
top: `${p.y}%`,
|
||||||
|
width: p.size,
|
||||||
|
height: p.size,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
y: [0, -20, 0],
|
||||||
|
x: [0, 10, -10, 0],
|
||||||
|
opacity: [0.2, 0.4, 0.2],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: p.duration,
|
||||||
|
repeat: Infinity,
|
||||||
|
delay: p.delay,
|
||||||
|
ease: "linear",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Glow orbs */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-20 left-20 w-64 h-64 bg-amber-500/10 rounded-full blur-3xl"
|
||||||
|
animate={{ scale: [1, 1.2, 1], x: [0, 30, 0], y: [0, -20, 0] }}
|
||||||
|
transition={{ duration: 12, repeat: Infinity }}
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
className="absolute bottom-20 right-20 w-80 h-80 bg-blue-500/10 rounded-full blur-3xl"
|
||||||
|
animate={{ scale: [1, 1.3, 1], x: [0, -30, 0], y: [0, 20, 0] }}
|
||||||
|
transition={{ duration: 15, repeat: Infinity }}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
app/components/Animation/OwnerPageAnimation.js
Normal file
26
app/components/Animation/OwnerPageAnimation.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
export const fadeInUp = {
|
||||||
|
initial: { opacity: 0, y: 20 },
|
||||||
|
animate: { opacity: 1, y: 0 },
|
||||||
|
transition: { duration: 0.5 },
|
||||||
|
};
|
||||||
|
export const staggerContainer = {
|
||||||
|
animate: { transition: { staggerChildren: 0.1 } },
|
||||||
|
};
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export const BackgroundElements = () => {
|
||||||
|
const circles = [
|
||||||
|
{ style: { top: "20%", right: "20%", width: "256px", height: "256px" }, className: "bg-amber-500/5" },
|
||||||
|
{ style: { bottom: "10%", left: "20%", width: "320px", height: "320px" }, className: "bg-blue-500/5" },
|
||||||
|
{ style: { bottom: "40%", left: "10%", width: "320px", height: "320px" }, className: "bg-blue-500/5" },
|
||||||
|
{ style: { bottom: "60%", left: "60%", width: "320px", height: "320px" }, className: "bg-blue-500/5" }
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{circles.map((circle, index) => (
|
||||||
|
<div key={index} style={circle.style} className={`absolute rounded-full pointer-events-none ${circle.className}`} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,12 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Home, Building, Calendar, Heart, Bell, Settings } from "lucide-react";
|
import { usePathname } from "next/navigation";
|
||||||
|
import { Home, Building2, Building, CalendarCheck, CalendarDays, ClipboardList, CreditCard, BookOpenCheck } from "lucide-react";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useNotifications } from "@/app/contexts/NotificationsContext";
|
import { useTranslation } from "react-i18next";
|
||||||
|
export default function BottomNav({ isOwner, isOwnerOrAgent }) {
|
||||||
export default function BottomNav({ isOwner }) {
|
const { t } = useTranslation();
|
||||||
const { unreadCount } = useNotifications();
|
const pathname = usePathname();
|
||||||
const [isMounted, setIsMounted] = useState(false);
|
const [isMounted, setIsMounted] = useState(false);
|
||||||
const bookingsHref = isOwner ? "/owner/reservations" : "/reservations";
|
const bookingsHref = isOwner ? "/owner/reservations" : "/reservations";
|
||||||
|
|
||||||
@ -15,43 +16,95 @@ export default function BottomNav({ isOwner }) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{ href: "/", label: "الرئيسية", icon: Home },
|
{ href: "/", label: t("home"), icon: Home },
|
||||||
{ href: "/properties", label: "عقاراتنا", icon: Building },
|
{ href: "/properties", label: t("ourProperties"), icon: Building2 },
|
||||||
{ href: bookingsHref, label: "الحجوزات", icon: Calendar },
|
|
||||||
{ href: "/favorites", label: "المفضلة", icon: Heart },
|
...(isOwnerOrAgent
|
||||||
{ href: "/notifications", label: "الإشعارات", icon: Bell, badge: isMounted ? unreadCount : 0 },
|
? [
|
||||||
{ href: "/settings", label: "الإعدادات", icon: Settings },
|
{ href: "/owner/properties", label: t("myProperties"), icon: Building },
|
||||||
|
{ href: "/owner/bookings", label: t("Myseizedproperties"), icon: CalendarCheck },
|
||||||
|
{ href: "/reservations", label: t("Mybookings"), icon: CalendarDays },
|
||||||
|
]
|
||||||
|
: [{ href: "/booked-properties", label: t("Mybookings"), icon: CalendarDays }]),
|
||||||
|
|
||||||
|
{ href: bookingsHref, label: t("Orders"), icon: ClipboardList },
|
||||||
|
{ href: "/payments", label: t("payments"), icon: CreditCard },
|
||||||
|
|
||||||
|
...(isOwnerOrAgent ? [{ href: "/owner/account-book", label: t("accountBook"), icon: BookOpenCheck }] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const isActive = (href) => {
|
||||||
|
if (href === "/") return pathname === "/";
|
||||||
|
return pathname.startsWith(href);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-50">
|
<div className="fixed bottom-0 left-0 right-0 z-50 flex justify-center px-5 pb-4 pt-4 pointer-events-none">
|
||||||
<div className="bg-white/95 backdrop-blur-sm border border-gray-200 rounded-3xl shadow-lg px-2 py-2 flex items-center gap-3">
|
<nav
|
||||||
{items.map((it) => {
|
className="
|
||||||
const Icon = it.icon;
|
w-fit max-w-full
|
||||||
return (
|
rounded-3xl
|
||||||
<Link
|
bg-gray-50
|
||||||
key={it.href}
|
px-5 py-3 sm:px-4
|
||||||
href={it.href}
|
shadow-[0_4px_24px_rgba(251,146,60,0.25)]
|
||||||
className="relative group flex flex-col items-center justify-center px-2.5 py-2 text-gray-700 hover:text-amber-600 transition-colors"
|
pointer-events-auto
|
||||||
>
|
"
|
||||||
<div className="relative">
|
>
|
||||||
<Icon className="w-6 h-6" />
|
<div
|
||||||
{it.badge > 0 && (
|
className="flex items-center sm:justify-center gap-0.5 sm:gap-1 overflow-x-auto max-w-full
|
||||||
<div className="absolute -top-2 -right-2 bg-red-500 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center">
|
[&::-webkit-scrollbar]:h-1.5
|
||||||
{it.badge > 9 ? "9+" : it.badge}
|
[&::-webkit-scrollbar-track]:bg-gray-100
|
||||||
</div>
|
[&::-webkit-scrollbar-thumb]:bg-gray-700
|
||||||
)}
|
[&::-webkit-scrollbar-thumb]:rounded-full hover:[&::-webkit-scrollbar-thumb]:bg-gray-700"
|
||||||
</div>
|
>
|
||||||
<div className="absolute -top-10 left-1/2 -translate-x-1/2 bg-gray-800 text-white text-xs rounded-lg px-2.5 py-1.5 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none shadow-lg">
|
{items.map((it) => {
|
||||||
{it.label}
|
const Icon = it.icon;
|
||||||
</div>
|
const active = isActive(it.href);
|
||||||
<div className="mt-1 text-xs text-center" aria-hidden>
|
|
||||||
{it.label}
|
return (
|
||||||
</div>
|
<Link
|
||||||
</Link>
|
key={it.href}
|
||||||
);
|
href={it.href}
|
||||||
})}
|
style={{ padding: "3px", width: "115px" }}
|
||||||
</div>
|
className={`
|
||||||
</nav>
|
flex
|
||||||
|
flex-col
|
||||||
|
items-center
|
||||||
|
justify-center
|
||||||
|
rounded-xl
|
||||||
|
transition-all
|
||||||
|
duration-200
|
||||||
|
ease-out
|
||||||
|
${active ? "bg-white shadow-sm text-amber-600" : "text-gray-700 hover:bg-white/80 hover:text-amber-600 hover:shadow-md"}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div className="relative">
|
||||||
|
<Icon className="h-5 w-5 sm:h-6 sm:w-6" />
|
||||||
|
|
||||||
|
{it.badge > 0 && (
|
||||||
|
<div
|
||||||
|
className="
|
||||||
|
absolute -right-2 -top-2
|
||||||
|
flex h-6 w-7 sm:h-5 sm:w-5
|
||||||
|
items-center justify-center
|
||||||
|
rounded-full
|
||||||
|
bg-red-500
|
||||||
|
text-[9px] sm:text-xs
|
||||||
|
font-bold
|
||||||
|
text-white
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{it.badge > 9 ? "9+" : it.badge}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-0.5 text-[10px] sm:mt-1 text-center sm:text-xs">{it.label}</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,11 +4,13 @@ import { useState } from 'react';
|
|||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Heart, Bell, CreditCard, Settings } from 'lucide-react';
|
import { Heart, Bell, CreditCard, Settings } from 'lucide-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useFavorites } from '@/app/contexts/FavoritesContext';
|
import { useFavorites } from '@/app/contexts/FavoritesContext';
|
||||||
import { useNotifications } from '@/app/contexts/NotificationsContext';
|
import { useNotifications } from '@/app/contexts/NotificationsContext';
|
||||||
import AuthService from '@/app/services/AuthService';
|
import AuthService from '@/app/services/AuthService';
|
||||||
|
|
||||||
export default function FloatingSidebar({ isRTL }) {
|
export default function FloatingSidebar({ isRTL }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { favorites } = useFavorites();
|
const { favorites } = useFavorites();
|
||||||
const { unreadCount } = useNotifications();
|
const { unreadCount } = useNotifications();
|
||||||
const [tooltip, setTooltip] = useState(null);
|
const [tooltip, setTooltip] = useState(null);
|
||||||
@ -88,7 +90,7 @@ export default function FloatingSidebar({ isRTL }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
{renderTooltip('favorites', 'المفضلة')}
|
{renderTooltip('favorites', t('favorites'))}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="relative group"
|
className="relative group"
|
||||||
@ -116,7 +118,7 @@ export default function FloatingSidebar({ isRTL }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
{renderTooltip('notifications', 'الإشعارات')}
|
{renderTooltip('notifications', t('notifications'))}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="relative group"
|
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" />
|
<CreditCard className="w-7 h-7 text-gray-600 transition-colors group-hover:text-amber-600" />
|
||||||
</Link>
|
</Link>
|
||||||
{renderTooltip('payments', 'المدفوعات')}
|
{renderTooltip('payments', t('payments'))}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="relative group"
|
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" />
|
<Settings className="w-7 h-7 text-gray-600 transition-colors group-hover:text-amber-600" />
|
||||||
</Link>
|
</Link>
|
||||||
{renderTooltip('settings', 'الإعدادات')}
|
{renderTooltip('settings', t('settings'))}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
17
app/components/HavAccount.js
Normal file
17
app/components/HavAccount.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { fadeInUp } from "./Animation/OwnerPageAnimation";
|
||||||
|
import Link from "next/link";
|
||||||
|
export default function HavAccount() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<motion.p variants={fadeInUp} className="text-center text-gray-400 mt-4">
|
||||||
|
{t("register.haveAccount")}
|
||||||
|
<Link href="/login" className="text-blue-400 hover:text-blue-300 font-medium transition-colors">
|
||||||
|
{t("register.loginLink")}
|
||||||
|
</Link>
|
||||||
|
</motion.p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
app/components/LoginComponent/CreateNewAccount.js
Normal file
17
app/components/LoginComponent/CreateNewAccount.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { itemVariants } from '../Animation/HeaderLoginAnimation';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
export default function CreateNewAccount() {
|
||||||
|
const {t}=useTranslation()
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<motion.p 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
45
app/components/LoginComponent/HeaderLogin.js
Normal file
45
app/components/LoginComponent/HeaderLogin.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { itemVariants } from "../Animation/HeaderLoginAnimation";
|
||||||
|
import { ArrowLeft, KeyRound,Home } from "lucide-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Link from "next/link";
|
||||||
|
//Back Link
|
||||||
|
export function BackLink() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<motion.div variants={itemVariants} className="absolute -top-16 left-0">
|
||||||
|
<Link href="/" className="group flex items-center gap-2 text-gray-400 hover:text-white transition-colors">
|
||||||
|
<motion.div whileHover={{ x: -5 }} transition={{ type: "spring", stiffness: 400 }}>
|
||||||
|
<ArrowLeft className="w-4 h-4" />
|
||||||
|
</motion.div>
|
||||||
|
<span>{t("backToHome")}</span>
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HeaderForm({ step }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="bg-gradient-to-r from-amber-700 to-amber-400 p-8 text-center relative overflow-hidden">
|
||||||
|
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ delay: 0.2, type: "spring" }} className="absolute -top-10 -right-10 w-40 h-40 bg-white/10 rounded-full" />
|
||||||
|
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ delay: 0.3, type: "spring" }} className="absolute -bottom-10 -left-10 w-40 h-40 bg-white/10 rounded-full" />
|
||||||
|
|
||||||
|
<motion.div initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} transition={{ delay: 0.2 }} className="relative z-10">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{step === "otp" ? <KeyRound className="w-10 h-10 text-white" /> : <Home className="w-10 h-10 text-white" />}
|
||||||
|
</motion.div>
|
||||||
|
<h1 className="text-3xl font-bold text-white mb-2">SweetHome</h1>
|
||||||
|
<p className="text-amber-100">{step === "otp" ? t("enterOTP") : t("welcomeBack")}</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
127
app/components/LoginComponent/Login.jsx
Normal file
127
app/components/LoginComponent/Login.jsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
"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 { useRouter } from "next/navigation";
|
||||||
|
import AuthService from "../../services/AuthService";
|
||||||
|
import InteractiveBackground from "../Animation/Background"
|
||||||
|
import { BackLink, HeaderForm } from "./HeaderLogin";
|
||||||
|
import HeaderAnimation, { containerVariants, itemVariants } from "../Animation/HeaderLoginAnimation";
|
||||||
|
import TabsLogin from "./Tabs";
|
||||||
|
import LoginForm from "./LoginForm";
|
||||||
|
import CreateNewAccount from "./CreateNewAccount";
|
||||||
|
import PolicyAndPrivecy from "./PolicyAndPrivecy";
|
||||||
|
import OtpComponent from "./OtpComponent";
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [step, setStep] = useState("login");
|
||||||
|
const [loginMethod, setLoginMethod] = useState("email");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isSuccess, setIsSuccess] = useState(false);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
credential: "", //email or phoneNumber
|
||||||
|
password: "",
|
||||||
|
rememberMe: false,
|
||||||
|
});
|
||||||
|
const [errors, setErrors] = useState({});
|
||||||
|
const validateForm = () => {
|
||||||
|
const newErrors = {};
|
||||||
|
if (!formData.credential) {
|
||||||
|
newErrors.credential = loginMethod === "email" ? t("emailRequired") : t("phoneRequired");
|
||||||
|
}
|
||||||
|
if (!formData.password) {
|
||||||
|
newErrors.password = t("passwordRequired");
|
||||||
|
}
|
||||||
|
setErrors(newErrors);
|
||||||
|
return Object.keys(newErrors).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogin = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!validateForm()) return;
|
||||||
|
setIsLoading(true);
|
||||||
|
setErrors({});
|
||||||
|
try {
|
||||||
|
const response = await AuthService.login({
|
||||||
|
loginMethod,
|
||||||
|
credential: formData.credential,
|
||||||
|
password: formData.password,
|
||||||
|
});
|
||||||
|
switch (response.type) {
|
||||||
|
case "SUCCESS":
|
||||||
|
toast.success(t("loginSuccess"));
|
||||||
|
router.push("/");
|
||||||
|
break;
|
||||||
|
case "OTP_REQUIRED":
|
||||||
|
toast(t("otpRequired"));
|
||||||
|
setStep("otp");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
toast.error("Login failed the email or password is wrong");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[Login] Error:", err);
|
||||||
|
toast.error(err.message || t("connectionError"), {
|
||||||
|
style: { background: "#fee2e2", color: "#991b1b" },
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-detect login method from input
|
||||||
|
const handleCredentialChange = (value) => {
|
||||||
|
setFormData({ ...formData, credential: value });
|
||||||
|
if (errors.credential) setErrors({ ...errors, credential: null });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handelTabs = (type) => {
|
||||||
|
setLoginMethod(type);
|
||||||
|
setFormData({ ...formData, credential: "" });
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center p-4 relative overflow-hidden">
|
||||||
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
<InteractiveBackground />
|
||||||
|
{/* Particles */}
|
||||||
|
<HeaderAnimation />
|
||||||
|
<motion.div variants={containerVariants} initial="hidden" animate="visible" className="relative w-full max-w-md z-10">
|
||||||
|
{/* Back link */}
|
||||||
|
<BackLink />
|
||||||
|
<motion.div variants={itemVariants} className="bg-white/10 backdrop-blur-2xl rounded-3xl shadow-2xl border border-white/10 overflow-hidden">
|
||||||
|
{/* Header */}
|
||||||
|
<HeaderForm step={step} />
|
||||||
|
<div className="p-8">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{step === "login" ? (
|
||||||
|
<motion.form key="login" initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: 20 }} onSubmit={handleLogin} className="space-y-6">
|
||||||
|
{/* Login method tabs */}
|
||||||
|
<TabsLogin loginMethod={loginMethod} handelTabs={handelTabs} />
|
||||||
|
{/* Form Login */}
|
||||||
|
<LoginForm
|
||||||
|
loginMethod={loginMethod}
|
||||||
|
errors={errors}
|
||||||
|
formData={formData}
|
||||||
|
isLoading={isLoading}
|
||||||
|
isSuccess={isSuccess}
|
||||||
|
setFormData={setFormData}
|
||||||
|
handleCredentialChange={handleCredentialChange}
|
||||||
|
/>
|
||||||
|
</motion.form>
|
||||||
|
) : (
|
||||||
|
// /* OTP Verification Step */
|
||||||
|
<OtpComponent isLoading={isLoading} setIsLoading={setIsLoading} isSuccess={isSuccess} setIsSuccess={setIsSuccess} loginMethod={loginMethod} formData={formData} setStep={setStep} />
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
<CreateNewAccount />
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
<PolicyAndPrivecy />
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
141
app/components/LoginComponent/LoginForm.js
Normal file
141
app/components/LoginComponent/LoginForm.js
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import { EyeOff, Mail, Phone, Eye, LogIn, Lock, Loader2, CheckCircle } from "lucide-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default function LoginForm({ loginMethod, errors, formData, isLoading, isSuccess, setFormData, handleCredentialChange }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<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">
|
||||||
|
{loginMethod === "email" ? (
|
||||||
|
<Mail className={`w-5 h-5 transition-colors ${errors.credential ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
|
||||||
|
) : (
|
||||||
|
<Phone className={`w-5 h-5 transition-colors ${errors.credential ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{loginMethod === "email" ? (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
// type={loginMethod === 'email' ? 'email' : 'tel'}
|
||||||
|
value={formData.credential}
|
||||||
|
onChange={(e) => handleCredentialChange(e.target.value)}
|
||||||
|
className={`w-full pr-12 pl-4 py-4 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${
|
||||||
|
errors.credential ? "border-red-500" : "border-gray-700"
|
||||||
|
}`}
|
||||||
|
placeholder={t("emailPlaceholder")}
|
||||||
|
dir="ltr"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.credential}
|
||||||
|
onChange={(e) => handleCredentialChange(e.target.value)}
|
||||||
|
className={`w-full pr-12 pl-4 py-4 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${
|
||||||
|
errors.credential ? "border-red-500" : "border-gray-700"
|
||||||
|
}`}
|
||||||
|
placeholder={t("phonePlaceholder")}
|
||||||
|
dir="ltr"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{errors.credential && (
|
||||||
|
<motion.p initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} className="text-red-500 text-sm mt-1">
|
||||||
|
{errors.credential}
|
||||||
|
</motion.p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 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">
|
||||||
|
<Lock className={`w-5 h-5 transition-colors ${errors.password ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type={showPassword ? "text" : "password"}
|
||||||
|
value={formData.password}
|
||||||
|
onChange={(e) => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
password: e.target.value,
|
||||||
|
});
|
||||||
|
if (errors.password) setErrors({ ...errors, password: null });
|
||||||
|
}}
|
||||||
|
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-amber-50 placeholder-gray-500 transition-all ${
|
||||||
|
errors.password ? "border-red-500" : "border-gray-700"
|
||||||
|
}`}
|
||||||
|
placeholder={t("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 hover:text-gray-300 transition-colors" /> : <Eye className="w-5 h-5 text-gray-400 hover:text-gray-300 transition-colors" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{errors.password && (
|
||||||
|
<motion.p initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} className="text-red-500 text-sm mt-1">
|
||||||
|
{errors.password}
|
||||||
|
</motion.p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Remember + Forgot */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer group">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.rememberMe}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
rememberMe: e.target.checked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
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>
|
||||||
|
|
||||||
|
{/* Submit */}
|
||||||
|
<motion.button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading || isSuccess}
|
||||||
|
className="relative w-full bg-gradient-to-r from-amber-500 to-amber-600 text-white py-4 rounded-xl font-bold text-lg overflow-hidden group disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<span className="relative z-10 flex items-center justify-center gap-2">
|
||||||
|
{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>
|
||||||
|
</motion.button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
143
app/components/LoginComponent/OtpComponent.js
Normal file
143
app/components/LoginComponent/OtpComponent.js
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { CheckCircle, KeyRound, Loader2, Shield } from "lucide-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { sendEmailOTP, sendPhoneOTP, verifyEmail, verifyPhone, getOwnerByUserId, getCustomerByUserId } from "@/app/utils/api";
|
||||||
|
import AuthService from "@/app/services/AuthService";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
|
export default function OtpComponent({ isLoading, setIsLoading, isSuccess, setIsSuccess, loginMethod, formData, setStep }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const router = useRouter();
|
||||||
|
const [otpCode, setOtpCode] = useState("");
|
||||||
|
const [otpError, setOtpError] = useState("");
|
||||||
|
const handleVerifyOTP = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!otpCode || otpCode.length < 4) {
|
||||||
|
setOtpError(t("otpRequired"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsLoading(true);
|
||||||
|
setOtpError("");
|
||||||
|
try {
|
||||||
|
const verifyFn = loginMethod === "email" ? verifyEmail : verifyPhone;
|
||||||
|
const result = await verifyFn(otpCode);
|
||||||
|
if (result?.ok) {
|
||||||
|
try {
|
||||||
|
await AuthService.cacheCurrentUser(getOwnerByUserId, getCustomerByUserId);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Failed to cache user", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setIsSuccess(true);
|
||||||
|
toast.success(t("verifySuccess"));
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push("/");
|
||||||
|
}, 1500);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[OTP] Error:", err);
|
||||||
|
setOtpError(err.message || t("verifyError"));
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resendOTP = async () => {
|
||||||
|
try {
|
||||||
|
if (loginMethod === "email") {
|
||||||
|
await sendEmailOTP();
|
||||||
|
} else {
|
||||||
|
await sendPhoneOTP();
|
||||||
|
}
|
||||||
|
toast.success(t("resendSuccess"), {
|
||||||
|
style: { background: "#dcfce7", color: "#166534" },
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[OTP] Resend failed:", err);
|
||||||
|
toast.error(t("resendFailed"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.form key="otp" initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} onSubmit={handleVerifyOTP} className="space-y-6">
|
||||||
|
<div className="text-center mb-4">
|
||||||
|
<Shield className="w-12 h-12 text-amber-500 mx-auto mb-3" />
|
||||||
|
<p className="text-amber-300 text-sm">
|
||||||
|
{t("sendOTPTo")}{" "}
|
||||||
|
<span className="text-amber-50 font-medium" dir="ltr">
|
||||||
|
{formData?.credential}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">{t("otpLabel")}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={otpCode}
|
||||||
|
onChange={(e) => {
|
||||||
|
setOtpCode(e.target.value);
|
||||||
|
if (otpError) setOtpError("");
|
||||||
|
}}
|
||||||
|
className={`w-full px-4 py-4 bg-gray-800 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white text-center text-2xl tracking-[0.5em] placeholder-gray-500 transition-all ${
|
||||||
|
otpError ? "border-red-500" : "border-gray-700"
|
||||||
|
}`}
|
||||||
|
placeholder="______"
|
||||||
|
maxLength={6}
|
||||||
|
dir="ltr"
|
||||||
|
/>
|
||||||
|
{otpError && (
|
||||||
|
<motion.p initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} className="text-red-500 text-sm mt-1 text-center">
|
||||||
|
{otpError}
|
||||||
|
</motion.p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading || isSuccess}
|
||||||
|
className="relative w-full bg-gradient-to-r from-amber-500 to-amber-600 text-white py-4 rounded-xl font-bold text-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-center gap-2">
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-5 h-5 animate-spin" />
|
||||||
|
{t("verifying")}
|
||||||
|
</>
|
||||||
|
) : isSuccess ? (
|
||||||
|
<>
|
||||||
|
<CheckCircle className="w-5 h-5" />
|
||||||
|
{t("success")}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<KeyRound className="w-5 h-5" />
|
||||||
|
{t("verify")}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setStep("login");
|
||||||
|
setOtpCode("");
|
||||||
|
setOtpError("");
|
||||||
|
}}
|
||||||
|
className="text-gray-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
← {t("back")}
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={resendOTP} className="text-amber-400 hover:text-amber-300 transition-colors">
|
||||||
|
{t("resendCode")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.form>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
app/components/LoginComponent/PolicyAndPrivecy.js
Normal file
21
app/components/LoginComponent/PolicyAndPrivecy.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { itemVariants } from "../Animation/HeaderLoginAnimation";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Link from "next/link";
|
||||||
|
export default function PolicyAndPrivecy() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<motion.p 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
app/components/LoginComponent/Tabs.js
Normal file
36
app/components/LoginComponent/Tabs.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Mail, Phone } from "lucide-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export default function TabsLogin({ loginMethod, handelTabs }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex gap-2 bg-white/5 p-1 rounded-xl">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
handelTabs("email");
|
||||||
|
}}
|
||||||
|
className={`flex-1 py-2.5 rounded-lg text-sm font-medium transition-all flex items-center justify-center gap-2 ${
|
||||||
|
loginMethod === "email" ? "bg-amber-500 text-white shadow-lg" : "text-gray-400 hover:text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Mail className="w-4 h-4" />
|
||||||
|
{t("emailMethod")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
handelTabs("phone");
|
||||||
|
}}
|
||||||
|
className={`flex-1 py-2.5 rounded-lg text-sm font-medium transition-all flex items-center justify-center gap-2 ${
|
||||||
|
loginMethod === "phone" ? "bg-amber-500 text-white shadow-lg" : "text-gray-400 hover:text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Phone className="w-4 h-4" />
|
||||||
|
{t("phoneMethod")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -3,7 +3,7 @@ import { usePathname } from 'next/navigation';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { LogIn, UserPlus } from 'lucide-react';
|
import { LogIn, UserPlus } from 'lucide-react';
|
||||||
|
|
||||||
export function NavLink({ href, children }) {
|
export function NavLink({ href, children, light }) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const isActive = pathname === href || pathname.startsWith(href + '/');
|
const isActive = pathname === href || pathname.startsWith(href + '/');
|
||||||
return (
|
return (
|
||||||
@ -11,19 +11,19 @@ export function NavLink({ href, children }) {
|
|||||||
href={href}
|
href={href}
|
||||||
className={`px-5 py-3 rounded-lg font-semibold transition-all duration-300 text-lg relative group ${
|
className={`px-5 py-3 rounded-lg font-semibold transition-all duration-300 text-lg relative group ${
|
||||||
isActive
|
isActive
|
||||||
? 'text-amber-600 bg-amber-50'
|
? light ? 'text-amber-300 bg-white/10' : 'text-amber-600 bg-amber-50'
|
||||||
: 'text-gray-700 hover:text-amber-600 hover:bg-gray-100'
|
: light ? 'text-white/90 hover:text-amber-300 hover:bg-white/10' : 'text-gray-700 hover:text-amber-600 hover:bg-gray-100'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<span className={`absolute bottom-0 left-1/2 transform -translate-x-1/2 h-0.5 transition-all duration-300 ${
|
<span className={`absolute bottom-0 left-1/2 transform -translate-x-1/2 h-0.5 transition-all duration-300 ${
|
||||||
isActive ? 'w-4/5 bg-amber-600' : 'w-0 bg-amber-600 group-hover:w-4/5'
|
isActive ? 'w-4/5 bg-amber-400' : 'w-0 bg-amber-400 group-hover:w-4/5'
|
||||||
}`}></span>
|
}`}></span>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MobileNavLink({ href, children, onClick }) {
|
export function MobileNavLink({ href, children, onClick, light }) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const isActive = pathname === href || pathname.startsWith(href + '/');
|
const isActive = pathname === href || pathname.startsWith(href + '/');
|
||||||
return (
|
return (
|
||||||
@ -32,8 +32,8 @@ export function MobileNavLink({ href, children, onClick }) {
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={`block px-3 py-2 rounded-md text-base font-medium transition-colors ${
|
className={`block px-3 py-2 rounded-md text-base font-medium transition-colors ${
|
||||||
isActive
|
isActive
|
||||||
? 'text-amber-600 bg-amber-50'
|
? light ? 'text-amber-300 bg-white/10' : 'text-amber-600 bg-amber-50'
|
||||||
: 'text-gray-700 hover:text-amber-600 hover:bg-gray-50'
|
: light ? 'text-white/80 hover:text-amber-300 hover:bg-white/10' : 'text-gray-700 hover:text-amber-600 hover:bg-gray-50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState, useRef } from "react";
|
import { useEffect, useState, useRef } from "react";
|
||||||
import { initializeApp, getApps } from "firebase/app";
|
import { initializeApp, getApps } from "firebase/app";
|
||||||
import { getMessaging, getToken, onMessage } from "firebase/messaging";
|
import { getMessaging, getToken, onMessage } from "firebase/messaging";
|
||||||
import AuthService from "../services/AuthService";
|
import AuthService from "../services/AuthService";
|
||||||
import { setFCMToken } from "../utils/api";
|
import { setFCMToken } from "../utils/api";
|
||||||
|
|
||||||
|
//فاير بيز
|
||||||
const firebaseConfig = {
|
const firebaseConfig = {
|
||||||
apiKey: "AIzaSyBZV7KBLRJSTApahfrO8lBesmIM3zNRSaY",
|
apiKey: "AIzaSyBZV7KBLRJSTApahfrO8lBesmIM3zNRSaY",
|
||||||
authDomain: "sweet-home-b2766.firebaseapp.com",
|
authDomain: "sweet-home-b2766.firebaseapp.com",
|
||||||
@ -16,6 +16,7 @@ const firebaseConfig = {
|
|||||||
measurementId: "G-M2V95NBJLX",
|
measurementId: "G-M2V95NBJLX",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//عم نعمل تهيئة للاعدادات
|
||||||
const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
|
const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
|
||||||
|
|
||||||
export default function NotificationHandler() {
|
export default function NotificationHandler() {
|
||||||
@ -26,11 +27,10 @@ export default function NotificationHandler() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function checkAuth() {
|
function checkAuth() {
|
||||||
if (initialized.current) return;
|
if (initialized.current) return;
|
||||||
|
|
||||||
if (!AuthService.getToken()) return;
|
if (!AuthService.getToken()) return;
|
||||||
initialized.current = true;
|
initialized.current = true;
|
||||||
|
|
||||||
if ("Notification" in window) {
|
if ("Notification" in window) {
|
||||||
|
//عم نشوف اذا حط سماح او رفض
|
||||||
if (Notification.permission === "default") {
|
if (Notification.permission === "default") {
|
||||||
setShowPrompt(true);
|
setShowPrompt(true);
|
||||||
} else if (Notification.permission === "granted") {
|
} else if (Notification.permission === "granted") {
|
||||||
@ -91,7 +91,6 @@ export default function NotificationHandler() {
|
|||||||
|
|
||||||
// This MUST be synchronous from a user gesture
|
// This MUST be synchronous from a user gesture
|
||||||
const permission = await Notification.requestPermission();
|
const permission = await Notification.requestPermission();
|
||||||
console.log("[FCM] Permission result:", permission);
|
|
||||||
|
|
||||||
if (permission === "granted") {
|
if (permission === "granted") {
|
||||||
await setupFCM();
|
await setupFCM();
|
||||||
|
|||||||
25
app/components/OwnerComponents/ButtomStepOne.js
Normal file
25
app/components/OwnerComponents/ButtomStepOne.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
export default function ButtomStepOne({ type }) {
|
||||||
|
const router = useRouter();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<>
|
||||||
|
<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 ${type === "owner" ? " from-amber-500 to-amber-600" : " from-blue-500 to-blue-600"} text-white py-3 px-4 rounded-xl font-medium ${type === "onwer" ? "hover:from-amber-600 hover:to-amber-700" : "hover:from-blue-600 hover:to-blue-700"} transition-all`}
|
||||||
|
>
|
||||||
|
{t("register.next")}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
app/components/OwnerComponents/ButtomStepTwo.js
Normal file
27
app/components/OwnerComponents/ButtomStepTwo.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export default function ButtomStepTwo({ setStep, isLoading, formData, type }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<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${type === "owner" ? " from-amber-500 to-amber-600" : " from-blue-500 to-blue-600"} text-amber-50 py-3 px-4 rounded-xl font-medium${type === "onwer" ? "hover:from-amber-600 hover:to-amber-700" : "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>{t("register.registering")}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
t("register.createAccount")
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
6
app/components/OwnerComponents/FormOwner.js
Normal file
6
app/components/OwnerComponents/FormOwner.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
export default function FormOwner(){
|
||||||
|
return(<>
|
||||||
|
|
||||||
|
</>)
|
||||||
|
}
|
||||||
44
app/components/OwnerComponents/HeaderOfForm.js
Normal file
44
app/components/OwnerComponents/HeaderOfForm.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { Building, Home } from "lucide-react";
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
{
|
||||||
|
colors: "from-amber-500 to-amber-600",
|
||||||
|
titleKey: "register.owner.step1Title",
|
||||||
|
title2Key: "register.owner.step1bTitle",
|
||||||
|
descKey: "register.owner.step1Desc",
|
||||||
|
desc2Key: "register.owner.step1bDesc",
|
||||||
|
icon: <Building className="w-10 h-10 text-white" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colors: "from-blue-500 to-blue-600",
|
||||||
|
titleKey: "register.tenant.step1Title",
|
||||||
|
title2Key: "register.tenant.step2Title",
|
||||||
|
descKey: "register.tenant.step1Desc",
|
||||||
|
desc2Key: "register.tenant.step2Desc",
|
||||||
|
icon: <Home className="w-10 h-10 text-white" />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
export default function HeaderOfForm({ step = 1, num = 0 }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const currentData = data[num] || data[0];
|
||||||
|
return (
|
||||||
|
<div className={`bg-gradient-to-r ${currentData.colors} p-8 text-center relative overflow-hidden`}>
|
||||||
|
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ delay: 0.2, type: "spring" }} className="absolute -top-10 -right-10 w-40 h-40 bg-white/10 rounded-full" />
|
||||||
|
<motion.div initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} className="relative z-10">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{data[num].icon}
|
||||||
|
</motion.div>
|
||||||
|
<h1 className="text-3xl font-bold text-white mb-2">{t(step === 1 ? t(currentData.titleKey) : t(currentData.title2Key))}</h1>
|
||||||
|
<p className="text-amber-100">{t(step === 1 ? t(currentData.descKey) : t(currentData.desc2Key))}</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
28
app/components/OwnerComponents/HeaderOfSteps.js
Normal file
28
app/components/OwnerComponents/HeaderOfSteps.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { ArrowLeft } from "lucide-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export default function HeaderOfSteps({ step, colors, numberStep }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<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>{t("forgotPassword.backToLogin")}</span>
|
||||||
|
</Link>
|
||||||
|
<span className="text-sm text-gray-200">{t("register.stepOf2", { step: step })}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{numberStep.map((s) => (
|
||||||
|
<motion.div key={s} className={`h-2 flex-1 rounded-full ${step >= s ? colors : "bg-gray-800"}`} animate={{ scaleX: step >= s ? 1 : 0.5 }} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
104
app/components/OwnerComponents/OtpDialog.js
Normal file
104
app/components/OwnerComponents/OtpDialog.js
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { KeyRound, Loader2, Shield } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import toast, { Toaster } from "react-hot-toast";
|
||||||
|
import { sendEmailOTP, verifyEmail } from "@/app/utils/api";
|
||||||
|
import AuthService from "@/app/services/AuthService";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
export default function OtpDialog({ formData, setShowOtpModal, type }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const router = useRouter();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [otpCode, setOtpCode] = useState("");
|
||||||
|
const handleVerifyOTP = async () => {
|
||||||
|
if (!otpCode || otpCode.length < 4) {
|
||||||
|
toast.error(t("register.otpRequired"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await verifyEmail(otpCode);
|
||||||
|
if (res.status === 200) {
|
||||||
|
AuthService.deleteToken();
|
||||||
|
toast.success(res.message || t("register.emailVerified"));
|
||||||
|
setShowOtpModal(false);
|
||||||
|
setTimeout(() => router.push("/login"), 1500);
|
||||||
|
} else {
|
||||||
|
toast.error(res.message || res.data?.message || t("accountVerification.otpInvalid"));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err.message || t("register.verificationError"));
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResendOTP = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await sendEmailOTP();
|
||||||
|
toast.success(t("register.newOtpSent"));
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(t("register.resendOtpFailed"));
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 z-50">
|
||||||
|
<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-amber-500/20 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||||
|
<Shield className={`w-8 h-8 ${type === "owner" ? "text-amber-500" : "text-blue-500"} `} />
|
||||||
|
</div>
|
||||||
|
<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={`${type === "owner" ? "text-amber-500" : "text-blue-500"} 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">
|
||||||
|
<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-amber-500 text-white text-center tracking-[0.5em] text-xl"
|
||||||
|
placeholder="------"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
onClick={handleVerifyOTP}
|
||||||
|
disabled={isLoading || !otpCode}
|
||||||
|
className={`flex-1 bg-gradient-to-r ${type === "owner" ? "from-amber-500 to-amber-600" : "from-blue-500 to-blue-600"} text-white py-3 rounded-xl font-medium ${type === "owner" ? "hover:from-amber-600 hover:to-amber-700" : "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>{t("register.verifying")}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
t("register.verify")
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleResendOTP}
|
||||||
|
disabled={isLoading}
|
||||||
|
className={`w-full text-center ${type === "owner" ? "text-amber-400 hover:text-amber-300" : "text-blue-400 hover:text-blue-300"} text-sm mt-3 disabled:opacity-50`}
|
||||||
|
>
|
||||||
|
{t("register.resendOtp")}
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
162
app/components/OwnerComponents/Owner.js
Normal file
162
app/components/OwnerComponents/Owner.js
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import toast, { Toaster } from "react-hot-toast";
|
||||||
|
import { addOwner, loginWithEmail } from "../../utils/api";
|
||||||
|
import AuthService from "../../services/AuthService";
|
||||||
|
import { OwnerType } from "../../enums";
|
||||||
|
import OtpDialog from "./OtpDialog";
|
||||||
|
import HeaderOfSteps from "./HeaderOfSteps";
|
||||||
|
import HeaderOfForm from "./HeaderOfForm";
|
||||||
|
import { BackgroundElements, fadeInUp, staggerContainer } from "../Animation/OwnerPageAnimation";
|
||||||
|
import { validateStep1, validateStep2 } from "../../validations/OwnerValidatoin";
|
||||||
|
import OwnerFormStepOne from "./OwnerFormStepOne";
|
||||||
|
import ButtomStepOne from "./ButtomStepOne";
|
||||||
|
import ButtomStepTwo from "./ButtomStepTwo";
|
||||||
|
import OwnerFormStepTwo from "./OwnerFormStepTwo";
|
||||||
|
import InteractiveBackground from "../Animation/Background";
|
||||||
|
|
||||||
|
export default function Owner() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [step, setStep] = useState(1);
|
||||||
|
const [showOtpModal, setShowOtpModal] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
firstName: "",
|
||||||
|
lastName: "",
|
||||||
|
email: "",
|
||||||
|
phone: "",
|
||||||
|
whatsapp: "",
|
||||||
|
phone2: "",
|
||||||
|
nationalNumber: "",
|
||||||
|
password: "",
|
||||||
|
confirmPassword: "",
|
||||||
|
ownerType: OwnerType.PERSON,
|
||||||
|
agreeTerms: false,
|
||||||
|
});
|
||||||
|
const [idImages, setIdImages] = useState({ front: null, back: null, license: null });
|
||||||
|
const [idImagePreviews, setIdImagePreviews] = useState({ front: "", back: "", license: "" });
|
||||||
|
const [errors, setErrors] = useState({});
|
||||||
|
|
||||||
|
const isCompany = Number(formData.ownerType) === OwnerType.REAL_ESTATE_AGENCY;
|
||||||
|
|
||||||
|
const handleNextStep = () => {
|
||||||
|
if (validateStep1(formData, setErrors, t)) {
|
||||||
|
setStep(2);
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
} else {
|
||||||
|
toast.error(t("validation.correctErrors"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!validateStep2(idImages, setErrors, t)) {
|
||||||
|
toast.error(t("register.completeRequiredImages"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!formData.agreeTerms) {
|
||||||
|
toast.error(t("validation.agreeToTerms"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsLoading(true);
|
||||||
|
const payload = {
|
||||||
|
firstName: formData.firstName,
|
||||||
|
lastName: formData.lastName,
|
||||||
|
email: formData.email,
|
||||||
|
phoneNumber: formData.phone || "",
|
||||||
|
whatsAppNumber: formData.whatsapp,
|
||||||
|
phone: formData.phone2,
|
||||||
|
nationalNumber: formData.nationalNumber,
|
||||||
|
password: formData.password,
|
||||||
|
ownerType: formData.ownerType,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const licenseImage = isCompany ? idImages.license : null;
|
||||||
|
const res = await addOwner(payload, idImages.front, idImages.back,licenseImage);
|
||||||
|
if (res.status === 200 || res.ok) {
|
||||||
|
toast.success(res.message || t("register.accountCreated")||res.data[0].message , { duration: 4000 });
|
||||||
|
const loginRes = await loginWithEmail(formData.email, formData.password);
|
||||||
|
if (loginRes.status === 206) {
|
||||||
|
const otpToken = loginRes.data;
|
||||||
|
if (otpToken) AuthService.addToken(otpToken);
|
||||||
|
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 || t("register.loginSuccess"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error(res.data[0].message || res.data?.message);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err.message || t("register.registrationError"));
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center p-4 relative overflow-hidden">
|
||||||
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
|
<BackgroundElements />
|
||||||
|
<InteractiveBackground />
|
||||||
|
</div>
|
||||||
|
<motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.5 }} className="relative z-10 w-full max-w-2xl">
|
||||||
|
<HeaderOfSteps step={step} numberStep={[1, 2]} colors={"bg-amber-500"} />
|
||||||
|
<motion.div
|
||||||
|
key={step}
|
||||||
|
initial={{ opacity: 0, x: 20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: -20 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="bg-white/5 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/10 overflow-hidden"
|
||||||
|
>
|
||||||
|
<HeaderOfForm step={step} num={0} />
|
||||||
|
<div className="p-8">
|
||||||
|
<motion.form
|
||||||
|
variants={staggerContainer}
|
||||||
|
initial="initial"
|
||||||
|
animate="animate"
|
||||||
|
onSubmit={
|
||||||
|
step === 1
|
||||||
|
? (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleNextStep();
|
||||||
|
}
|
||||||
|
: handleSubmit
|
||||||
|
}
|
||||||
|
className="space-y-6"
|
||||||
|
>
|
||||||
|
{step === 1 && (
|
||||||
|
<OwnerFormStepOne
|
||||||
|
setErrors={setErrors}
|
||||||
|
setFormData={setFormData}
|
||||||
|
formData={formData}
|
||||||
|
errors={errors}
|
||||||
|
isCompany={isCompany}
|
||||||
|
setIdImagePreviews={setIdImagePreviews}
|
||||||
|
setIdImages={setIdImages}
|
||||||
|
type={"owner"}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{step === 2 && (
|
||||||
|
<>
|
||||||
|
<OwnerFormStepTwo idImagePreviews={idImagePreviews} errors={errors} setIdImagePreviews={setIdImagePreviews} setIdImages={setIdImages} setFormData={setFormData} formData={formData} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<motion.div variants={fadeInUp} className="flex gap-3 pt-4">
|
||||||
|
{step === 1 ? ( <> <ButtomStepOne type={"owner"} /></>) : ( <> <ButtomStepTwo formData={formData} isLoading={isLoading} setStep={setStep} type={"owner"} /> </>)}
|
||||||
|
</motion.div>
|
||||||
|
</motion.form>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
<AnimatePresence>{showOtpModal && <OtpDialog formData={formData} setShowOtpModal={setShowOtpModal} type={"owner"} />}</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
271
app/components/OwnerComponents/OwnerFormStepOne.js
Normal file
271
app/components/OwnerComponents/OwnerFormStepOne.js
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { fadeInUp } from "../Animation/OwnerPageAnimation";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import { User, Mail, Phone, Lock, Eye, EyeOff, MessageCircle, Camera, X, CheckCircle, XCircle } from "lucide-react";
|
||||||
|
import { CustomerTypeLabels, OwnerTypeLabels } from "@/app/enums";
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
import { handleImageUpload } from "@/app/HelperFunction/UploadImage";
|
||||||
|
import UploadImage from "./UploadImage";
|
||||||
|
|
||||||
|
export default function OwnerFormStepOne({ formData, errors, setFormData, setErrors, isCompany, idImagePreviews, setIdImagePreviews, setIdImages, type }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||||
|
const fileInputLicenseRef = useRef(null);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<motion.div variants={fadeInUp} className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<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-amber-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-amber-500 focus:border-transparent text-amber-50 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">
|
||||||
|
{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-amber-500 focus:border-transparent text-amber-50 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">
|
||||||
|
{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-amber-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-amber-500 focus:border-transparent text-amber-50 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">
|
||||||
|
{t("register.phone")} <span className="text-gray-500">({t("register.optional")})</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 text-gray-400 group-focus-within:text-amber-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 border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all"
|
||||||
|
placeholder={t("register.phoneOptionalPlaceholder")}
|
||||||
|
/>
|
||||||
|
</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">
|
||||||
|
{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">
|
||||||
|
<MessageCircle className={`w-5 h-5 ${errors.whatsapp ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-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-amber-500 focus:border-transparent text-amber-50 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">
|
||||||
|
{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-amber-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-amber-500 focus:border-transparent text-amber-50 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">
|
||||||
|
{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-amber-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-amber-500 focus:border-transparent text-amber-50 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}>
|
||||||
|
{type === "owner" ? (
|
||||||
|
<>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||||
|
{t("register.ownerType")} <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={formData.ownerType.toString()}
|
||||||
|
onChange={(e) => {
|
||||||
|
const selectedType = parseInt(e.target.value, 10);
|
||||||
|
setFormData((prev) => ({ ...prev, ownerType: selectedType }));
|
||||||
|
}}
|
||||||
|
className="w-full py-3 px-4 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent appearance-none text-gray-900 cursor-pointer"
|
||||||
|
>
|
||||||
|
{Object.entries(OwnerTypeLabels).map(([value, label]) => (
|
||||||
|
<option key={value} value={value} className="bg-gray-900 ">
|
||||||
|
{label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<motion.div variants={fadeInUp}>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{isCompany && <UploadImage idImagePreviews={idImagePreviews} type={"license"} errors={errors.license} setIdImagePreviews={setIdImagePreviews} setIdImages={setIdImages} />}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
<motion.div variants={fadeInUp}>
|
||||||
|
<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-amber-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-amber-500 focus:border-transparent text-amber-50 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>
|
||||||
|
</div>
|
||||||
|
{errors.password && <p className="text-red-500 text-sm mt-1">{errors.password}</p>}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div variants={fadeInUp}>
|
||||||
|
<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-amber-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-amber-500 focus:border-transparent text-amber-50 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>
|
||||||
|
{formData.confirmPassword && (
|
||||||
|
<div className="absolute inset-y-0 left-12 flex items-center">
|
||||||
|
{formData.password === formData.confirmPassword ? <CheckCircle className="w-5 h-5 text-green-500" /> : <XCircle className="w-5 h-5 text-red-500" />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{errors.confirmPassword && <p className="text-red-500 text-sm mt-1">{errors.confirmPassword}</p>}
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
app/components/OwnerComponents/OwnerFormStepTwo.js
Normal file
12
app/components/OwnerComponents/OwnerFormStepTwo.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import Policy from "./PolicyAgree";
|
||||||
|
import UploadImage from "./UploadImage";
|
||||||
|
export default function OwnerFormStepTwo({idImagePreviews,errors,setIdImagePreviews,setIdImages,setFormData,formData}) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<UploadImage idImagePreviews={idImagePreviews} type={"front"} errors={errors.front} setIdImagePreviews={setIdImagePreviews} setIdImages={setIdImages} />
|
||||||
|
<UploadImage idImagePreviews={idImagePreviews} type={"back"} errors={errors.back} setIdImagePreviews={setIdImagePreviews} setIdImages={setIdImages} />
|
||||||
|
<Policy setFormData={setFormData} formData={formData} type={"owner"} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
31
app/components/OwnerComponents/PolicyAgree.js
Normal file
31
app/components/OwnerComponents/PolicyAgree.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { fadeInUp } from "../Animation/OwnerPageAnimation";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
export default function Policy({ formData, setFormData, type }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<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 ${type === "owner" ? `text-amber-500 focus:ring-amber-500` : ` text-blue-500 focus:ring-blue-500`} `}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<label htmlFor="terms" className="text-sm text-gray-300">
|
||||||
|
{t("register.agreeTerms")}{" "}
|
||||||
|
<Link href="/terms" className={`${type === "owner" ? `text-amber-500 focus:ring-amber-500` : ` text-blue-500 focus:ring-blue-500`}`}>
|
||||||
|
{t("register.termsOfUse")}
|
||||||
|
</Link>{" "}
|
||||||
|
{t("register.and")}{" "}
|
||||||
|
<Link href="/privacy" className={`${type === "owner" ? `text-amber-500 focus:ring-amber-500` : ` text-blue-500 focus:ring-blue-500`}`}>
|
||||||
|
{t("register.privacyPolicy")}
|
||||||
|
</Link>
|
||||||
|
</label>
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
85
app/components/OwnerComponents/UploadImage.js
Normal file
85
app/components/OwnerComponents/UploadImage.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { fadeInUp } from "../Animation/OwnerPageAnimation";
|
||||||
|
import { handleImageUpload } from "@/app/HelperFunction/UploadImage";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { Camera, X } from "lucide-react";
|
||||||
|
import { useRef } from "react";
|
||||||
|
|
||||||
|
export default function UploadImage({ idImagePreviews, type, errors, setIdImagePreviews, setIdImages }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const fileInputRef = useRef(null);
|
||||||
|
const currentPreview = idImagePreviews?.[type];
|
||||||
|
const handleRemoveImage = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIdImages((prev) => ({ ...prev, [type]: null }));
|
||||||
|
setIdImagePreviews((prev) => ({ ...prev, [type]: "" }));
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLabel = () => {
|
||||||
|
switch (type) {
|
||||||
|
case "front":
|
||||||
|
return t("register.owner.frontIdLabel");
|
||||||
|
case "back":
|
||||||
|
return t("register.owner.backIdLabel");
|
||||||
|
case "license":
|
||||||
|
return t("register.owner.licenseImageLabel");
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getClickText = () => {
|
||||||
|
return type === "license" ? t("register.owner.clickUploadLicense") : t("register.owner.clickUploadImage");
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAltText = () => {
|
||||||
|
switch (type) {
|
||||||
|
case "front":
|
||||||
|
return t("register.uploadFrontAlt");
|
||||||
|
case "back":
|
||||||
|
return t("register.uploadBackAlt");
|
||||||
|
case "license":
|
||||||
|
return t("register.uploadLicenseAlt");
|
||||||
|
default:
|
||||||
|
return "Preview Image";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div variants={fadeInUp} key={`${type}-upload-step`}>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||||
|
{getLabel()} {type === "license" ? <span className="text-gray-400">({t("register.optional")})</span> : <span className="text-red-500">*</span>}
|
||||||
|
</label>
|
||||||
|
{type === "license" && <div className="mb-2 text-xs text-gray-400">{t("register.owner.licenseHint")}</div>}
|
||||||
|
<div
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
className={`relative border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all ${
|
||||||
|
currentPreview ? "border-green-500 bg-green-500/10" : errors ? "border-red-500 bg-red-500/10" : "border-gray-700 hover:border-amber-500 hover:bg-white/5"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<input ref={fileInputRef} type="file" accept="image/*" onChange={(e) => handleImageUpload(type, e.target.files?.[0], t, setIdImagePreviews, setIdImages)} className="hidden" />
|
||||||
|
{currentPreview ? (
|
||||||
|
<div className="relative">
|
||||||
|
<Image src={currentPreview} alt={getAltText()} width={200} height={120} className="mx-auto rounded-lg object-cover" />
|
||||||
|
<button type="button" onClick={handleRemoveImage} className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 rounded-full flex items-center justify-center hover:bg-red-600 transition-colors">
|
||||||
|
<X className="w-4 h-4 text-white" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Camera className="w-12 h-12 text-gray-500 mx-auto mb-3" />
|
||||||
|
<p className="text-gray-400">{getClickText()}</p>
|
||||||
|
<p className="text-xs text-gray-500 mt-2">JPEG, PNG, JPG • {t("register.upTo5MB")}</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{errors && <p className="text-red-500 text-sm mt-1">{errors}</p>}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,14 +1,14 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from "react";
|
||||||
import L from 'leaflet';
|
import L from "leaflet";
|
||||||
import 'leaflet/dist/leaflet.css';
|
import "leaflet/dist/leaflet.css";
|
||||||
|
|
||||||
delete L.Icon.Default.prototype._getIconUrl;
|
delete L.Icon.Default.prototype._getIconUrl;
|
||||||
L.Icon.Default.mergeOptions({
|
L.Icon.Default.mergeOptions({
|
||||||
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
|
iconRetinaUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png",
|
||||||
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
|
iconUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png",
|
||||||
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
|
shadowUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png",
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function PropertyMapWithMarkers({ properties = [], onPropertyClick }) {
|
export default function PropertyMapWithMarkers({ properties = [], onPropertyClick }) {
|
||||||
@ -22,7 +22,7 @@ export default function PropertyMapWithMarkers({ properties = [], onPropertyClic
|
|||||||
|
|
||||||
const map = L.map(mapRef.current).setView([33.5138, 38.9968], 7);
|
const map = L.map(mapRef.current).setView([33.5138, 38.9968], 7);
|
||||||
|
|
||||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||||
maxZoom: 19,
|
maxZoom: 19,
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
@ -41,32 +41,32 @@ export default function PropertyMapWithMarkers({ properties = [], onPropertyClic
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mapInstanceRef.current || !mapLoaded) return;
|
if (!mapInstanceRef.current || !mapLoaded) return;
|
||||||
|
|
||||||
markersRef.current.forEach(marker => marker.remove());
|
markersRef.current.forEach((marker) => marker.remove());
|
||||||
markersRef.current = [];
|
markersRef.current = [];
|
||||||
|
|
||||||
properties.forEach(property => {
|
properties.forEach((property) => {
|
||||||
if (property.lat && property.lng) {
|
if (property.lat && property.lng) {
|
||||||
const marker = L.marker([property.lat, property.lng]).addTo(mapInstanceRef.current);
|
const marker = L.marker([property.lat, property.lng]).addTo(mapInstanceRef.current);
|
||||||
|
|
||||||
const popupContent = `
|
const popupContent = `
|
||||||
<div dir="rtl" style="text-align: right; padding: 12px; max-width: 250px;">
|
<div dir="rtl" style="text-align: right; padding: 12px; max-width: 250px;">
|
||||||
<h3 style="font-weight: bold; font-size: 16px; margin-bottom: 8px; color: #111;">${property.title || 'عقار'}</h3>
|
<h3 style="font-weight: bold; font-size: 16px; margin-bottom: 8px; color: #111;">${property.title || "عقار"}</h3>
|
||||||
<p style="font-size: 14px; color: #666; margin-bottom: 8px;">${property.address || property.location?.address || ''}</p>
|
<p style="font-size: 14px; color: #666; margin-bottom: 8px;">${property.address || property.location?.address || ""}</p>
|
||||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
||||||
<span style="font-weight: bold; font-size: 18px; color: #d97706;">${formatPrice(property)}</span>
|
<span style="font-weight: bold; font-size: 18px; color: #d97706;">${formatPrice(property)}</span>
|
||||||
</div>
|
</div>
|
||||||
${property.images && property.images.length > 0 ? `<img src="${property.images[0]}" alt="${property.title}" style="width: 100%; height: 96px; object-fit: cover; border-radius: 8px; margin-bottom: 8px;" onerror="this.src='/property-placeholder.jpg'" />` : ''}
|
${property.images && property.images.length > 0 ? `<img src="${property.images[0] ?? "https://tse4.mm.bing.net/th/id/OIP.-VT-J9Brp8KDSlb0Z_990wAAAA?r=0&rs=1&pid=ImgDetMain&o=7&rm=3"}" alt="${property.title}" style="width: 100%; height: 96px; object-fit: cover; border-radius: 8px; margin-bottom: 8px;" onerror="this.src='/property-placeholder.jpg'" />` : ""}
|
||||||
<div style="font-size: 12px; color: #888;">
|
<div style="font-size: 12px; color: #888;">
|
||||||
${property.type ? `<p>النوع: ${property.type}</p>` : ''}
|
${property.type ? `<p>النوع: ${property.type}</p>` : ""}
|
||||||
${property.bedrooms > 0 ? `<p>غرف نوم: ${property.bedrooms}</p>` : ''}
|
${property.bedrooms > 0 ? `<p>غرف نوم: ${property.bedrooms}</p>` : ""}
|
||||||
${property.bathrooms > 0 ? `<p>حمامات: ${property.bathrooms}</p>` : ''}
|
${property.bathrooms > 0 ? `<p>حمامات: ${property.bathrooms}</p>` : ""}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
marker.bindPopup(popupContent);
|
marker.bindPopup(popupContent);
|
||||||
|
|
||||||
marker.on('click', () => {
|
marker.on("click", () => {
|
||||||
if (onPropertyClick) {
|
if (onPropertyClick) {
|
||||||
onPropertyClick(property);
|
onPropertyClick(property);
|
||||||
}
|
}
|
||||||
@ -83,9 +83,9 @@ export default function PropertyMapWithMarkers({ properties = [], onPropertyClic
|
|||||||
}, [properties, mapLoaded]);
|
}, [properties, mapLoaded]);
|
||||||
|
|
||||||
const formatPrice = (property) => {
|
const formatPrice = (property) => {
|
||||||
if (property.priceUnit === 'monthly') {
|
if (property.priceUnit === "monthly") {
|
||||||
return `${property.price?.toLocaleString() || 0} ل.س/شهر`;
|
return `${property.price?.toLocaleString() || 0} ل.س/شهر`;
|
||||||
} else if (property.priceUnit === 'daily') {
|
} else if (property.priceUnit === "daily") {
|
||||||
return `${property.price?.toLocaleString() || 0} ل.س/يوم`;
|
return `${property.price?.toLocaleString() || 0} ل.س/يوم`;
|
||||||
} else {
|
} else {
|
||||||
return `${property.price?.toLocaleString() || 0} ل.س`;
|
return `${property.price?.toLocaleString() || 0} ل.س`;
|
||||||
|
|||||||
28
app/components/home/ArrowForScroll.js
Normal file
28
app/components/home/ArrowForScroll.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { motion } from "framer-motion";
|
||||||
|
export default function ArrowForScroll() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
className="absolute bottom-8 left-1/2 transform -translate-x-1/2 cursor-pointer"
|
||||||
|
animate={{
|
||||||
|
y: [0, 10, 0],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 1.5,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "easeInOut",
|
||||||
|
}}
|
||||||
|
onClick={() =>
|
||||||
|
window.scrollTo({
|
||||||
|
top: window.innerHeight,
|
||||||
|
behavior: "smooth",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<svg className="w-6 h-6 text-amber-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
||||||
|
</svg>
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
app/components/home/FeatureSection.js
Normal file
21
app/components/home/FeatureSection.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Lock, ShieldCheck, Zap } from "lucide-react";
|
||||||
|
import FeaturesOurCompany from "./FeaturesOurCompany";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
export default function FeatureSection() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: false }} transition={{ duration: 0.6 }} className="text-center mt-5">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-5 p-5">
|
||||||
|
<FeaturesOurCompany icon={<ShieldCheck className="w-6 h-6 text-amber-600" />} title={t("feature1Title")} discripction={t("feature1Description")} />
|
||||||
|
<FeaturesOurCompany icon={<Lock className="w-6 h-6 text-blue-600" />} title={t("feature2Title")} discripction={t("feature2Description")} />
|
||||||
|
<FeaturesOurCompany icon={<Zap className="w-6 h-6 text-green-600" />} title={t("feature3Title")} discripction={t("feature3Description")} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
app/components/home/FeaturesOurCompany.js
Normal file
21
app/components/home/FeaturesOurCompany.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { motion } from "framer-motion";
|
||||||
|
export default function FeaturesOurCompany({ icon, title, discripction }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<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: false }}
|
||||||
|
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">{icon}</div>
|
||||||
|
<h3 className="text-lg font-bold text-gray-900">{title}</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-600 text-sm leading-relaxed">{discripction}</p>
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
32
app/components/home/FilterSelect.js
Normal file
32
app/components/home/FilterSelect.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
export default function FilterSelect({ onChange, icon, value, option, label }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-amber-50 mb-2">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{icon??""}
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
|
||||||
|
backgroundRepeat: "no-repeat",
|
||||||
|
backgroundPosition: "left 1rem center",
|
||||||
|
backgroundSize: "1rem",
|
||||||
|
paddingLeft: "2.5rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{option.map((opt) => (
|
||||||
|
<option key={opt.id} value={opt.id}>
|
||||||
|
{opt.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,319 +1,117 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
import { useState } from "react";
|
||||||
import { useState } from 'react';
|
import { motion } from "framer-motion";
|
||||||
import Link from 'next/link';
|
import { useTranslation } from "react-i18next";
|
||||||
import { motion } from 'framer-motion';
|
import { Search, MapPin, Home, DollarSign } from "lucide-react";
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useFilterOptions } from "@/app/hooks/UseFilterOfHeroSearch";
|
||||||
import { Search, MapPin, Home, DollarSign, ShieldCheck } from 'lucide-react';
|
import ShowLoginDialog from "./showLoginDialog";
|
||||||
|
import FilterSelect from "./FilterSelect";
|
||||||
export default function HeroSearch({ onSearch, isAuthenticated }) {
|
export default function HeroSearch({ onSearch }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [activeTab, setActiveTab] = useState('buy');
|
const { cities, identityTypes, ownerSources, priceRanges, propertyTypes, rentPeriods } = useFilterOptions();
|
||||||
|
const [activeTab, setActiveTab] = useState("buy");
|
||||||
const [filters, setFilters] = useState({
|
const [filters, setFilters] = useState({
|
||||||
city: 'all',
|
city: "all",
|
||||||
propertyType: 'all',
|
propertyType: "all",
|
||||||
priceRange: 'all',
|
priceRange: "all",
|
||||||
identityType: 'syrian',
|
identityType: "syrian",
|
||||||
ownerSource: 'all',
|
ownerSource: "all",
|
||||||
rentPeriod: 'all',
|
rentPeriod: "all",
|
||||||
availableToday: false
|
availableToday: false,
|
||||||
});
|
});
|
||||||
const [showLoginDialog, setShowLoginDialog] = useState(false);
|
const [showLoginDialog, setShowLoginDialog] = useState(false);
|
||||||
|
|
||||||
const cities = [
|
|
||||||
{ id: 'all', label: 'جميع المدن' },
|
|
||||||
{ id: 'دمشق', label: 'دمشق' },
|
|
||||||
{ id: 'حلب', label: 'حلب' },
|
|
||||||
{ id: 'حمص', label: 'حمص' },
|
|
||||||
{ id: 'اللاذقية', label: 'اللاذقية' },
|
|
||||||
{ id: 'درعا', label: 'درعا' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const propertyTypes = [
|
|
||||||
{ id: 'all', label: 'الكل' },
|
|
||||||
{ id: 'apartment', label: 'شقق سكنية' },
|
|
||||||
{ id: 'studio', label: 'استوديو' },
|
|
||||||
{ id: 'commercial', label: 'عقار تجاري' },
|
|
||||||
{ id: 'villa', label: 'فيلا / مزرعة' }
|
|
||||||
];
|
|
||||||
|
|
||||||
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$' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const identityTypes = [
|
|
||||||
{ id: 'syrian', label: 'هوية سورية' },
|
|
||||||
{ id: 'passport', label: 'جواز سفر' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const ownerSources = [
|
|
||||||
{ id: 'all', label: 'الكل' },
|
|
||||||
{ id: 'owner', label: 'من المالك' },
|
|
||||||
{ id: 'agency', label: 'من مكتب عقاري' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const rentPeriods = [
|
|
||||||
{ id: 'all', label: 'الكل' },
|
|
||||||
{ id: 'daily', label: 'إيجار يومي' },
|
|
||||||
{ id: 'monthly', label: 'إيجار شهري' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleTabClick = (tab) => {
|
const handleTabClick = (tab) => {
|
||||||
setActiveTab(tab);
|
setActiveTab(tab);
|
||||||
if ((tab === 'rent' || tab === 'sell') && !isAuthenticated) {
|
|
||||||
setShowLoginDialog(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handleSearch();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
if ((activeTab === 'rent' || activeTab === 'sell') && !isAuthenticated) {
|
|
||||||
setShowLoginDialog(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
onSearch({
|
onSearch({
|
||||||
...filters,
|
...filters,
|
||||||
mode: activeTab,
|
mode: activeTab,
|
||||||
city: filters.city || 'all',
|
city: filters.city || "all",
|
||||||
propertyType: filters.propertyType || 'all',
|
propertyType: filters.propertyType || "all",
|
||||||
priceRange: filters.priceRange || 'all',
|
priceRange: filters.priceRange || "all",
|
||||||
ownerSource: filters.ownerSource || 'all',
|
ownerSource: filters.ownerSource || "all",
|
||||||
rentPeriod: filters.rentPeriod || 'all'
|
rentPeriod: filters.rentPeriod || "all",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="bg-white/10 backdrop-blur-lg rounded-2xl p-6 sm:p-8 border border-white/20 shadow-2xl"
|
className="bg-white/10 backdrop-blur-lg rounded-2xl p-6 sm:p-8 border border-white/20 shadow-2xl"
|
||||||
initial={{ opacity: 0, y: 30 }}
|
initial={{ opacity: 0, y: 30 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.8, delay: 0.5 }}
|
transition={{ duration: 0.8, delay: 0.5 }}
|
||||||
>
|
>
|
||||||
<div className="flex flex-wrap gap-2 mb-8">
|
<div className="flex flex-wrap gap-2 mb-8">
|
||||||
{['rent', 'buy', 'sell'].map((tab) => (
|
{["rent", "buy", "sell"].map((tab) => {
|
||||||
<motion.button
|
const isActive = activeTab === tab;
|
||||||
key={tab}
|
|
||||||
onClick={() => handleTabClick(tab)}
|
|
||||||
className={`px-4 py-2 rounded-lg font-medium text-sm transition-all ${
|
|
||||||
activeTab === tab
|
|
||||||
? 'bg-amber-500 text-white'
|
|
||||||
: 'bg-white/20 text-white hover:bg-white/30'
|
|
||||||
}`}
|
|
||||||
whileHover={{ scale: 1.05 }}
|
|
||||||
whileTap={{ scale: 0.95 }}
|
|
||||||
>
|
|
||||||
{t(`${tab}Tab`)}
|
|
||||||
</motion.button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-white mb-2">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<MapPin className="w-4 h-4" />
|
|
||||||
{t("cityStreetLabel")}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={filters.city}
|
|
||||||
onChange={(e) => setFilters({...filters, city: e.target.value})}
|
|
||||||
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
|
|
||||||
style={{
|
|
||||||
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
|
|
||||||
backgroundRepeat: 'no-repeat',
|
|
||||||
backgroundPosition: 'left 1rem center',
|
|
||||||
backgroundSize: '1rem',
|
|
||||||
paddingLeft: '2.5rem'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{cities.map(city => (
|
|
||||||
<option key={city.id} value={city.id}>{city.label}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-white mb-2">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Home className="w-4 h-4" />
|
|
||||||
{t("rentTypeLabel")}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={filters.propertyType}
|
|
||||||
onChange={(e) => setFilters({...filters, propertyType: e.target.value})}
|
|
||||||
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
|
|
||||||
style={{
|
|
||||||
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
|
|
||||||
backgroundRepeat: 'no-repeat',
|
|
||||||
backgroundPosition: 'left 1rem center',
|
|
||||||
backgroundSize: '1rem',
|
|
||||||
paddingLeft: '2.5rem'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{propertyTypes.map(type => (
|
|
||||||
<option key={type.id} value={type.id}>{type.label}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-white mb-2">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<DollarSign className="w-4 h-4" />
|
|
||||||
{t("priceLabel")}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={filters.priceRange}
|
|
||||||
onChange={(e) => setFilters({...filters, priceRange: e.target.value})}
|
|
||||||
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
|
|
||||||
style={{
|
|
||||||
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
|
|
||||||
backgroundRepeat: 'no-repeat',
|
|
||||||
backgroundPosition: 'left 1rem center',
|
|
||||||
backgroundSize: '1rem',
|
|
||||||
paddingLeft: '2.5rem'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{priceRanges.map(range => (
|
|
||||||
<option key={range.id} value={range.id}>{range.label}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-white mb-2">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
نوع الوثيقة
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={filters.identityType}
|
|
||||||
onChange={(e) => setFilters({...filters, identityType: e.target.value})}
|
|
||||||
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
|
|
||||||
style={{
|
|
||||||
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
|
|
||||||
backgroundRepeat: 'no-repeat',
|
|
||||||
backgroundPosition: 'left 1rem center',
|
|
||||||
backgroundSize: '1rem',
|
|
||||||
paddingLeft: '2.5rem'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{identityTypes.map(type => (
|
|
||||||
<option key={type.id} value={type.id}>{type.label}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</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>
|
|
||||||
<select
|
|
||||||
value={filters.ownerSource}
|
|
||||||
onChange={(e) => setFilters({ ...filters, ownerSource: e.target.value })}
|
|
||||||
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
|
|
||||||
style={{
|
|
||||||
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
|
|
||||||
backgroundRepeat: 'no-repeat',
|
|
||||||
backgroundPosition: 'left 1rem center',
|
|
||||||
backgroundSize: '1rem',
|
|
||||||
paddingLeft: '2.5rem'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{ownerSources.map((source) => (
|
|
||||||
<option key={source.id} value={source.id}>
|
|
||||||
{source.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
return (
|
||||||
<label className="block text-sm font-medium text-white mb-2">نوع الإيجار</label>
|
|
||||||
<select
|
|
||||||
value={filters.rentPeriod}
|
|
||||||
onChange={(e) => setFilters({ ...filters, rentPeriod: e.target.value })}
|
|
||||||
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
|
|
||||||
style={{
|
|
||||||
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
|
|
||||||
backgroundRepeat: 'no-repeat',
|
|
||||||
backgroundPosition: 'left 1rem center',
|
|
||||||
backgroundSize: '1rem',
|
|
||||||
paddingLeft: '2.5rem'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{rentPeriods.map((period) => (
|
|
||||||
<option key={period.id} value={period.id}>
|
|
||||||
{period.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="md:col-span-2 flex flex-col justify-between p-4 rounded-2xl border border-dashed border-white/30 bg-white/5">
|
|
||||||
<label className="mt-4 flex items-center gap-3 text-white text-sm">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={filters.availableToday}
|
|
||||||
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>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-6">
|
|
||||||
<motion.button
|
|
||||||
onClick={handleSearch}
|
|
||||||
className="w-full bg-amber-500 hover:bg-amber-600 text-white font-bold py-4 px-6 rounded-xl transition-all duration-300 flex items-center justify-center text-base gap-3 shadow-lg hover:shadow-xl"
|
|
||||||
whileHover={{ scale: 1.02 }}
|
|
||||||
whileTap={{ scale: 0.98 }}
|
|
||||||
>
|
|
||||||
<Search className="w-5 h-5" />
|
|
||||||
{t("searchButton")}
|
|
||||||
</motion.button>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{showLoginDialog && !isAuthenticated && (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 px-4 py-8">
|
|
||||||
<div className="w-full max-w-md rounded-3xl bg-white p-6 shadow-2xl border border-gray-200">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="rounded-2xl bg-gray-50 p-4">
|
|
||||||
<p className="text-sm text-gray-700">اضغط على تسجيل الدخول لاستكمال البحث أو إدارة عقاراتك.</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
key={tab}
|
||||||
onClick={() => setShowLoginDialog(false)}
|
onClick={() => handleTabClick(tab)}
|
||||||
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"
|
className={`relative px-5 py-2.5 rounded-lg font-medium text-sm transition-colors duration-200 outline-none select-none ${isActive ? "text-white" : "text-amber-50 hover:text-white"}`}
|
||||||
>
|
>
|
||||||
إغلاق
|
{isActive && <motion.div layoutId="activeTabBackground" className="absolute inset-0 bg-amber-500 rounded-lg shadow-md" transition={{ type: "spring", stiffness: 500, damping: 35 }} />}
|
||||||
|
<span className="relative z-10">{t(`${tab}Tab`)}</span>
|
||||||
</button>
|
</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"
|
</div>
|
||||||
>
|
{/* city */}
|
||||||
تسجيل الدخول
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
</Link>
|
<FilterSelect icon={<MapPin className="w-4 h-4" />} value={filters.city} onChange={(e) => setFilters({ ...filters, city: e.target.value })} option={cities} label={t(t("cityStreetLabel"))} />
|
||||||
</div>
|
{activeTab === "rent" && (
|
||||||
|
<FilterSelect
|
||||||
|
icon={<Home className="w-4 h-4" />}
|
||||||
|
value={filters.propertyType}
|
||||||
|
onChange={(e) => setFilters({ ...filters, city: e.target.value })}
|
||||||
|
option={propertyTypes}
|
||||||
|
label={t("rentTypeLabel")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/* price */}
|
||||||
|
<FilterSelect
|
||||||
|
icon={<DollarSign className="w-4 h-4" />}
|
||||||
|
value={filters.priceRange}
|
||||||
|
onChange={(e) => setFilters({ ...filters, priceRange: e.target.value })}
|
||||||
|
option={priceRanges}
|
||||||
|
label={t("priceLabel")}
|
||||||
|
/>
|
||||||
|
{/* type */}
|
||||||
|
<FilterSelect value={filters.identityType} onChange={(e) => setFilters({ ...filters, identityType: e.target.value })} option={identityTypes} label={t("identityTypeLabel")} />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mt-4">
|
||||||
|
{/* owener */}
|
||||||
|
<FilterSelect value={filters.ownerSource} onChange={(e) => setFilters({ ...filters, ownerSource: e.target.value })} option={ownerSources} label={t("ownerSourceLabel")} />
|
||||||
|
{activeTab === "rent" && <FilterSelect value={filters.rentPeriod} onChange={(e) => setFilters({ ...filters, rentPeriod: e.target.value })} option={rentPeriods} label={t("rentTypeLabel")} />}
|
||||||
|
<div className={`${activeTab === "rent" ? "md:col-span-2" : "md:col-span-3"} flex flex-col justify-between p-4 rounded-2xl border border-dashed border-white/30 bg-white/5`}>
|
||||||
|
{/* checkbox */}
|
||||||
|
<label className="mt-4 flex items-center gap-3 text-amber-50 text-sm">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={filters.availableToday}
|
||||||
|
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">{t("availableToday")}</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="mt-6">
|
||||||
)}
|
<motion.button
|
||||||
|
onClick={handleSearch}
|
||||||
|
className="w-full bg-amber-500 hover:bg-amber-600 text-white font-bold py-4 px-6 rounded-xl transition-all duration-300 flex items-center justify-center text-base gap-3 shadow-lg hover:shadow-xl"
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<Search className="w-5 h-5" />
|
||||||
|
{t("searchButton")}
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
{showLoginDialog && !isAuthenticated && <ShowLoginDialog setShowLoginDialog={setShowLoginDialog} />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
54
app/components/home/HeroSection.js
Normal file
54
app/components/home/HeroSection.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
export default function HeroSection() {
|
||||||
|
const {t} =useTranslation()
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
className="text-center mb-12"
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
variants={{
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: { staggerChildren: 0.2 },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<motion.h1
|
||||||
|
className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-amber-50 mb-6 leading-tight tracking-tight"
|
||||||
|
variants={{
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: { opacity: 1, y: 0 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("heroTitleLine1")}
|
||||||
|
<br />
|
||||||
|
<motion.span
|
||||||
|
className="text-amber-300"
|
||||||
|
animate={{
|
||||||
|
y: [0, -10, 0],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 2,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "easeInOut",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("heroTitleLine2")}
|
||||||
|
</motion.span>
|
||||||
|
</motion.h1>
|
||||||
|
<motion.p
|
||||||
|
className="text-base sm:text-lg text-amber-50 max-w-2xl mx-auto leading-relaxed"
|
||||||
|
variants={{
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: { opacity: 1, y: 0 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("heroSubtitle")}
|
||||||
|
</motion.p>
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
71
app/components/home/HomeClient.jsx
Normal file
71
app/components/home/HomeClient.jsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
"use client";
|
||||||
|
import { useState, useRef } from "react";
|
||||||
|
import { AnimatePresence } from "framer-motion";
|
||||||
|
import HeroSearch from "./HeroSearch";
|
||||||
|
import ShowIfOwner from "./ShowIfOwner";
|
||||||
|
import MapSection from "./MapSection";
|
||||||
|
import useApplyFilters from "../../hooks/useApplyFilters";
|
||||||
|
import HeroSection from "./HeroSection";
|
||||||
|
import FeatureSection from "./FeatureSection";
|
||||||
|
import ArrowForScroll from "./ArrowForScroll";
|
||||||
|
import useAuth from "@/app/hooks/useAuth";
|
||||||
|
|
||||||
|
export default function HomeClient() {
|
||||||
|
const mapSectionRef = useRef(null);
|
||||||
|
const [showMap, setShowMap] = useState(false);
|
||||||
|
const [isScrolling, setIsScrolling] = useState(false);
|
||||||
|
const { filteredProperties, applyFilters, resetFilters } = useApplyFilters();
|
||||||
|
const { name, isOwner, isAuthenticated, role } = useAuth();
|
||||||
|
const handleSearch = (filters) => {
|
||||||
|
applyFilters(filters);
|
||||||
|
if (!showMap) {
|
||||||
|
setShowMap(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollToMap();
|
||||||
|
}, 300);
|
||||||
|
} else {
|
||||||
|
scrollToMap();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const scrollToMap = () => {
|
||||||
|
if (mapSectionRef.current) {
|
||||||
|
setIsScrolling(true);
|
||||||
|
mapSectionRef.current.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "center",
|
||||||
|
});
|
||||||
|
setTimeout(() => setIsScrolling(false), 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const resetSearch = () => {
|
||||||
|
setShowMap(false);
|
||||||
|
resetFilters();
|
||||||
|
window.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen">
|
||||||
|
<section className="relative min-h-screen flex items-center justify-center overflow-hidden">
|
||||||
|
<div className="relative z-10 container mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
<HeroSection />
|
||||||
|
{!isOwner && <HeroSearch onSearch={handleSearch} />}
|
||||||
|
{isOwner && <ShowIfOwner name={name} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!showMap && !isOwner && <ArrowForScroll />}
|
||||||
|
</section>
|
||||||
|
{!isOwner && (
|
||||||
|
<>
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{showMap && <MapSection mapSectionRef={mapSectionRef} isScrolling={isScrolling} resetSearch={resetSearch} filteredProperties={filteredProperties} />}
|
||||||
|
</AnimatePresence>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<FeatureSection />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
80
app/components/home/MapSection.js
Normal file
80
app/components/home/MapSection.js
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import PropertyMapWithMarkers from "../PropertyMapWithMarkers";
|
||||||
|
export default function MapSection({ mapSectionRef, isScrolling, resetSearch, filteredProperties }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<motion.section
|
||||||
|
ref={mapSectionRef}
|
||||||
|
initial={{ opacity: 0, y: 50 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -50 }}
|
||||||
|
transition={{
|
||||||
|
type: "spring",
|
||||||
|
damping: 20,
|
||||||
|
stiffness: 100,
|
||||||
|
duration: 0.6,
|
||||||
|
}}
|
||||||
|
className="py-12 bg-gray-50 relative"
|
||||||
|
>
|
||||||
|
{isScrolling && <motion.div className="absolute top-0 left-0 right-0 h-1 bg-amber-500 z-10" initial={{ scaleX: 0 }} animate={{ scaleX: 1 }} transition={{ duration: 1, ease: "easeInOut" }} />}
|
||||||
|
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.2 }} className="text-center mb-8">
|
||||||
|
<div className="flex items-center justify-center gap-4 mb-2">
|
||||||
|
<h2 className="text-3xl font-bold text-gray-900">{filteredProperties.length > 0 ? t("searchResults") : t("no-properties")}</h2>
|
||||||
|
<motion.button
|
||||||
|
onClick={resetSearch}
|
||||||
|
className="px-4 py-2 bg-white border border-gray-300 rounded-full text-sm font-medium text-gray-700 hover:bg-gray-50 shadow-sm flex items-center gap-2"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<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">{t("foundPropertiesCount", { count: filteredProperties.length })}</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-600">{t("noPropertiesMatchFilters")}</p>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="bg-white rounded-2xl shadow-xl overflow-hidden border border-gray-200"
|
||||||
|
initial={{ scale: 0.95, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
transition={{ delay: 0.3, type: "spring" }}
|
||||||
|
>
|
||||||
|
{filteredProperties.length > 0 ? (
|
||||||
|
<PropertyMapWithMarkers
|
||||||
|
properties={filteredProperties.map((p) => ({
|
||||||
|
...p,
|
||||||
|
lat: p.location.lat,
|
||||||
|
lng: p.location.lng,
|
||||||
|
address: p.location.address,
|
||||||
|
}))}
|
||||||
|
onPropertyClick={() => {}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="h-[400px] flex flex-col items-center justify-center bg-gray-50">
|
||||||
|
<div className="w-24 h-24 bg-gray-200 rounded-full flex items-center justify-center mb-4">
|
||||||
|
<svg className="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<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">{t("no-properties")}</h3>
|
||||||
|
<p className="text-gray-500">{t("try-changing-search-criteria")}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</motion.section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
app/components/home/ShowIfOwner.js
Normal file
26
app/components/home/ShowIfOwner.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Building } from "lucide-react";
|
||||||
|
export default function ShowIfOwner({ name }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.5 }}
|
||||||
|
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">
|
||||||
|
{t("ownerGreeting")} {name ?? " notFound "}!
|
||||||
|
</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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
app/components/home/showLoginDialog.js
Normal file
38
app/components/home/showLoginDialog.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { ShieldCheck } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
export default function ShowLoginDialog({setShowLoginDialog}) {
|
||||||
|
const {t}=useTranslation()
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 px-4 py-8">
|
||||||
|
<div className="w-full max-w-md rounded-3xl bg-white p-6 shadow-2xl border border-gray-200">
|
||||||
|
<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">{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">{t("loginPrompt")}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
85
app/components/property/AddPropertyMap.js
Normal file
85
app/components/property/AddPropertyMap.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
export default function AddPropertyMap({ mapCenter, mapZoom, selectedLocation, onMapClick, onMarkerDragEnd }) {
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
const mapRef = useRef(null);
|
||||||
|
const markerRef = useRef(null);
|
||||||
|
const LRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function init() {
|
||||||
|
const L = await import('leaflet');
|
||||||
|
LRef.current = L;
|
||||||
|
|
||||||
|
delete L.Icon.Default.prototype._getIconUrl;
|
||||||
|
L.Icon.Default.mergeOptions({
|
||||||
|
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
|
||||||
|
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
|
||||||
|
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!containerRef.current || mapRef.current) return;
|
||||||
|
|
||||||
|
const map = L.map(containerRef.current, {
|
||||||
|
center: mapCenter,
|
||||||
|
zoom: mapZoom,
|
||||||
|
doubleClickZoom: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© OpenStreetMap contributors',
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
map.on('click', (e) => {
|
||||||
|
const pos = [e.latlng.lat, e.latlng.lng];
|
||||||
|
placeMarker(pos);
|
||||||
|
map.setView(pos, mapZoom);
|
||||||
|
onMapClick(pos);
|
||||||
|
});
|
||||||
|
|
||||||
|
mapRef.current = map;
|
||||||
|
setTimeout(() => map.invalidateSize(), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (mapRef.current) {
|
||||||
|
mapRef.current.remove();
|
||||||
|
mapRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function placeMarker(pos) {
|
||||||
|
const L = LRef.current;
|
||||||
|
if (!L || !mapRef.current) return;
|
||||||
|
|
||||||
|
if (markerRef.current) {
|
||||||
|
markerRef.current.setLatLng(pos);
|
||||||
|
} else {
|
||||||
|
markerRef.current = L.marker(pos, { draggable: true }).addTo(mapRef.current);
|
||||||
|
markerRef.current.on('dragend', (ev) => {
|
||||||
|
const p = ev.target.getLatLng();
|
||||||
|
onMarkerDragEnd(p.lat, p.lng);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const map = mapRef.current;
|
||||||
|
if (!map) return;
|
||||||
|
map.setView(mapCenter, mapZoom);
|
||||||
|
}, [mapCenter, mapZoom]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedLocation || !mapRef.current) return;
|
||||||
|
const pos = [selectedLocation.lat, selectedLocation.lng];
|
||||||
|
mapRef.current.setView(pos, mapZoom);
|
||||||
|
placeMarker(pos);
|
||||||
|
}, [selectedLocation]);
|
||||||
|
|
||||||
|
return <div ref={containerRef} className="w-full h-full z-0" />;
|
||||||
|
}
|
||||||
246
app/components/property/DateSelectionCalendar.js
Normal file
246
app/components/property/DateSelectionCalendar.js
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useCallback } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { ChevronRight, ChevronLeft } from "lucide-react";
|
||||||
|
|
||||||
|
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",
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function DateSelectionCalendar({
|
||||||
|
mode,
|
||||||
|
availableDates,
|
||||||
|
selectedStart,
|
||||||
|
selectedEnd,
|
||||||
|
onSelectStart,
|
||||||
|
onSelectEnd,
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [calendarMonth, setCalendarMonth] = useState(() => new Date().getMonth());
|
||||||
|
const [calendarYear, setCalendarYear] = useState(() => new Date().getFullYear());
|
||||||
|
const [rangeError, setRangeError] = useState(null);
|
||||||
|
|
||||||
|
const isPastDate = useCallback((dateStr) => {
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
return new Date(dateStr) < today;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const isDateAvailable = useCallback(
|
||||||
|
(dateStr) => (availableDates ? availableDates.has(dateStr) : true),
|
||||||
|
[availableDates]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isMonthFullyAvailable = useCallback(
|
||||||
|
(year, monthIdx) => {
|
||||||
|
const daysInMonth = new Date(year, monthIdx + 1, 0).getDate();
|
||||||
|
for (let day = 1; day <= daysInMonth; day++) {
|
||||||
|
const dateStr = `${year}-${String(monthIdx + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
||||||
|
if (isPastDate(dateStr) || !isDateAvailable(dateStr)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[isPastDate, isDateAvailable]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isPastMonth = useCallback((year, monthIdx) => {
|
||||||
|
const now = new Date();
|
||||||
|
return (
|
||||||
|
year < now.getFullYear() ||
|
||||||
|
(year === now.getFullYear() && monthIdx < now.getMonth())
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDayClick = (dateStr) => {
|
||||||
|
setRangeError(null);
|
||||||
|
if (!selectedStart || selectedEnd) {
|
||||||
|
onSelectStart(dateStr);
|
||||||
|
onSelectEnd(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (new Date(dateStr) <= new Date(selectedStart)) {
|
||||||
|
onSelectStart(dateStr);
|
||||||
|
onSelectEnd(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const start = new Date(selectedStart);
|
||||||
|
const end = new Date(dateStr);
|
||||||
|
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
|
||||||
|
const ds = d.toISOString().split("T")[0];
|
||||||
|
if (!isDateAvailable(ds)) {
|
||||||
|
setRangeError(t("unavailableInRange", { defaultValue: "توجد أيام غير متاحة ضمن الفترة المحددة" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onSelectEnd(dateStr);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMonthClick = (year, monthIdx) => {
|
||||||
|
setRangeError(null);
|
||||||
|
const monthStr = `${year}-${String(monthIdx + 1).padStart(2, "0")}`;
|
||||||
|
const firstDay = `${monthStr}-01`;
|
||||||
|
if (!selectedStart || selectedEnd || monthStr <= (selectedStart || "").substring(0, 7)) {
|
||||||
|
onSelectStart(firstDay);
|
||||||
|
onSelectEnd(null);
|
||||||
|
} else {
|
||||||
|
const lastDay = new Date(year, monthIdx + 1, 0).getDate();
|
||||||
|
onSelectEnd(`${monthStr}-${String(lastDay).padStart(2, "0")}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateMonth = (delta) => {
|
||||||
|
let month = calendarMonth + delta;
|
||||||
|
let year = calendarYear;
|
||||||
|
if (month < 0) {
|
||||||
|
month = 11;
|
||||||
|
year--;
|
||||||
|
}
|
||||||
|
if (month > 11) {
|
||||||
|
month = 0;
|
||||||
|
year++;
|
||||||
|
}
|
||||||
|
setCalendarMonth(month);
|
||||||
|
setCalendarYear(year);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isEntryStep = !selectedStart || !!selectedEnd;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-3">
|
||||||
|
{/* Step Indicator */}
|
||||||
|
<div className="flex items-center justify-center gap-2 mb-3">
|
||||||
|
<div
|
||||||
|
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${isEntryStep ? "bg-amber-100 text-amber-800" : "bg-gray-100 text-gray-500"}`}
|
||||||
|
>
|
||||||
|
<div className={`w-1.5 h-1.5 rounded-full ${isEntryStep ? "bg-amber-500" : "bg-gray-400"}`} />
|
||||||
|
{t("selectStartDate")}
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-300 text-xs">←</div>
|
||||||
|
<div className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${!isEntryStep ? "bg-amber-100 text-amber-800" : "bg-gray-100 text-gray-500"}`}>
|
||||||
|
<div className={`w-1.5 h-1.5 rounded-full ${!isEntryStep ? "bg-amber-500" : "bg-gray-400"}`} />
|
||||||
|
{t("selectEndDate")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{mode === "daily" ? (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<button onClick={() => navigateMonth(-1)} className="p-1 rounded-lg hover:bg-gray-100 transition-colors">
|
||||||
|
<ChevronRight className="w-4 h-4 text-gray-600" />
|
||||||
|
</button>
|
||||||
|
<span className="text-sm font-bold text-gray-900">
|
||||||
|
{t(MONTH_KEYS[calendarMonth])} {calendarYear}
|
||||||
|
</span>
|
||||||
|
<button onClick={() => navigateMonth(1)} className="p-1 rounded-lg hover:bg-gray-100 transition-colors">
|
||||||
|
<ChevronLeft className="w-4 h-4 text-gray-600" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-7 mb-1">
|
||||||
|
{DAY_KEYS.map((dk, i) => (
|
||||||
|
<div key={i} className="text-center text-[10px] text-gray-400 font-medium py-1">
|
||||||
|
{t(dk)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(() => {
|
||||||
|
const firstDay = new Date(calendarYear, calendarMonth, 1).getDay();
|
||||||
|
const daysInMonth = new Date(calendarYear, calendarMonth + 1, 0).getDate();
|
||||||
|
const cells = [];
|
||||||
|
for (let i = 0; i < firstDay; i++) {
|
||||||
|
cells.push(<div key={`e-${i}`} />);
|
||||||
|
}
|
||||||
|
for (let day = 1; day <= daysInMonth; day++) {
|
||||||
|
const dateStr = `${calendarYear}-${String(calendarMonth + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
||||||
|
const past = isPastDate(dateStr);
|
||||||
|
const available = isDateAvailable(dateStr);
|
||||||
|
const isSelStart = dateStr === selectedStart;
|
||||||
|
const isSelEnd = dateStr === selectedEnd;
|
||||||
|
const inRange =
|
||||||
|
selectedStart &&
|
||||||
|
selectedEnd &&
|
||||||
|
new Date(dateStr) > new Date(selectedStart) &&
|
||||||
|
new Date(dateStr) < new Date(selectedEnd);
|
||||||
|
const disabled = past || !available;
|
||||||
|
cells.push(
|
||||||
|
<button
|
||||||
|
key={dateStr}
|
||||||
|
onClick={() => !disabled && handleDayClick(dateStr)}
|
||||||
|
disabled={disabled}
|
||||||
|
className={`text-center py-1.5 text-xs rounded-lg transition-all ${disabled ? "text-gray-300 cursor-not-allowed" : ""} ${isSelStart || isSelEnd ? "bg-amber-500 text-white font-bold shadow-sm" : ""} ${inRange ? "bg-amber-100 text-amber-800" : ""} ${!disabled && !isSelStart && !isSelEnd && !inRange && available ? "hover:bg-amber-50 text-gray-700" : ""} ${!disabled && !isSelStart && !isSelEnd && !inRange && !available ? "text-red-300" : ""}`}
|
||||||
|
>
|
||||||
|
{day}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <div className="grid grid-cols-7 gap-0.5">{cells}</div>;
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<button onClick={() => setCalendarYear((prev) => prev - 1)} className="p-1 rounded-lg hover:bg-gray-100 transition-colors">
|
||||||
|
<ChevronRight className="w-4 h-4 text-gray-600" />
|
||||||
|
</button>
|
||||||
|
<span className="text-sm font-bold text-gray-900">{calendarYear}</span>
|
||||||
|
<button onClick={() => setCalendarYear((prev) => prev + 1)} className="p-1 rounded-lg hover:bg-gray-100 transition-colors">
|
||||||
|
<ChevronLeft className="w-4 h-4 text-gray-600" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
{MONTH_KEYS.map((mk, idx) => {
|
||||||
|
const monthStr = `${calendarYear}-${String(idx + 1).padStart(2, "0")}`;
|
||||||
|
const isSelStart = selectedStart && selectedStart.startsWith(monthStr);
|
||||||
|
const isSelEnd = selectedEnd && selectedEnd.startsWith(monthStr);
|
||||||
|
const inRange =
|
||||||
|
selectedStart &&
|
||||||
|
selectedEnd &&
|
||||||
|
monthStr > selectedStart.substring(0, 7) &&
|
||||||
|
monthStr < selectedEnd.substring(0, 7);
|
||||||
|
const disabled = isPastMonth(calendarYear, idx) || !isMonthFullyAvailable(calendarYear, idx);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={idx}
|
||||||
|
onClick={() => !disabled && handleMonthClick(calendarYear, idx)}
|
||||||
|
disabled={disabled}
|
||||||
|
className={`p-2.5 rounded-xl text-center text-xs font-medium transition-all border ${disabled ? "text-gray-300 border-gray-100 bg-gray-50 cursor-not-allowed" : ""} ${isSelStart || isSelEnd ? "bg-amber-500 text-white border-amber-500 shadow-sm" : ""} ${!disabled && inRange ? "bg-amber-100 text-amber-800 border-amber-200" : ""} ${!disabled && !isSelStart && !isSelEnd && !inRange ? "bg-white text-gray-700 border-gray-200 hover:border-amber-300" : ""}`}
|
||||||
|
>
|
||||||
|
{t(mk)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{rangeError && (
|
||||||
|
<div className="bg-red-50 text-red-600 p-2.5 rounded-xl text-xs mt-3">
|
||||||
|
{rangeError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { X, Loader2 } from 'lucide-react';
|
import { X, Loader2 } from 'lucide-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import StarRating from './StarRating';
|
import StarRating from './StarRating';
|
||||||
import { addCustomerRating } from '../../utils/ratings';
|
import { addCustomerRating } from '../../utils/ratings';
|
||||||
@ -10,11 +11,12 @@ const RatingField = ({ label, value, onChange }) => (
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="block text-sm font-medium text-gray-700">{label} <span className="text-red-500">*</span></label>
|
<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} />
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default function CustomerRatingForm({ reservationId, onSuccess, onCancel }) {
|
export default function CustomerRatingForm({ reservationId, onSuccess, onCancel }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [furnitureIntegrityRating, setFurnitureIntegrityRating] = useState(0);
|
const [furnitureIntegrityRating, setFurnitureIntegrityRating] = useState(0);
|
||||||
const [termsComplianceRating, setTermsComplianceRating] = useState(0);
|
const [termsComplianceRating, setTermsComplianceRating] = useState(0);
|
||||||
const [renterBehaviorRating, setRenterBehaviorRating] = useState(0);
|
const [renterBehaviorRating, setRenterBehaviorRating] = useState(0);
|
||||||
@ -22,9 +24,9 @@ export default function CustomerRatingForm({ reservationId, onSuccess, onCancel
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const validate = () => {
|
const validate = () => {
|
||||||
if (furnitureIntegrityRating === 0) return 'الحفاظ على الأثاث';
|
if (furnitureIntegrityRating === 0) return t('ratings.furnitureIntegrity');
|
||||||
if (termsComplianceRating === 0) return 'الالتزام بالشروط';
|
if (termsComplianceRating === 0) return t('ratings.termsCompliance');
|
||||||
if (renterBehaviorRating === 0) return 'سلوك المستأجر';
|
if (renterBehaviorRating === 0) return t('ratings.renterBehavior');
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -32,7 +34,7 @@ export default function CustomerRatingForm({ reservationId, onSuccess, onCancel
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const missing = validate();
|
const missing = validate();
|
||||||
if (missing) {
|
if (missing) {
|
||||||
toast.error(`يرجى تقييم: ${missing}`);
|
toast.error(t('ratings.pleaseRate') + ': ' + missing);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,10 +47,10 @@ export default function CustomerRatingForm({ reservationId, onSuccess, onCancel
|
|||||||
renterBehaviorRating,
|
renterBehaviorRating,
|
||||||
comment: comment.trim() || null,
|
comment: comment.trim() || null,
|
||||||
});
|
});
|
||||||
toast.success('تم إرسال تقييم المستأجر بنجاح!');
|
toast.success(t('ratings.customerRatingSubmitted'));
|
||||||
onSuccess?.();
|
onSuccess?.();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error('حدث خطأ، حاول مرة أخرى');
|
toast.error(t('ratings.submitError'));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -57,7 +59,7 @@ export default function CustomerRatingForm({ reservationId, onSuccess, onCancel
|
|||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-2xl shadow-lg border border-gray-200 p-6 max-w-lg mx-auto">
|
<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">
|
<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 && (
|
{onCancel && (
|
||||||
<button onClick={onCancel} className="text-gray-400 hover:text-gray-600">
|
<button onClick={onCancel} className="text-gray-400 hover:text-gray-600">
|
||||||
<X size={20} />
|
<X size={20} />
|
||||||
@ -65,17 +67,17 @@ export default function CustomerRatingForm({ reservationId, onSuccess, onCancel
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={handleSubmit} className="space-y-5">
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
<RatingField label="الحفاظ على الأثاث" value={furnitureIntegrityRating} onChange={setFurnitureIntegrityRating} />
|
<RatingField label={t('ratings.furnitureIntegrity')} value={furnitureIntegrityRating} onChange={setFurnitureIntegrityRating} />
|
||||||
<RatingField label="الالتزام بالشروط" value={termsComplianceRating} onChange={setTermsComplianceRating} />
|
<RatingField label={t('ratings.termsCompliance')} value={termsComplianceRating} onChange={setTermsComplianceRating} />
|
||||||
<RatingField label="سلوك المستأجر" value={renterBehaviorRating} onChange={setRenterBehaviorRating} />
|
<RatingField label={t('ratings.renterBehavior')} value={renterBehaviorRating} onChange={setRenterBehaviorRating} />
|
||||||
<div>
|
<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
|
<textarea
|
||||||
rows={3}
|
rows={3}
|
||||||
value={comment}
|
value={comment}
|
||||||
onChange={(e) => setComment(e.target.value)}
|
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"
|
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>
|
</div>
|
||||||
<button
|
<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"
|
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 && <Loader2 className="w-5 h-5 animate-spin" />}
|
||||||
{loading ? 'جاري الإرسال...' : 'إرسال التقييم'}
|
{loading ? t('ratings.sending') : t('ratings.submitRating')}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
32
app/contexts/ThemeContext.js
Normal file
32
app/contexts/ThemeContext.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { createContext, useContext, useState, useEffect } from "react";
|
||||||
|
|
||||||
|
const ThemeContext = createContext();
|
||||||
|
|
||||||
|
export function ThemeProvider({ children }) {
|
||||||
|
const [theme, setTheme] = useState("dark");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const stored = localStorage.getItem("theme");
|
||||||
|
if (stored === "dark" || (!stored && window.matchMedia("(prefers-color-scheme: dark)").matches)) {
|
||||||
|
setTheme("dark");
|
||||||
|
document.documentElement.classList.add("dark");
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
const next = theme === "light" ? "dark" : "light";
|
||||||
|
setTheme(next);
|
||||||
|
document.documentElement.classList.toggle("dark", next === "dark");
|
||||||
|
localStorage.setItem("theme", next);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTheme = () => useContext(ThemeContext);
|
||||||
@ -26,4 +26,13 @@ const BookingStatusColors = Object.freeze({
|
|||||||
[BookingStatus.cancelled]: 'red',
|
[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 };
|
||||||
|
|||||||
@ -56,4 +56,17 @@ const BuildingTypeByKey = Object.freeze({
|
|||||||
warehouse: BuildingType.WAREHOUSE,
|
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 };
|
||||||
|
|||||||
@ -10,4 +10,10 @@ const CancellationReasonLabels = Object.freeze({
|
|||||||
[CancellationReason.OWNER_CANCELED]: 'إلغاء من المالك',
|
[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 };
|
||||||
|
|||||||
@ -18,7 +18,21 @@ const City = Object.freeze({
|
|||||||
QAMISHLI: 'القامشلي',
|
QAMISHLI: 'القامشلي',
|
||||||
RURAL_DAMASCUS: 'ريف دمشق',
|
RURAL_DAMASCUS: 'ريف دمشق',
|
||||||
});
|
});
|
||||||
|
export const CityEnum = Object.freeze({
|
||||||
|
DAMASCUS: 0,
|
||||||
|
ALEPPO: 1,
|
||||||
|
HOMS: 2,
|
||||||
|
LATAKIA: 3,
|
||||||
|
DARAA: 4,
|
||||||
|
TARTOUS: 5,
|
||||||
|
SUWEIDA: 6,
|
||||||
|
DEIR_EZZOR: 7,
|
||||||
|
RAQQA: 8,
|
||||||
|
IDLIB: 9,
|
||||||
|
HASAKAH: 10,
|
||||||
|
Qamishli: 11,
|
||||||
|
RURAL_DAMASCUS: 12,
|
||||||
|
});
|
||||||
// All cities as a flat array
|
// All cities as a flat array
|
||||||
const CitiesList = Object.freeze(Object.values(City));
|
const CitiesList = Object.freeze(Object.values(City));
|
||||||
|
|
||||||
@ -35,4 +49,22 @@ function extractCity(address) {
|
|||||||
return '';
|
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, };
|
||||||
|
|||||||
@ -16,4 +16,10 @@ const CommissionTypeLabels = Object.freeze({
|
|||||||
[CommissionType.FROM_BOTH]: 'من الاثنين',
|
[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 };
|
||||||
|
|||||||
@ -23,4 +23,11 @@ const CurrencySymbols = Object.freeze({
|
|||||||
[Currency.TRY]: 'TRY',
|
[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 };
|
||||||
|
|||||||
@ -14,4 +14,9 @@ const CustomerTypeLabels = Object.freeze({
|
|||||||
[CustomerType.FAMILY]: 'عائلي',
|
[CustomerType.FAMILY]: 'عائلي',
|
||||||
});
|
});
|
||||||
|
|
||||||
export { CustomerType, CustomerTypeLabels };
|
const CustomerTypeTranslationKeys = Object.freeze({
|
||||||
|
[CustomerType.PERSONAL]: 'customerType.personal',
|
||||||
|
[CustomerType.FAMILY]: 'customerType.family',
|
||||||
|
});
|
||||||
|
|
||||||
|
export { CustomerType, CustomerTypeLabels, CustomerTypeTranslationKeys };
|
||||||
|
|||||||
19
app/enums/DocumentType.js
Normal file
19
app/enums/DocumentType.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* Document type enum for owner property submission.
|
||||||
|
* Values are intentionally English keys so the UI can store normalized identifiers.
|
||||||
|
*/
|
||||||
|
const DocumentType = Object.freeze({
|
||||||
|
PASSPORT: 'Passport',
|
||||||
|
IDCARD: 'IDCard',
|
||||||
|
BOTH: 'Both',
|
||||||
|
});
|
||||||
|
|
||||||
|
const DocumentTypeLabels = Object.freeze({
|
||||||
|
[DocumentType.PASSPORT]: 'جواز سفر',
|
||||||
|
[DocumentType.IDCARD]: 'بطاقة شخصية',
|
||||||
|
[DocumentType.BOTH]: 'كلاهما',
|
||||||
|
});
|
||||||
|
|
||||||
|
const DocumentTypeList = Object.freeze(Object.values(DocumentType));
|
||||||
|
|
||||||
|
export { DocumentType, DocumentTypeLabels, DocumentTypeList };
|
||||||
39
app/enums/Governorate.js
Normal file
39
app/enums/Governorate.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Syrian governorates enum using English normalized values.
|
||||||
|
* Labels are provided separately for Arabic display.
|
||||||
|
*/
|
||||||
|
const Governorate = Object.freeze({
|
||||||
|
DAMASCUS: 'Damascus',
|
||||||
|
ALEPPO: 'Aleppo',
|
||||||
|
LATAKIA: 'Latakia',
|
||||||
|
HAMA: 'Hama',
|
||||||
|
TARTOUS: 'Tartous',
|
||||||
|
DEIR_EZZOR: 'DeirEzzor',
|
||||||
|
RAQQA: 'Raqqa',
|
||||||
|
IDLIB: 'Idlib',
|
||||||
|
DRAA: 'Daraa',
|
||||||
|
SUWEIDA: 'Suweida',
|
||||||
|
HASAKAH: 'Hasakah',
|
||||||
|
QUNEITRA: 'Quneitra',
|
||||||
|
RURAL_DAMASCUS: 'RuralDamascus',
|
||||||
|
});
|
||||||
|
|
||||||
|
const GovernorateLabels = Object.freeze({
|
||||||
|
[Governorate.DAMASCUS]: 'دمشق',
|
||||||
|
[Governorate.ALEPPO]: 'حلب',
|
||||||
|
[Governorate.LATAKIA]: 'اللاذقية',
|
||||||
|
[Governorate.HAMA]: 'حماة',
|
||||||
|
[Governorate.TARTOUS]: 'طرطوس',
|
||||||
|
[Governorate.DEIR_EZZOR]: 'دير الزور',
|
||||||
|
[Governorate.RAQQA]: 'الرقة',
|
||||||
|
[Governorate.IDLIB]: 'إدلب',
|
||||||
|
[Governorate.DRAA]: 'درعا',
|
||||||
|
[Governorate.SUWEIDA]: 'السويداء',
|
||||||
|
[Governorate.HASAKAH]: 'الحسكة',
|
||||||
|
[Governorate.QUNEITRA]: 'القنيطرة',
|
||||||
|
[Governorate.RURAL_DAMASCUS]: 'ريف دمشق',
|
||||||
|
});
|
||||||
|
|
||||||
|
const GovernorateList = Object.freeze(Object.values(Governorate));
|
||||||
|
|
||||||
|
export { Governorate, GovernorateLabels, GovernorateList };
|
||||||
@ -20,4 +20,9 @@ const IdentityTypeFlags = Object.freeze({
|
|||||||
[IdentityType.PASSPORT]: '🛂',
|
[IdentityType.PASSPORT]: '🛂',
|
||||||
});
|
});
|
||||||
|
|
||||||
export { IdentityType, IdentityTypeLabels, IdentityTypeFlags };
|
const IdentityTypeTranslationKeys = Object.freeze({
|
||||||
|
[IdentityType.SYRIAN]: 'identityType.syrian',
|
||||||
|
[IdentityType.PASSPORT]: 'identityType.passport',
|
||||||
|
});
|
||||||
|
|
||||||
|
export { IdentityType, IdentityTypeLabels, IdentityTypeFlags, IdentityTypeTranslationKeys };
|
||||||
|
|||||||
@ -14,4 +14,9 @@ const OwnerTypeLabels = Object.freeze({
|
|||||||
[OwnerType.REAL_ESTATE_AGENCY]: 'وكالة عقارية',
|
[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 };
|
||||||
|
|||||||
@ -38,4 +38,20 @@ const PropertyServiceLabels = Object.freeze({
|
|||||||
// All services as array
|
// All services as array
|
||||||
const PropertyServicesList = Object.freeze(Object.values(PropertyService));
|
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 };
|
||||||
|
|||||||
@ -27,4 +27,10 @@ const PropertyStatusByKey = Object.freeze({
|
|||||||
booked: PropertyStatus.BOOKED,
|
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 };
|
||||||
|
|||||||
@ -18,4 +18,10 @@ const PropertyTermLabels = Object.freeze({
|
|||||||
// All terms as array
|
// All terms as array
|
||||||
const PropertyTermsList = Object.freeze(Object.values(PropertyTerm));
|
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 };
|
||||||
|
|||||||
@ -13,4 +13,9 @@ const RentPropertyConditionLabels = Object.freeze({
|
|||||||
[RentPropertyCondition.WITHOUT_FURNITURE]: 'غير مفروش',
|
[RentPropertyCondition.WITHOUT_FURNITURE]: 'غير مفروش',
|
||||||
});
|
});
|
||||||
|
|
||||||
export { RentPropertyCondition, RentPropertyConditionLabels };
|
const RentPropertyConditionTranslationKeys = Object.freeze({
|
||||||
|
[RentPropertyCondition.WITH_FURNITURE]: 'rentCondition.furnished',
|
||||||
|
[RentPropertyCondition.WITHOUT_FURNITURE]: 'rentCondition.unfurnished',
|
||||||
|
});
|
||||||
|
|
||||||
|
export { RentPropertyCondition, RentPropertyConditionLabels, RentPropertyConditionTranslationKeys };
|
||||||
|
|||||||
@ -14,4 +14,10 @@ const RentPropertyTypeLabels = Object.freeze({
|
|||||||
[RentPropertyType.SEMI_FURNISHED]: 'مفروش جزئياً',
|
[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 };
|
||||||
|
|||||||
@ -13,4 +13,9 @@ const RentTypeLabels = Object.freeze({
|
|||||||
[RentType.DAILY]: 'يومي',
|
[RentType.DAILY]: 'يومي',
|
||||||
});
|
});
|
||||||
|
|
||||||
export { RentType, RentTypeLabels };
|
const RentTypeTranslationKeys = Object.freeze({
|
||||||
|
[RentType.MONTHLY]: 'rentType.monthly',
|
||||||
|
[RentType.DAILY]: 'rentType.daily',
|
||||||
|
});
|
||||||
|
|
||||||
|
export { RentType, RentTypeLabels, RentTypeTranslationKeys };
|
||||||
|
|||||||
@ -8,4 +8,9 @@ const SalePropertiesStatusLabels = Object.freeze({
|
|||||||
[SalePropertiesStatus.CONFIRMED]: 'مؤكد',
|
[SalePropertiesStatus.CONFIRMED]: 'مؤكد',
|
||||||
});
|
});
|
||||||
|
|
||||||
export { SalePropertiesStatus, SalePropertiesStatusLabels };
|
const SalePropertiesStatusTranslationKeys = Object.freeze({
|
||||||
|
[SalePropertiesStatus.PENDING]: 'saleStatus.pending',
|
||||||
|
[SalePropertiesStatus.CONFIRMED]: 'saleStatus.confirmed',
|
||||||
|
});
|
||||||
|
|
||||||
|
export { SalePropertiesStatus, SalePropertiesStatusLabels, SalePropertiesStatusTranslationKeys };
|
||||||
|
|||||||
@ -8,4 +8,9 @@ const TransactionTypeLabels = Object.freeze({
|
|||||||
[TransactionType.MANUAL]: 'يدوي',
|
[TransactionType.MANUAL]: 'يدوي',
|
||||||
});
|
});
|
||||||
|
|
||||||
export { TransactionType, TransactionTypeLabels };
|
const TransactionTypeTranslationKeys = Object.freeze({
|
||||||
|
[TransactionType.ONLINE]: 'transactionType.online',
|
||||||
|
[TransactionType.MANUAL]: 'transactionType.manual',
|
||||||
|
});
|
||||||
|
|
||||||
|
export { TransactionType, TransactionTypeLabels, TransactionTypeTranslationKeys };
|
||||||
|
|||||||
@ -7,18 +7,28 @@ const UserRole = Object.freeze({
|
|||||||
GUEST: 'guest',
|
GUEST: 'guest',
|
||||||
CUSTOMER: 'customer',
|
CUSTOMER: 'customer',
|
||||||
OWNER: 'owner',
|
OWNER: 'owner',
|
||||||
|
AGENT: 'agent',
|
||||||
});
|
});
|
||||||
|
|
||||||
const UserRoleLabels = Object.freeze({
|
const UserRoleLabels = Object.freeze({
|
||||||
[UserRole.GUEST]: 'زائر',
|
[UserRole.GUEST]: 'زائر',
|
||||||
[UserRole.CUSTOMER]: 'مستأجر',
|
[UserRole.CUSTOMER]: 'مستأجر',
|
||||||
[UserRole.OWNER]: 'مالك عقار',
|
[UserRole.OWNER]: 'مالك عقار',
|
||||||
|
[UserRole.AGENT]: 'وسيط عقاري',
|
||||||
});
|
});
|
||||||
|
|
||||||
const UserRoleColors = Object.freeze({
|
const UserRoleColors = Object.freeze({
|
||||||
[UserRole.GUEST]: 'gray',
|
[UserRole.GUEST]: 'gray',
|
||||||
[UserRole.CUSTOMER]: 'blue',
|
[UserRole.CUSTOMER]: 'blue',
|
||||||
[UserRole.OWNER]: 'amber',
|
[UserRole.OWNER]: 'amber',
|
||||||
|
[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 };
|
||||||
|
|||||||
@ -18,6 +18,8 @@ export { RentPropertyType, RentPropertyTypeLabels } from './RentPropertyType';
|
|||||||
export { RentType, RentTypeLabels } from './RentType';
|
export { RentType, RentTypeLabels } from './RentType';
|
||||||
export { PropertyService, PropertyServiceLabels, PropertyServicesList } from './PropertyService';
|
export { PropertyService, PropertyServiceLabels, PropertyServicesList } from './PropertyService';
|
||||||
export { PropertyTerm, PropertyTermLabels, PropertyTermsList } from './PropertyTerm';
|
export { PropertyTerm, PropertyTermLabels, PropertyTermsList } from './PropertyTerm';
|
||||||
|
export { DocumentType, DocumentTypeLabels, DocumentTypeList } from './DocumentType';
|
||||||
|
export { Governorate, GovernorateLabels, GovernorateList } from './Governorate';
|
||||||
export { Currency, CurrencyLabels, CurrencySymbols } from './Currency';
|
export { Currency, CurrencyLabels, CurrencySymbols } from './Currency';
|
||||||
export { DeviceType } from './DeviceType';
|
export { DeviceType } from './DeviceType';
|
||||||
export { SalePropertiesStatus, SalePropertiesStatusLabels } from './SalePropertiesStatus';
|
export { SalePropertiesStatus, SalePropertiesStatusLabels } from './SalePropertiesStatus';
|
||||||
|
|||||||
@ -2,47 +2,25 @@
|
|||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { HelpCircle, ChevronDown, Search } from 'lucide-react';
|
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() {
|
export default function FAQPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [openIndex, setOpenIndex] = useState(null);
|
const [openIndex, setOpenIndex] = useState(null);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
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(
|
const filteredFaqs = faqs.filter(
|
||||||
(faq) =>
|
(faq) =>
|
||||||
faq.question.includes(searchTerm) ||
|
faq.question.includes(searchTerm) ||
|
||||||
@ -54,7 +32,7 @@ export default function FAQPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12" dir="rtl">
|
<div className="min-h-screen py-12" dir="rtl">
|
||||||
<div className="container mx-auto px-4 max-w-4xl">
|
<div className="container mx-auto px-4 max-w-4xl">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: -20 }}
|
initial={{ opacity: 0, y: -20 }}
|
||||||
@ -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">
|
<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" />
|
<HelpCircle className="w-10 h-10 text-amber-600" />
|
||||||
</div>
|
</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">
|
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
||||||
إجابات على أكثر الأسئلة شيوعاً حول استخدام المنصة
|
{t('faq.pageDescription')}
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</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" />
|
<Search className="absolute right-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="ابحث في الأسئلة الشائعة..."
|
placeholder={t('faq.searchPlaceholder')}
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
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"
|
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 }}
|
animate={{ opacity: 1 }}
|
||||||
className="text-center py-12"
|
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>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
BIN
app/favicon.ico
BIN
app/favicon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 59 KiB |
@ -1,15 +1,18 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from 'next/navigation';
|
import { useTranslation } from "react-i18next";
|
||||||
import { motion } from 'framer-motion';
|
import { useRouter } from "next/navigation";
|
||||||
import Link from 'next/link';
|
import { motion } from "framer-motion";
|
||||||
import Image from 'next/image';
|
import Link from "next/link";
|
||||||
import { Heart, MapPin, Bed, Bath, Square, X, ImageIcon } from 'lucide-react';
|
import Image from "next/image";
|
||||||
import { useFavorites } from '@/app/contexts/FavoritesContext';
|
import { Heart, MapPin, Bed, Bath, Square, X, ImageIcon } from "lucide-react";
|
||||||
import AuthService from '@/app/services/AuthService';
|
import { useFavorites } from "@/app/contexts/FavoritesContext";
|
||||||
|
import AuthService from "@/app/services/AuthService";
|
||||||
|
import Loading from "../loading";
|
||||||
|
|
||||||
export default function FavoritesPage() {
|
export default function FavoritesPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { favorites, isLoading: favoritesLoading, removeFavorite } = useFavorites();
|
const { favorites, isLoading: favoritesLoading, removeFavorite } = useFavorites();
|
||||||
|
|
||||||
@ -23,38 +26,28 @@ export default function FavoritesPage() {
|
|||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
const formatCurrency = (amount) => {
|
const formatCurrency = (amount) => {
|
||||||
return amount?.toLocaleString() + ' ل.س';
|
return amount?.toLocaleString() + " " + t("currency-syp-suffix");
|
||||||
};
|
};
|
||||||
|
|
||||||
if (favoritesLoading && favorites.length === 0) {
|
if (favoritesLoading && favorites.length === 0) {
|
||||||
return (
|
return <Loading />;
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="w-16 h-16 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
|
||||||
<p className="text-gray-600">جاري التحميل...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 py-8">
|
<div className="min-h-screen bg-gray-50 py-8">
|
||||||
<div className="container mx-auto px-4 max-w-6xl">
|
<div className="container mx-auto px-4 max-w-6xl">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">المفضلة</h1>
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t("favorites")}</h1>
|
||||||
<p className="text-gray-600">العقارات التي قمت بحفظها</p>
|
<p className="text-gray-600">{t("saved-properties")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{favorites.length === 0 ? (
|
{favorites.length === 0 ? (
|
||||||
<div className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
|
<div className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
|
||||||
<Heart className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
<Heart className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||||
<h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد عقارات في المفضلة</h3>
|
<h3 className="text-xl font-bold text-gray-700 mb-2">{t("no-favorites")}</h3>
|
||||||
<p className="text-gray-500 mb-6">يمكنك إضافة العقارات التي تعجبك بالنقر على أيقونة القلب</p>
|
<p className="text-gray-500 mb-6">{t("add-favorites-hint")}</p>
|
||||||
<Link
|
<Link href="/properties" className="inline-flex items-center gap-2 bg-amber-500 text-white px-6 py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors">
|
||||||
href="/properties"
|
{t("browse-properties")}
|
||||||
className="inline-flex items-center gap-2 bg-amber-500 text-white px-6 py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors"
|
|
||||||
>
|
|
||||||
استعرض العقارات
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -68,13 +61,7 @@ export default function FavoritesPage() {
|
|||||||
>
|
>
|
||||||
<div className="relative h-48 bg-gray-100">
|
<div className="relative h-48 bg-gray-100">
|
||||||
{property.images && property.images[0] ? (
|
{property.images && property.images[0] ? (
|
||||||
<Image
|
<Image src={property.images[0]} alt={property.title} fill className="object-cover" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" />
|
||||||
src={property.images[0]}
|
|
||||||
alt={property.title}
|
|
||||||
fill
|
|
||||||
className="object-cover"
|
|
||||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full flex items-center justify-center">
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
<ImageIcon className="w-12 h-12 text-gray-400" />
|
<ImageIcon className="w-12 h-12 text-gray-400" />
|
||||||
@ -93,7 +80,7 @@ export default function FavoritesPage() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-lg text-xs font-medium">
|
<span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-lg text-xs font-medium">
|
||||||
{property.type === 'apartment' ? 'شقة' : property.type === 'villa' ? 'فيلا' : 'بيت'}
|
{property.type === "apartment" ? t("buildingType.apartment") : property.type === "villa" ? t("buildingType.villa") : t("buildingType.house")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-bold text-gray-900 mb-1 line-clamp-1">{property.title}</h3>
|
<h3 className="font-bold text-gray-900 mb-1 line-clamp-1">{property.title}</h3>
|
||||||
@ -106,7 +93,7 @@ export default function FavoritesPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<div className="text-xl font-bold text-gray-900">{formatCurrency(property.price)}</div>
|
<div className="text-xl font-bold text-gray-900">{formatCurrency(property.price)}</div>
|
||||||
<div className="text-xs text-gray-500">/{property.priceUnit === 'daily' ? 'يوم' : 'شهر'}</div>
|
<div className="text-xs text-gray-500">/{property.priceUnit === "daily" ? t("day") : t("month")}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -127,11 +114,8 @@ export default function FavoritesPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link
|
<Link href={`/property/${property.id}`} className="block w-full bg-amber-500 text-white py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors text-center">
|
||||||
href={`/property/${property.id}`}
|
{t("viewDetails")}
|
||||||
className="block w-full bg-amber-500 text-white py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors text-center"
|
|
||||||
>
|
|
||||||
عرض التفاصيل
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
|
|
||||||
export default function Loading() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center">
|
|
||||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-center">
|
|
||||||
<div className="w-14 h-14 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
|
||||||
<p className="text-gray-400 text-lg">جاري التحميل...</p>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -6,10 +6,13 @@ import { useRouter } from 'next/navigation';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import toast, { Toaster } from 'react-hot-toast';
|
import toast, { Toaster } from 'react-hot-toast';
|
||||||
import { Mail, Key, Lock, CheckCircle, ArrowLeft, RefreshCw } from 'lucide-react';
|
import { Mail, Key, Lock, CheckCircle, ArrowLeft, RefreshCw } from 'lucide-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { requestForgetPasswordOtp, verifyForgetPasswordOtp } from '../utils/api';
|
import { requestForgetPasswordOtp, verifyForgetPasswordOtp } from '../utils/api';
|
||||||
|
import InteractiveBackground from '../components/Animation/Background';
|
||||||
|
|
||||||
export default function ForgotPasswordPage() {
|
export default function ForgotPasswordPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { t } = useTranslation();
|
||||||
const [step, setStep] = useState(1);
|
const [step, setStep] = useState(1);
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [code, setCode] = useState('');
|
const [code, setCode] = useState('');
|
||||||
@ -20,17 +23,17 @@ export default function ForgotPasswordPage() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!email.trim()) {
|
if (!email.trim()) {
|
||||||
toast.error('يرجى إدخال البريد الإلكتروني');
|
toast.error(t('forgotPassword.emailRequired'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
await requestForgetPasswordOtp(email);
|
await requestForgetPasswordOtp(email);
|
||||||
toast.success('تم إرسال رمز التحقق إلى بريدك الإلكتروني');
|
toast.success(t('forgotPassword.otpSent'));
|
||||||
setStep(2);
|
setStep(2);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err?.message || 'فشل إرسال رمز التحقق');
|
toast.error(err?.message || t('forgotPassword.otpSendFailed'));
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@ -40,22 +43,22 @@ export default function ForgotPasswordPage() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!code.trim()) {
|
if (!code.trim()) {
|
||||||
toast.error('يرجى إدخال رمز التحقق');
|
toast.error(t('forgotPassword.otpRequired'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newPassword.length < 6) {
|
if (newPassword.length < 6) {
|
||||||
toast.error('كلمة المرور الجديدة يجب أن تكون 6 أحرف على الأقل');
|
toast.error(t('validation.passwordMinLength'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
await verifyForgetPasswordOtp(email, code, newPassword);
|
await verifyForgetPasswordOtp(email, code, newPassword);
|
||||||
toast.success('تم إعادة تعيين كلمة المرور بنجاح');
|
toast.success(t('forgotPassword.resetSuccess'));
|
||||||
setTimeout(() => router.push('/login'), 1200);
|
setTimeout(() => router.push('/login'), 1200);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err?.message || 'فشل التحقق من الرمز');
|
toast.error(err?.message || t('forgotPassword.otpVerifyFailed'));
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@ -64,8 +67,9 @@ export default function ForgotPasswordPage() {
|
|||||||
const inputClass = "w-full pr-12 pl-4 py-3 bg-white/10 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400 transition-all";
|
const inputClass = "w-full pr-12 pl-4 py-3 bg-white/10 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400 transition-all";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-amber-50 via-orange-50 to-yellow-50 flex items-center justify-center p-4 relative overflow-hidden">
|
<div className="min-h-screen flex items-center justify-center p-4 relative overflow-hidden">
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
<InteractiveBackground/>
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
<div className="absolute -top-40 -right-40 w-80 h-80 bg-amber-400 rounded-full opacity-20 blur-3xl animate-pulse"></div>
|
<div className="absolute -top-40 -right-40 w-80 h-80 bg-amber-400 rounded-full opacity-20 blur-3xl animate-pulse"></div>
|
||||||
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-orange-400 rounded-full opacity-20 blur-3xl animate-pulse delay-1000"></div>
|
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-orange-400 rounded-full opacity-20 blur-3xl animate-pulse delay-1000"></div>
|
||||||
@ -84,7 +88,7 @@ export default function ForgotPasswordPage() {
|
|||||||
>
|
>
|
||||||
<Link href="/login" className="flex items-center gap-2 text-gray-600 hover:text-amber-600 transition-colors group">
|
<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" />
|
<ArrowLeft className="w-5 h-5 group-hover:-translate-x-1 transition-transform" />
|
||||||
<span>العودة لتسجيل الدخول</span>
|
<span>{t('forgotPassword.backToLogin')}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@ -95,7 +99,7 @@ export default function ForgotPasswordPage() {
|
|||||||
animate={{ y: 0, opacity: 1 }}
|
animate={{ y: 0, opacity: 1 }}
|
||||||
className="text-3xl font-bold text-white mb-2"
|
className="text-3xl font-bold text-white mb-2"
|
||||||
>
|
>
|
||||||
استعادة كلمة المرور
|
{t('forgotPassword.title')}
|
||||||
</motion.h1>
|
</motion.h1>
|
||||||
<motion.p
|
<motion.p
|
||||||
initial={{ y: 20, opacity: 0 }}
|
initial={{ y: 20, opacity: 0 }}
|
||||||
@ -103,7 +107,7 @@ export default function ForgotPasswordPage() {
|
|||||||
transition={{ delay: 0.1 }}
|
transition={{ delay: 0.1 }}
|
||||||
className="text-amber-100"
|
className="text-amber-100"
|
||||||
>
|
>
|
||||||
{step === 1 ? 'أدخل بريدك الإلكتروني لاستلام رمز التحقق' : 'أدخل رمز التحقق وكلمة المرور الجديدة'}
|
{step === 1 ? t('forgotPassword.step1Instruction') : t('forgotPassword.step2Instruction')}
|
||||||
</motion.p>
|
</motion.p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -120,7 +124,7 @@ export default function ForgotPasswordPage() {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
البريد الإلكتروني
|
{t('forgotPassword.emailLabel')}
|
||||||
</label>
|
</label>
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
@ -131,7 +135,7 @@ export default function ForgotPasswordPage() {
|
|||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
className={inputClass}
|
className={inputClass}
|
||||||
placeholder="أدخل بريدك الإلكتروني"
|
placeholder={t('forgotPassword.emailPlaceholder')}
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@ -148,10 +152,10 @@ export default function ForgotPasswordPage() {
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||||
<span>جاري الإرسال...</span>
|
<span>{t('forgotPassword.sending')}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
'إرسال رمز التحقق'
|
t('forgotPassword.sendOtp')
|
||||||
)}
|
)}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</motion.form>
|
</motion.form>
|
||||||
@ -168,7 +172,7 @@ export default function ForgotPasswordPage() {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
رمز التحقق
|
{t('forgotPassword.otpLabel')}
|
||||||
</label>
|
</label>
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
@ -179,7 +183,7 @@ export default function ForgotPasswordPage() {
|
|||||||
value={code}
|
value={code}
|
||||||
onChange={(e) => setCode(e.target.value)}
|
onChange={(e) => setCode(e.target.value)}
|
||||||
className={inputClass}
|
className={inputClass}
|
||||||
placeholder="أدخل رمز التحقق"
|
placeholder={t('forgotPassword.otpPlaceholder')}
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@ -188,7 +192,7 @@ export default function ForgotPasswordPage() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
كلمة المرور الجديدة
|
{t('forgotPassword.newPasswordLabel')}
|
||||||
</label>
|
</label>
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
@ -199,7 +203,7 @@ export default function ForgotPasswordPage() {
|
|||||||
value={newPassword}
|
value={newPassword}
|
||||||
onChange={(e) => setNewPassword(e.target.value)}
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
className={inputClass}
|
className={inputClass}
|
||||||
placeholder="أدخل كلمة المرور الجديدة"
|
placeholder={t('forgotPassword.newPasswordPlaceholder')}
|
||||||
required
|
required
|
||||||
minLength={6}
|
minLength={6}
|
||||||
/>
|
/>
|
||||||
@ -216,10 +220,10 @@ export default function ForgotPasswordPage() {
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||||
<span>جاري التحقق...</span>
|
<span>{t('forgotPassword.verifying')}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
'إعادة تعيين كلمة المرور'
|
t('forgotPassword.resetPassword')
|
||||||
)}
|
)}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
|
|
||||||
@ -230,7 +234,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"
|
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" />
|
<RefreshCw className="w-4 h-4" />
|
||||||
<span>تغيير البريد الإلكتروني</span>
|
<span>{t('forgotPassword.changeEmail')}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</motion.form>
|
</motion.form>
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
/* ─── Madani Arabic Font ─── */
|
/* ─── Madani Arabic Font ─── */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Madani Arabic';
|
font-family: 'Madani Arabic';
|
||||||
@ -92,6 +94,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: #0f172a;
|
||||||
|
--foreground: #f5e6d3;
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--color-white: #1e293b;
|
||||||
|
--color-gray-50: #0f172a;
|
||||||
|
--color-gray-100: #1e293b;
|
||||||
|
--color-gray-200: #334155;
|
||||||
|
--color-gray-300: #475569;
|
||||||
|
--color-gray-400: #64748b;
|
||||||
|
--color-gray-500: #94a3b8;
|
||||||
|
--color-gray-600: #cbd5e1;
|
||||||
|
--color-gray-700: #e2e8f0;
|
||||||
|
--color-gray-800: #f1f5f9;
|
||||||
|
--color-gray-900: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
font-family: 'Madani Arabic', 'Noto Sans Arabic', 'Cairo', Arial, sans-serif;
|
font-family: 'Madani Arabic', 'Noto Sans Arabic', 'Cairo', Arial, sans-serif;
|
||||||
}
|
}
|
||||||
@ -135,6 +157,13 @@ body {
|
|||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-icon {
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
.dark .nav-icon {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
.property-tooltip {
|
.property-tooltip {
|
||||||
background: white !important;
|
background: white !important;
|
||||||
color: #1f2937 !important;
|
color: #1f2937 !important;
|
||||||
|
|||||||
57
app/hooks/UseFilterOfHeroSearch.js
Normal file
57
app/hooks/UseFilterOfHeroSearch.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export const useFilterOptions = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const cities = [
|
||||||
|
{ 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: 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: 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: t("syrianId") },
|
||||||
|
{ id: "passport", label: t("passport") },
|
||||||
|
];
|
||||||
|
|
||||||
|
const ownerSources = [
|
||||||
|
{ id: "all", label: t("all") },
|
||||||
|
{ id: "owner", label: t("fromOwner") },
|
||||||
|
{ id: "agency", label: t("fromAgency") },
|
||||||
|
];
|
||||||
|
|
||||||
|
const rentPeriods = [
|
||||||
|
{ id: "all", label: t("all") },
|
||||||
|
{ id: "daily", label: t("dailyRent") },
|
||||||
|
{ id: "monthly", label: t("monthlyRent") },
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
cities,
|
||||||
|
propertyTypes,
|
||||||
|
priceRanges,
|
||||||
|
identityTypes,
|
||||||
|
ownerSources,
|
||||||
|
rentPeriods,
|
||||||
|
};
|
||||||
|
};
|
||||||
99
app/hooks/useApplyFilters.js
Normal file
99
app/hooks/useApplyFilters.js
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { getRentProperties, getSaleProperties } from ".././utils/api";
|
||||||
|
import { mapApiProperty } from "../HelperFunction/ApiProperty";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
export default function useApplyFilters() {
|
||||||
|
const [allProperties, setAllProperties] = useState([]);
|
||||||
|
const [rentProperties, setRentProperties] = useState([]);
|
||||||
|
const [saleProperties, setSaleProperties] = useState([]);
|
||||||
|
const [searchFilters, setSearchFilters] = useState(null);
|
||||||
|
const [filteredProperties, setFilteredProperties] = useState([]);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchProperties() {
|
||||||
|
try {
|
||||||
|
const [rentData, saleData] = await Promise.all([getRentProperties().catch(() => []), getSaleProperties().catch(() => [])]);
|
||||||
|
const rentList = Array.isArray(rentData) ? rentData : [];
|
||||||
|
const saleList = Array.isArray(saleData) ? saleData : [];
|
||||||
|
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);
|
||||||
|
setAllProperties([...mappedRent, ...mappedSale]);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[Home] Failed to fetch properties:", err);
|
||||||
|
} finally {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchProperties();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const applyFilters = (filters) => {
|
||||||
|
setSearchFilters(filters);
|
||||||
|
let propertiesToFilter = [];
|
||||||
|
if (filters.mode === "rent") {
|
||||||
|
propertiesToFilter = rentProperties;
|
||||||
|
} else if (filters.mode === "buy" || filters.mode === "sell") {
|
||||||
|
propertiesToFilter = saleProperties;
|
||||||
|
} else {
|
||||||
|
propertiesToFilter = allProperties;
|
||||||
|
}
|
||||||
|
const filtered = propertiesToFilter.filter((property) => {
|
||||||
|
if (filters.city && filters.city !== "all" && property.location.city !== filters.city) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (filters.propertyType && filters.propertyType !== "all" && property.type !== filters.propertyType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (filters.priceRange && filters.priceRange !== "all") {
|
||||||
|
const priceUSD = property.priceUSD;
|
||||||
|
switch (filters.priceRange) {
|
||||||
|
case "0-500":
|
||||||
|
if (priceUSD > 500) return false;
|
||||||
|
break;
|
||||||
|
case "500-1000":
|
||||||
|
if (priceUSD < 501 || priceUSD > 1000) return false;
|
||||||
|
break;
|
||||||
|
case "1000-2000":
|
||||||
|
if (priceUSD < 1001 || priceUSD > 2000) return false;
|
||||||
|
break;
|
||||||
|
case "2000-3000":
|
||||||
|
if (priceUSD < 2001 || priceUSD > 300) return false;
|
||||||
|
break;
|
||||||
|
case "3000+":
|
||||||
|
if (priceUSD < 3001) return false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.ownerSource && filters.ownerSource !== "all") {
|
||||||
|
if (filters.ownerSource === "owner" && property.ownerSource !== "owner") return false;
|
||||||
|
if (filters.ownerSource === "agency" && property.ownerSource !== "agency") return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.rentPeriod && filters.rentPeriod !== "all" && property.listingType === "rent") {
|
||||||
|
if (filters.rentPeriod === "daily" && !property.priceDisplay.daily) return false;
|
||||||
|
if (filters.rentPeriod === "monthly" && !property.priceDisplay.monthly) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.availableToday) {
|
||||||
|
if (property.status !== "available") return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.identityType && property.allowedIdentities) {
|
||||||
|
if (!property.allowedIdentities.includes(filters.identityType)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
setFilteredProperties(filtered);
|
||||||
|
};
|
||||||
|
const resetFilters = () => {
|
||||||
|
setSearchFilters(null);
|
||||||
|
setFilteredProperties([]);
|
||||||
|
};
|
||||||
|
return { filteredProperties, searchFilters, applyFilters, resetFilters };
|
||||||
|
}
|
||||||
18
app/hooks/useAuth.js
Normal file
18
app/hooks/useAuth.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// app/hooks/useAuth.js
|
||||||
|
import AuthService from "../services/AuthService";
|
||||||
|
|
||||||
|
export default function useAuth() {
|
||||||
|
const authUser = AuthService.getUser();
|
||||||
|
const role = authUser?.roles;
|
||||||
|
return {
|
||||||
|
user: authUser,
|
||||||
|
name: authUser?.name ?? "Not found",
|
||||||
|
email: authUser?.email ?? "Not found",
|
||||||
|
isOwner: role?.includes("Owner"),
|
||||||
|
isAgent: role?.includes("RealEstateAgent",""),
|
||||||
|
isCustomer: role?.includes("Customer"),
|
||||||
|
role: role,
|
||||||
|
isGuest: !authUser,
|
||||||
|
isAuthenticated: !!authUser,
|
||||||
|
};
|
||||||
|
}
|
||||||
3439
app/i18n/config.js
3439
app/i18n/config.js
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,9 @@
|
|||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import ClientLayout from "./ClientLayout";
|
import ClientLayout from "./ClientLayout";
|
||||||
|
import { ThemeProvider } from "./contexts/ThemeContext";
|
||||||
|
import { NotificationsProvider } from "./contexts/NotificationsContext";
|
||||||
|
import { FavoritesProvider } from "./contexts/FavoritesContext";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@ -24,7 +27,7 @@ export const metadata = {
|
|||||||
|
|
||||||
export default function Layout({ children }) {
|
export default function Layout({ children }) {
|
||||||
return (
|
return (
|
||||||
<html lang="ar" dir="rtl">
|
<html lang="ar" dir="rtl" className="overflow-x-hidden">
|
||||||
<head>
|
<head>
|
||||||
<link
|
<link
|
||||||
rel="preload"
|
rel="preload"
|
||||||
@ -52,7 +55,13 @@ export default function Layout({ children }) {
|
|||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
style={{ fontFamily: "'Madani Arabic', 'Noto Sans Arabic', 'Cairo', Arial, sans-serif" }}
|
style={{ fontFamily: "'Madani Arabic', 'Noto Sans Arabic', 'Cairo', Arial, sans-serif" }}
|
||||||
>
|
>
|
||||||
<ClientLayout>{children}</ClientLayout>
|
<ThemeProvider>
|
||||||
|
<NotificationsProvider>
|
||||||
|
<FavoritesProvider>
|
||||||
|
<ClientLayout>{children}</ClientLayout>
|
||||||
|
</FavoritesProvider>
|
||||||
|
</NotificationsProvider>
|
||||||
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,17 +1,37 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from "framer-motion";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center p-4" style={{ backgroundColor: "#0f172a" }}>
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1.3 }} transition={{ duration: 0.4 }} className="text-center flex flex-col items-center justify-center gap-4">
|
||||||
initial={{ opacity: 0 }}
|
<motion.div
|
||||||
animate={{ opacity: 1 }}
|
animate={{
|
||||||
className="text-center"
|
scale: [1, 1.06, 1],
|
||||||
>
|
opacity: [0.8, 1, 0.8],
|
||||||
<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-500 text-lg">جاري التحميل...</p>
|
transition={{
|
||||||
|
duration: 2,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "easeInOut",
|
||||||
|
}}
|
||||||
|
className="relative w-25 h-36 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 100 100" className="w-full h-full fill-amber-500/10 stroke-amber-500 stroke-[4] drop-shadow-[0_0_12px_rgba(245,158,11,0.2)] " xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<polygon points="50,5 95,27.5 95,72.5 50,95 5,72.5 5,27.5" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center p-2 text-center">
|
||||||
|
<span className="text-amber-400 font-bold text-[10px] tracking-wide select-none">Sweet Home</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<p className="text-amber-400 text-sm font-bold tracking-wide animate-pulse" suppressHydrationWarning>
|
||||||
|
{t("loading")}
|
||||||
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import Link from 'next/link';
|
|||||||
|
|
||||||
export default function Error({ error, reset }) {
|
export default function Error({ error, reset }) {
|
||||||
return (
|
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">
|
<div className="min-h-screen bg-gray-900 flex items-center justify-center p-4">
|
||||||
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="text-center max-w-md">
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="text-center max-w-md">
|
||||||
<div className="w-20 h-20 bg-red-500/20 rounded-full flex items-center justify-center mx-auto mb-6">
|
<div className="w-20 h-20 bg-red-500/20 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
<AlertTriangle className="w-10 h-10 text-red-500" />
|
<AlertTriangle className="w-10 h-10 text-red-500" />
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
|
|
||||||
export default function Loading() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center">
|
|
||||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-center">
|
|
||||||
<div className="w-14 h-14 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
|
||||||
<p className="text-gray-400 text-lg">جاري التحميل...</p>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,735 +1,8 @@
|
|||||||
"use client";
|
import Login from "../components/LoginComponent/Login";
|
||||||
|
export const metadata = {
|
||||||
import { useState } from "react";
|
title: "صفحة تسجيل الدخول ",
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
describtion: "صفحة تسجيل الدخول في Sweet Home",
|
||||||
import toast, { Toaster } from "react-hot-toast";
|
};
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import {
|
|
||||||
Mail,
|
|
||||||
Lock,
|
|
||||||
Eye,
|
|
||||||
EyeOff,
|
|
||||||
ArrowLeft,
|
|
||||||
LogIn,
|
|
||||||
CheckCircle,
|
|
||||||
Loader2,
|
|
||||||
Home,
|
|
||||||
Shield,
|
|
||||||
Phone,
|
|
||||||
KeyRound,
|
|
||||||
} from "lucide-react";
|
|
||||||
import {
|
|
||||||
loginWithEmail,
|
|
||||||
loginWithPhone,
|
|
||||||
sendEmailOTP,
|
|
||||||
sendPhoneOTP,
|
|
||||||
verifyEmail,
|
|
||||||
verifyPhone,
|
|
||||||
isEmail,
|
|
||||||
isPhoneNumber,
|
|
||||||
getOwnerByUserId,
|
|
||||||
getCustomerByUserId,
|
|
||||||
} from "../utils/api";
|
|
||||||
import AuthService from "../services/AuthService";
|
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const router = useRouter();
|
return <Login />;
|
||||||
|
|
||||||
// Step: 'login' | 'otp'
|
|
||||||
const [step, setStep] = useState("login");
|
|
||||||
const [loginMethod, setLoginMethod] = useState("email"); // 'email' | 'phone'
|
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [isSuccess, setIsSuccess] = useState(false);
|
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
credential: "",
|
|
||||||
password: "",
|
|
||||||
rememberMe: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [otpCode, setOtpCode] = useState("");
|
|
||||||
const [otpError, setOtpError] = useState("");
|
|
||||||
const [errors, setErrors] = useState({});
|
|
||||||
|
|
||||||
const validateForm = () => {
|
|
||||||
const newErrors = {};
|
|
||||||
|
|
||||||
if (!formData.credential) {
|
|
||||||
newErrors.credential =
|
|
||||||
loginMethod === "email"
|
|
||||||
? "البريد الإلكتروني مطلوب"
|
|
||||||
: "رقم الهاتف مطلوب";
|
|
||||||
// } else if (loginMethod === 'email' && !isEmail(formData.credential)) {
|
|
||||||
// newErrors.credential = 'البريد الإلكتروني غير صالح';
|
|
||||||
// } else if (loginMethod === 'phone' && !isPhoneNumber(formData.credential)) {
|
|
||||||
newErrors.credential = "رقم الهاتف غير صالح";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.password) {
|
|
||||||
newErrors.password = "كلمة المرور مطلوبة";
|
|
||||||
}
|
|
||||||
|
|
||||||
setErrors(newErrors);
|
|
||||||
return Object.keys(newErrors).length === 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLogin = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!validateForm()) return;
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
setErrors({});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const loginFn = loginMethod === "email" ? loginWithEmail : loginWithPhone;
|
|
||||||
console.log(
|
|
||||||
"[Login] Attempting login via",
|
|
||||||
loginMethod,
|
|
||||||
":",
|
|
||||||
formData.credential,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await loginFn(formData.credential, formData.password);
|
|
||||||
|
|
||||||
console.log("[Login] Response status:", result.status);
|
|
||||||
|
|
||||||
if (result.status === 200) {
|
|
||||||
const token =
|
|
||||||
typeof result.data === "string"
|
|
||||||
? result.data
|
|
||||||
: result.data?.token || result.data?.accessToken;
|
|
||||||
AuthService.addToken(token);
|
|
||||||
console.log("[Login] Token stored");
|
|
||||||
|
|
||||||
// Fetch user profile to get full name
|
|
||||||
const authUser = AuthService.getUser();
|
|
||||||
if (authUser?.id) {
|
|
||||||
try {
|
|
||||||
const isOwner = AuthService.isOwner();
|
|
||||||
const fetchFn = isOwner ? getOwnerByUserId : getCustomerByUserId;
|
|
||||||
const profile = await fetchFn(authUser.id);
|
|
||||||
if (profile) {
|
|
||||||
AuthService.cacheUser({
|
|
||||||
name:
|
|
||||||
profile.fullName ||
|
|
||||||
profile.name ||
|
|
||||||
`${profile.firstName || ""} ${profile.lastName || ""}`.trim(),
|
|
||||||
email: profile.email || authUser.email,
|
|
||||||
phone: profile.phone || profile.phoneNumber || authUser.phone,
|
|
||||||
});
|
|
||||||
console.log("[Login] User profile cached");
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.warn("[Login] Failed to fetch profile:", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const userRole = AuthService.isOwner() ? "owner" : "customer";
|
|
||||||
console.log("[Login] User role:", userRole);
|
|
||||||
|
|
||||||
setIsSuccess(true);
|
|
||||||
toast.success("تم تسجيل الدخول بنجاح!", {
|
|
||||||
style: { background: "#dcfce7", color: "#166534" },
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
router.push("/");
|
|
||||||
}, 1500);
|
|
||||||
} else if (result.status === 206) {
|
|
||||||
console.log("[Login] 206 — OTP required");
|
|
||||||
const tempToken =
|
|
||||||
typeof result.data === "string"
|
|
||||||
? result.data
|
|
||||||
: result.data?.token || result.data?.accessToken;
|
|
||||||
if (tempToken) {
|
|
||||||
AuthService.addToken(tempToken);
|
|
||||||
console.log("[Login] Temp token stored for OTP");
|
|
||||||
}
|
|
||||||
toast("يرجى إدخال رمز التحقق", {
|
|
||||||
icon: "🔐",
|
|
||||||
style: { background: "#fef3c7", color: "#92400e" },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send OTP
|
|
||||||
try {
|
|
||||||
if (loginMethod === "email") {
|
|
||||||
await sendEmailOTP();
|
|
||||||
} else {
|
|
||||||
await sendPhoneOTP();
|
|
||||||
}
|
|
||||||
console.log("[Login] OTP sent successfully");
|
|
||||||
} catch (otpErr) {
|
|
||||||
console.warn("[Login] OTP send failed, proceeding anyway:", otpErr);
|
|
||||||
}
|
|
||||||
|
|
||||||
setStep("otp");
|
|
||||||
} else {
|
|
||||||
// Other error
|
|
||||||
console.error("[Login] Unexpected status:", result.status, result.data);
|
|
||||||
toast.error(
|
|
||||||
result.data?.message || result.data || "بيانات الدخول غير صحيحة",
|
|
||||||
{
|
|
||||||
style: { background: "#fee2e2", color: "#991b1b" },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("[Login] Error:", err);
|
|
||||||
toast.error(err.message || "حدث خطأ في الاتصال", {
|
|
||||||
style: { background: "#fee2e2", color: "#991b1b" },
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleVerifyOTP = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!otpCode || otpCode.length < 4) {
|
|
||||||
setOtpError("يرجى إدخال رمز التحقق");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
setOtpError("");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const verifyFn = loginMethod === "email" ? verifyEmail : verifyPhone;
|
|
||||||
console.log("[OTP] Verifying code:", otpCode);
|
|
||||||
|
|
||||||
const result = await verifyFn(otpCode);
|
|
||||||
console.log("[OTP] Verify response status:", result.status);
|
|
||||||
|
|
||||||
if (result.ok) {
|
|
||||||
const finalToken =
|
|
||||||
typeof result.data === "string"
|
|
||||||
? result.data
|
|
||||||
: result.data?.token || result.data?.accessToken;
|
|
||||||
if (finalToken && typeof finalToken === "string") {
|
|
||||||
AuthService.addToken(finalToken);
|
|
||||||
console.log("[OTP] Final token stored");
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSuccess(true);
|
|
||||||
toast.success("تم التحقق بنجاح!", {
|
|
||||||
style: { background: "#dcfce7", color: "#166534" },
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log("[OTP] Redirecting to home");
|
|
||||||
router.push("/");
|
|
||||||
}, 1500);
|
|
||||||
} else {
|
|
||||||
console.error("[OTP] Verification failed:", result.data);
|
|
||||||
setOtpError(result.data?.message || "رمز التحقق غير صحيح");
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("[OTP] Error:", err);
|
|
||||||
setOtpError(err.message || "حدث خطأ في التحقق");
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resendOTP = async () => {
|
|
||||||
try {
|
|
||||||
console.log("[OTP] Resending OTP via", loginMethod);
|
|
||||||
if (loginMethod === "email") {
|
|
||||||
await sendEmailOTP();
|
|
||||||
} else {
|
|
||||||
await sendPhoneOTP();
|
|
||||||
}
|
|
||||||
toast.success("تم إرسال رمز التحقق مجدداً", {
|
|
||||||
style: { background: "#dcfce7", color: "#166534" },
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error("[OTP] Resend failed:", err);
|
|
||||||
toast.error("فشل إرسال الرمز");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Auto-detect login method from input
|
|
||||||
const handleCredentialChange = (value) => {
|
|
||||||
setFormData({ ...formData, credential: value });
|
|
||||||
if (errors.credential) setErrors({ ...errors, credential: null });
|
|
||||||
|
|
||||||
// Auto-switch method
|
|
||||||
// if (isEmail(value)) {
|
|
||||||
// setLoginMethod('email');
|
|
||||||
// } else if (isPhoneNumber(value)) {
|
|
||||||
// setLoginMethod('phone');
|
|
||||||
// }
|
|
||||||
};
|
|
||||||
|
|
||||||
const particles = Array.from({ length: 20 }, (_, i) => ({
|
|
||||||
id: i,
|
|
||||||
x: Math.random() * 100,
|
|
||||||
y: Math.random() * 100,
|
|
||||||
size: Math.random() * 3 + 1,
|
|
||||||
duration: Math.random() * 15 + 10,
|
|
||||||
delay: Math.random() * 5,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const containerVariants = {
|
|
||||||
hidden: { opacity: 0 },
|
|
||||||
visible: {
|
|
||||||
opacity: 1,
|
|
||||||
transition: { staggerChildren: 0.1, delayChildren: 0.2 },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const itemVariants = {
|
|
||||||
hidden: { y: 20, opacity: 0 },
|
|
||||||
visible: {
|
|
||||||
y: 0,
|
|
||||||
opacity: 1,
|
|
||||||
transition: { type: "spring", stiffness: 100 },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
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">
|
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
|
||||||
|
|
||||||
{/* Particles */}
|
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
|
||||||
{particles.map((p) => (
|
|
||||||
<motion.div
|
|
||||||
key={p.id}
|
|
||||||
className="absolute rounded-full bg-amber-500/20"
|
|
||||||
style={{
|
|
||||||
left: `${p.x}%`,
|
|
||||||
top: `${p.y}%`,
|
|
||||||
width: p.size,
|
|
||||||
height: p.size,
|
|
||||||
}}
|
|
||||||
animate={{
|
|
||||||
y: [0, -20, 0],
|
|
||||||
x: [0, 10, -10, 0],
|
|
||||||
opacity: [0.2, 0.4, 0.2],
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
duration: p.duration,
|
|
||||||
repeat: Infinity,
|
|
||||||
delay: p.delay,
|
|
||||||
ease: "linear",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Glow orbs */}
|
|
||||||
<motion.div
|
|
||||||
className="absolute top-20 left-20 w-64 h-64 bg-amber-500/10 rounded-full blur-3xl"
|
|
||||||
animate={{ scale: [1, 1.2, 1], x: [0, 30, 0], y: [0, -20, 0] }}
|
|
||||||
transition={{ duration: 12, repeat: Infinity }}
|
|
||||||
/>
|
|
||||||
<motion.div
|
|
||||||
className="absolute bottom-20 right-20 w-80 h-80 bg-blue-500/10 rounded-full blur-3xl"
|
|
||||||
animate={{ scale: [1, 1.3, 1], x: [0, -30, 0], y: [0, 20, 0] }}
|
|
||||||
transition={{ duration: 15, repeat: Infinity }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
variants={containerVariants}
|
|
||||||
initial="hidden"
|
|
||||||
animate="visible"
|
|
||||||
className="relative w-full max-w-md z-10"
|
|
||||||
>
|
|
||||||
{/* Back link */}
|
|
||||||
<motion.div variants={itemVariants} className="absolute -top-16 left-0">
|
|
||||||
<Link
|
|
||||||
href="/"
|
|
||||||
className="group flex items-center gap-2 text-gray-400 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
<motion.div
|
|
||||||
whileHover={{ x: -5 }}
|
|
||||||
transition={{ type: "spring", stiffness: 400 }}
|
|
||||||
>
|
|
||||||
<ArrowLeft className="w-4 h-4" />
|
|
||||||
</motion.div>
|
|
||||||
<span>العودة للرئيسية</span>
|
|
||||||
</Link>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
variants={itemVariants}
|
|
||||||
className="bg-white/10 backdrop-blur-2xl rounded-3xl shadow-2xl border border-white/10 overflow-hidden"
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<div className="bg-gradient-to-r from-amber-500 to-amber-600 p-8 text-center relative overflow-hidden">
|
|
||||||
<motion.div
|
|
||||||
initial={{ scale: 0 }}
|
|
||||||
animate={{ scale: 1 }}
|
|
||||||
transition={{ delay: 0.2, type: "spring" }}
|
|
||||||
className="absolute -top-10 -right-10 w-40 h-40 bg-white/10 rounded-full"
|
|
||||||
/>
|
|
||||||
<motion.div
|
|
||||||
initial={{ scale: 0 }}
|
|
||||||
animate={{ scale: 1 }}
|
|
||||||
transition={{ delay: 0.3, type: "spring" }}
|
|
||||||
className="absolute -bottom-10 -left-10 w-40 h-40 bg-white/10 rounded-full"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ y: 20, opacity: 0 }}
|
|
||||||
animate={{ y: 0, opacity: 1 }}
|
|
||||||
transition={{ delay: 0.2 }}
|
|
||||||
className="relative z-10"
|
|
||||||
>
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
{step === "otp" ? (
|
|
||||||
<KeyRound className="w-10 h-10 text-white" />
|
|
||||||
) : (
|
|
||||||
<Home className="w-10 h-10 text-white" />
|
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
<h1 className="text-3xl font-bold text-white mb-2">SweetHome</h1>
|
|
||||||
<p className="text-amber-100">
|
|
||||||
{step === "otp" ? "أدخل رمز التحقق" : "مرحباً بعودتك!"}
|
|
||||||
</p>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-8">
|
|
||||||
<AnimatePresence mode="wait">
|
|
||||||
{step === "login" ? (
|
|
||||||
<motion.form
|
|
||||||
key="login"
|
|
||||||
initial={{ opacity: 0, x: -20 }}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
exit={{ opacity: 0, x: 20 }}
|
|
||||||
onSubmit={handleLogin}
|
|
||||||
className="space-y-6"
|
|
||||||
>
|
|
||||||
{/* Login method tabs */}
|
|
||||||
<div className="flex gap-2 bg-white/5 p-1 rounded-xl">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setLoginMethod("email");
|
|
||||||
setFormData({ ...formData, credential: "" });
|
|
||||||
}}
|
|
||||||
className={`flex-1 py-2.5 rounded-lg text-sm font-medium transition-all flex items-center justify-center gap-2 ${
|
|
||||||
loginMethod === "email"
|
|
||||||
? "bg-amber-500 text-white shadow-lg"
|
|
||||||
: "text-gray-400 hover:text-white"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Mail className="w-4 h-4" />
|
|
||||||
بريد إلكتروني
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setLoginMethod("phone");
|
|
||||||
setFormData({ ...formData, credential: "" });
|
|
||||||
}}
|
|
||||||
className={`flex-1 py-2.5 rounded-lg text-sm font-medium transition-all flex items-center justify-center gap-2 ${
|
|
||||||
loginMethod === "phone"
|
|
||||||
? "bg-amber-500 text-white shadow-lg"
|
|
||||||
: "text-gray-400 hover:text-white"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Phone className="w-4 h-4" />
|
|
||||||
رقم الهاتف
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Credential input */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
||||||
{loginMethod === "email"
|
|
||||||
? "البريد الإلكتروني"
|
|
||||||
: "رقم الهاتف"}
|
|
||||||
</label>
|
|
||||||
<div className="relative group">
|
|
||||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
|
||||||
{loginMethod === "email" ? (
|
|
||||||
<Mail
|
|
||||||
className={`w-5 h-5 transition-colors ${errors.credential ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Phone
|
|
||||||
className={`w-5 h-5 transition-colors ${errors.credential ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
// type={loginMethod === 'email' ? 'email' : 'tel'}
|
|
||||||
value={formData.credential}
|
|
||||||
onChange={(e) => handleCredentialChange(e.target.value)}
|
|
||||||
className={`w-full pr-12 pl-4 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.credential
|
|
||||||
? "border-red-500"
|
|
||||||
: "border-gray-700"
|
|
||||||
}`}
|
|
||||||
placeholder={
|
|
||||||
loginMethod === "email"
|
|
||||||
? "example@email.com"
|
|
||||||
: "+963XXXXXXXXX"
|
|
||||||
}
|
|
||||||
dir="ltr"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{errors.credential && (
|
|
||||||
<motion.p
|
|
||||||
initial={{ opacity: 0, y: -10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="text-red-500 text-sm mt-1"
|
|
||||||
>
|
|
||||||
{errors.credential}
|
|
||||||
</motion.p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Password */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
||||||
كلمة المرور
|
|
||||||
</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 transition-colors ${errors.password ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type={showPassword ? "text" : "password"}
|
|
||||||
value={formData.password}
|
|
||||||
onChange={(e) => {
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
password: e.target.value,
|
|
||||||
});
|
|
||||||
if (errors.password)
|
|
||||||
setErrors({ ...errors, password: null });
|
|
||||||
}}
|
|
||||||
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="أدخل كلمة المرور"
|
|
||||||
/>
|
|
||||||
<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 hover:text-gray-300 transition-colors" />
|
|
||||||
) : (
|
|
||||||
<Eye className="w-5 h-5 text-gray-400 hover:text-gray-300 transition-colors" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{errors.password && (
|
|
||||||
<motion.p
|
|
||||||
initial={{ opacity: 0, y: -10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="text-red-500 text-sm mt-1"
|
|
||||||
>
|
|
||||||
{errors.password}
|
|
||||||
</motion.p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Remember + Forgot */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<label className="flex items-center gap-2 cursor-pointer group">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={formData.rememberMe}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
rememberMe: e.target.checked,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
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">
|
|
||||||
تذكرني
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<Link
|
|
||||||
href="/forgot-password"
|
|
||||||
className="text-sm text-amber-400 hover:text-amber-300 transition-colors"
|
|
||||||
>
|
|
||||||
نسيت كلمة المرور؟
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Submit */}
|
|
||||||
<motion.button
|
|
||||||
type="submit"
|
|
||||||
disabled={isLoading || isSuccess}
|
|
||||||
className="relative w-full bg-gradient-to-r from-amber-500 to-amber-600 text-white py-4 rounded-xl font-bold text-lg overflow-hidden group disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
whileHover={{ scale: 1.02 }}
|
|
||||||
whileTap={{ scale: 0.98 }}
|
|
||||||
>
|
|
||||||
<span className="relative z-10 flex items-center justify-center gap-2">
|
|
||||||
{isLoading ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="w-5 h-5 animate-spin" />
|
|
||||||
جاري تسجيل الدخول...
|
|
||||||
</>
|
|
||||||
) : isSuccess ? (
|
|
||||||
<>
|
|
||||||
<CheckCircle className="w-5 h-5" />
|
|
||||||
تم بنجاح!
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<LogIn className="w-5 h-5" />
|
|
||||||
تسجيل الدخول
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</motion.button>
|
|
||||||
</motion.form>
|
|
||||||
) : (
|
|
||||||
/* OTP Verification Step */
|
|
||||||
<motion.form
|
|
||||||
key="otp"
|
|
||||||
initial={{ opacity: 0, x: 20 }}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
exit={{ opacity: 0, x: -20 }}
|
|
||||||
onSubmit={handleVerifyOTP}
|
|
||||||
className="space-y-6"
|
|
||||||
>
|
|
||||||
<div className="text-center mb-4">
|
|
||||||
<Shield className="w-12 h-12 text-amber-500 mx-auto mb-3" />
|
|
||||||
<p className="text-gray-300 text-sm">
|
|
||||||
تم إرسال رمز التحقق إلى{" "}
|
|
||||||
<span className="text-white font-medium" dir="ltr">
|
|
||||||
{formData.credential}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
||||||
رمز التحقق
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={otpCode}
|
|
||||||
onChange={(e) => {
|
|
||||||
setOtpCode(e.target.value);
|
|
||||||
if (otpError) setOtpError("");
|
|
||||||
}}
|
|
||||||
className={`w-full px-4 py-4 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white text-center text-2xl tracking-[0.5em] placeholder-gray-500 transition-all ${
|
|
||||||
otpError ? "border-red-500" : "border-gray-700"
|
|
||||||
}`}
|
|
||||||
placeholder="______"
|
|
||||||
maxLength={6}
|
|
||||||
dir="ltr"
|
|
||||||
/>
|
|
||||||
{otpError && (
|
|
||||||
<motion.p
|
|
||||||
initial={{ opacity: 0, y: -10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="text-red-500 text-sm mt-1 text-center"
|
|
||||||
>
|
|
||||||
{otpError}
|
|
||||||
</motion.p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<motion.button
|
|
||||||
type="submit"
|
|
||||||
disabled={isLoading || isSuccess}
|
|
||||||
className="relative w-full bg-gradient-to-r from-amber-500 to-amber-600 text-white py-4 rounded-xl font-bold text-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
whileHover={{ scale: 1.02 }}
|
|
||||||
whileTap={{ scale: 0.98 }}
|
|
||||||
>
|
|
||||||
<span className="flex items-center justify-center gap-2">
|
|
||||||
{isLoading ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="w-5 h-5 animate-spin" />
|
|
||||||
جاري التحقق...
|
|
||||||
</>
|
|
||||||
) : isSuccess ? (
|
|
||||||
<>
|
|
||||||
<CheckCircle className="w-5 h-5" />
|
|
||||||
تم بنجاح!
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<KeyRound className="w-5 h-5" />
|
|
||||||
تحقق
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</motion.button>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between text-sm">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setStep("login");
|
|
||||||
setOtpCode("");
|
|
||||||
setOtpError("");
|
|
||||||
console.log("[OTP] Going back to login");
|
|
||||||
}}
|
|
||||||
className="text-gray-400 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
← العودة
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={resendOTP}
|
|
||||||
className="text-amber-400 hover:text-amber-300 transition-colors"
|
|
||||||
>
|
|
||||||
إعادة إرسال الرمز
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</motion.form>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
<motion.p
|
|
||||||
variants={itemVariants}
|
|
||||||
className="text-center text-gray-400 mt-6"
|
|
||||||
>
|
|
||||||
ليس لديك حساب؟{" "}
|
|
||||||
<Link
|
|
||||||
href="/auth/choose-role"
|
|
||||||
className="text-amber-400 hover:text-amber-300 font-medium transition-colors"
|
|
||||||
>
|
|
||||||
إنشاء حساب جديد
|
|
||||||
</Link>
|
|
||||||
</motion.p>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.p
|
|
||||||
variants={itemVariants}
|
|
||||||
className="text-center text-gray-500 text-xs mt-4"
|
|
||||||
>
|
|
||||||
بتسجيل الدخول، أنت توافق على{" "}
|
|
||||||
<Link
|
|
||||||
href="/terms"
|
|
||||||
className="text-amber-400 hover:text-amber-300 transition-colors"
|
|
||||||
>
|
|
||||||
شروط الاستخدام
|
|
||||||
</Link>{" "}
|
|
||||||
و{" "}
|
|
||||||
<Link
|
|
||||||
href="/privacy"
|
|
||||||
className="text-amber-400 hover:text-amber-300 transition-colors"
|
|
||||||
>
|
|
||||||
سياسة الخصوصية
|
|
||||||
</Link>
|
|
||||||
</motion.p>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,19 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { Star, User, Calendar, MessageSquare, ThumbsUp, Loader2 } from 'lucide-react';
|
import { Star, User, Calendar, MessageSquare, ThumbsUp, Loader2 } from 'lucide-react';
|
||||||
import toast, { Toaster } from 'react-hot-toast';
|
import toast, { Toaster } from 'react-hot-toast';
|
||||||
import { getCustomerRatings } from '../utils/ratings';
|
import { getCustomerRatings } from '../utils/ratings';
|
||||||
import AuthService from '../services/AuthService';
|
import AuthService from '../services/AuthService';
|
||||||
import StarRating from '../components/ratings/StarRating';
|
import StarRating from '../components/ratings/StarRating';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
const RATING_FIELDS = [
|
const RATING_FIELDS = [
|
||||||
{ key: 'cleanRating', label: 'النظافة' },
|
{ key: 'cleanRating', label: 'ratings.cleanliness' },
|
||||||
{ key: 'servicesRating', label: 'الخدمات' },
|
{ key: 'servicesRating', label: 'ratings.services' },
|
||||||
{ key: 'ownerBehaviorRating', label: 'سلوك المالك' },
|
{ key: 'ownerBehaviorRating', label: 'ratings.ownerBehavior' },
|
||||||
{ key: 'experienceRating', label: 'التجربة العامة' },
|
{ key: 'experienceRating', label: 'ratings.experience' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function calcOverall(ratings) {
|
function calcOverall(ratings) {
|
||||||
@ -24,7 +25,7 @@ function calcOverall(ratings) {
|
|||||||
return total / ratings.length;
|
return total / ratings.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
function RatingCard({ rating }) {
|
function RatingCard({ rating, t }) {
|
||||||
const dateStr = rating.createdAt
|
const dateStr = rating.createdAt
|
||||||
? new Date(rating.createdAt).toLocaleDateString('ar')
|
? new Date(rating.createdAt).toLocaleDateString('ar')
|
||||||
: null;
|
: null;
|
||||||
@ -49,7 +50,7 @@ function RatingCard({ rating }) {
|
|||||||
<div className="space-y-3 mb-4">
|
<div className="space-y-3 mb-4">
|
||||||
{RATING_FIELDS.map(({ key, label }) => (
|
{RATING_FIELDS.map(({ key, label }) => (
|
||||||
<div key={key} className="flex items-center justify-between">
|
<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">
|
<div className="flex items-center gap-2">
|
||||||
<StarRating rating={Number(rating[key]) || 0} size={18} readOnly />
|
<StarRating rating={Number(rating[key]) || 0} size={18} readOnly />
|
||||||
<span className="text-sm font-medium text-gray-700 w-6 text-left">
|
<span className="text-sm font-medium text-gray-700 w-6 text-left">
|
||||||
@ -71,18 +72,19 @@ function RatingCard({ rating }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function MyRatesPage() {
|
export default function MyRatesPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [ratings, setRatings] = useState([]);
|
const [ratings, setRatings] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const user = AuthService.getUser();
|
const user = AuthService.getUser();
|
||||||
if (!user || !user.id) {
|
if (!user || !user.id) {
|
||||||
toast.error('يرجى تسجيل الدخول أولاً');
|
toast.error(t('auth.loginRequired'));
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
loadRatings(user.id);
|
loadRatings(user.id);
|
||||||
}, []);
|
}, [t]);
|
||||||
|
|
||||||
async function loadRatings(userId) {
|
async function loadRatings(userId) {
|
||||||
try {
|
try {
|
||||||
@ -91,20 +93,14 @@ export default function MyRatesPage() {
|
|||||||
setRatings(list);
|
setRatings(list);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
toast.error('فشل تحميل التقييمات');
|
toast.error(t('ratings.loadError'));
|
||||||
setRatings([]);
|
setRatings([]);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center" dir="rtl">
|
|
||||||
<Loader2 className="w-12 h-12 text-amber-500 animate-spin" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const overall = calcOverall(ratings);
|
const overall = calcOverall(ratings);
|
||||||
|
|
||||||
@ -113,8 +109,8 @@ export default function MyRatesPage() {
|
|||||||
<Toaster position="top-center" reverseOrder={false} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
<div className="container mx-auto px-4">
|
<div className="container mx-auto px-4">
|
||||||
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="mb-8">
|
<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>
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t('ratings.myRatings')}</h1>
|
||||||
<p className="text-gray-600">التقييمات التي تلقيتها من مالكي العقارات</p>
|
<p className="text-gray-600">{t('ratings.receivedFromOwners')}</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{ratings.length > 0 && (
|
{ratings.length > 0 && (
|
||||||
@ -125,9 +121,9 @@ export default function MyRatesPage() {
|
|||||||
<StarRating rating={Math.round(overall)} size={24} readOnly />
|
<StarRating rating={Math.round(overall)} size={24} readOnly />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-4xl font-bold">{overall.toFixed(1)}</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">
|
<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>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
@ -136,13 +132,13 @@ export default function MyRatesPage() {
|
|||||||
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 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">
|
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" />
|
<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>
|
<h3 className="text-xl font-bold text-gray-900 mb-2">{t('ratings.noRatings')}</h3>
|
||||||
<p className="text-gray-500">لم تقم باستئجار أي عقار بعد لتتلقى تقييمات</p>
|
<p className="text-gray-500">{t('ratings.noRatingsDescription')}</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
{ratings.map((r, i) => (
|
{ratings.map((r, i) => (
|
||||||
<RatingCard key={r.id || i} rating={r} />
|
<RatingCard key={r.id || i} rating={r} t={t} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,13 +1,16 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from 'next/navigation';
|
import { useTranslation } from "react-i18next";
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { useRouter } from "next/navigation";
|
||||||
import { Bell, CheckCircle, XCircle, Calendar, MessageCircle, CheckCheck, Loader2 } from 'lucide-react';
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import AuthService from '@/app/services/AuthService';
|
import { Bell, CheckCircle, XCircle, Calendar, MessageCircle, CheckCheck, Loader2 } from "lucide-react";
|
||||||
import { getUserNotifications } from '@/app/utils/api';
|
import AuthService from "@/app/services/AuthService";
|
||||||
|
import { getUserNotifications } from "@/app/utils/api";
|
||||||
|
import Loading from "../loading";
|
||||||
|
|
||||||
export default function NotificationsPage() {
|
export default function NotificationsPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [notifications, setNotifications] = useState([]);
|
const [notifications, setNotifications] = useState([]);
|
||||||
const [unreadCount, setUnreadCount] = useState(0);
|
const [unreadCount, setUnreadCount] = useState(0);
|
||||||
@ -16,7 +19,7 @@ export default function NotificationsPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!AuthService.isAuthenticated()) {
|
if (!AuthService.isAuthenticated()) {
|
||||||
router.push('/login');
|
router.push("/login");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fetchNotifications();
|
fetchNotifications();
|
||||||
@ -31,36 +34,27 @@ export default function NotificationsPage() {
|
|||||||
setNotifications(items);
|
setNotifications(items);
|
||||||
setUnreadCount(items.length);
|
setUnreadCount(items.length);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching notifications:', err);
|
console.error("Error fetching notifications:", err);
|
||||||
setError(err.message || 'فشل تحميل الإشعارات');
|
setError(err.message || t("fetch-notifications-failed"));
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const markAsRead = (id) => {
|
const markAsRead = (id) => {
|
||||||
setNotifications(prev =>
|
setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, read: true } : n)));
|
||||||
prev.map(n => (n.id === id ? { ...n, read: true } : n))
|
setUnreadCount((prev) => Math.max(0, prev - 1));
|
||||||
);
|
|
||||||
setUnreadCount(prev => Math.max(0, prev - 1));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const markAllAsRead = () => {
|
const markAllAsRead = () => {
|
||||||
setNotifications(prev => prev.map(n => ({ ...n, read: true })));
|
setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
|
||||||
setUnreadCount(0);
|
setUnreadCount(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
<div>
|
||||||
<motion.div
|
<Loading />
|
||||||
initial={{ opacity: 0, scale: 0.9 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
className="text-center"
|
|
||||||
>
|
|
||||||
<Loader2 className="w-12 h-12 text-amber-500 mx-auto mb-4 animate-spin" />
|
|
||||||
<p className="text-gray-600">جاري التحميل...</p>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -68,13 +62,9 @@ export default function NotificationsPage() {
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="text-center">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="text-center"
|
|
||||||
>
|
|
||||||
<XCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
|
<XCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
|
||||||
<h3 className="text-xl font-bold text-gray-700 mb-2">خطأ في التحميل</h3>
|
<h3 className="text-xl font-bold text-gray-700 mb-2">{t("loading-error")}</h3>
|
||||||
<p className="text-gray-500 mb-4">{error}</p>
|
<p className="text-gray-500 mb-4">{error}</p>
|
||||||
<motion.button
|
<motion.button
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
@ -82,7 +72,7 @@ export default function NotificationsPage() {
|
|||||||
onClick={fetchNotifications}
|
onClick={fetchNotifications}
|
||||||
className="px-6 py-2 bg-amber-500 text-white rounded-xl font-medium hover:bg-amber-600 transition-colors"
|
className="px-6 py-2 bg-amber-500 text-white rounded-xl font-medium hover:bg-amber-600 transition-colors"
|
||||||
>
|
>
|
||||||
إعادة المحاولة
|
{t("retry")}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
@ -92,16 +82,10 @@ export default function NotificationsPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 py-8">
|
<div className="min-h-screen bg-gray-50 py-8">
|
||||||
<div className="container mx-auto px-4 max-w-4xl">
|
<div className="container mx-auto px-4 max-w-4xl">
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="flex justify-between items-center mb-8">
|
||||||
initial={{ opacity: 0, y: -20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="flex justify-between items-center mb-8"
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">الإشعارات</h1>
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t("notifications")}</h1>
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600">{unreadCount > 0 ? t("unread-notifications", { count: unreadCount }) : t("all-notifications-read")}</p>
|
||||||
{unreadCount > 0 ? `لديك ${unreadCount} إشعار غير مقروء` : 'جميع الإشعارات مقروءة'}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
{unreadCount > 0 && (
|
{unreadCount > 0 && (
|
||||||
<motion.button
|
<motion.button
|
||||||
@ -111,20 +95,16 @@ export default function NotificationsPage() {
|
|||||||
className="flex items-center gap-2 px-4 py-2 bg-white border border-gray-200 rounded-xl text-sm font-medium text-gray-700 hover:bg-gray-50 transition-all shadow-sm"
|
className="flex items-center gap-2 px-4 py-2 bg-white border border-gray-200 rounded-xl text-sm font-medium text-gray-700 hover:bg-gray-50 transition-all shadow-sm"
|
||||||
>
|
>
|
||||||
<CheckCheck className="w-4 h-4 text-amber-500" />
|
<CheckCheck className="w-4 h-4 text-amber-500" />
|
||||||
تحديد الكل كمقروء
|
{t("mark-all-read")}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
)}
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{notifications.length === 0 ? (
|
{notifications.length === 0 ? (
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300"
|
|
||||||
>
|
|
||||||
<Bell className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
<Bell className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||||
<h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد إشعارات</h3>
|
<h3 className="text-xl font-bold text-gray-700 mb-2">{t("no-notifications")}</h3>
|
||||||
<p className="text-gray-500">ستظهر هنا الإشعارات المتعلقة بحجوزاتك ومدفوعاتك</p>
|
<p className="text-gray-500">{t("notifications-empty-hint")}</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@ -135,32 +115,20 @@ export default function NotificationsPage() {
|
|||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, x: 100 }}
|
exit={{ opacity: 0, x: 100 }}
|
||||||
transition={{ delay: index * 0.05, type: 'spring', stiffness: 100 }}
|
transition={{ delay: index * 0.05, type: "spring", stiffness: 100 }}
|
||||||
whileHover={{ scale: 1.01 }}
|
whileHover={{ scale: 1.01 }}
|
||||||
onClick={() => markAsRead(notification.id)}
|
onClick={() => markAsRead(notification.id)}
|
||||||
className={`bg-white rounded-2xl shadow-sm border transition-all hover:shadow-md cursor-pointer ${
|
className={`bg-white rounded-2xl shadow-sm border transition-all hover:shadow-md cursor-pointer ${!notification.read ? "border-amber-200 bg-amber-50/50" : "border-gray-200"}`}
|
||||||
!notification.read ? 'border-amber-200 bg-amber-50/50' : 'border-gray-200'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className="p-5 flex gap-4">
|
<div className="p-5 flex gap-4">
|
||||||
<div className={`w-12 h-12 rounded-full flex items-center justify-center shrink-0 ${
|
<div className={`w-12 h-12 rounded-full flex items-center justify-center shrink-0 ${!notification.read ? "bg-amber-100" : "bg-gray-100"}`}>
|
||||||
!notification.read ? 'bg-amber-100' : 'bg-gray-100'
|
{!notification.read ? <Bell className="w-6 h-6 text-amber-600" /> : <CheckCircle className="w-6 h-6 text-gray-400" />}
|
||||||
}`}>
|
|
||||||
{!notification.read ? (
|
|
||||||
<Bell className="w-6 h-6 text-amber-600" />
|
|
||||||
) : (
|
|
||||||
<CheckCircle className="w-6 h-6 text-gray-400" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex justify-between items-start gap-4">
|
<div className="flex justify-between items-start gap-4">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<h3 className={`text-base ${!notification.read ? 'font-bold text-gray-900' : 'font-medium text-gray-700'}`}>
|
<h3 className={`text-base ${!notification.read ? "font-bold text-gray-900" : "font-medium text-gray-700"}`}>{notification.title}</h3>
|
||||||
{notification.title}
|
{notification.message && <p className="text-gray-500 text-sm mt-1 line-clamp-2">{notification.message}</p>}
|
||||||
</h3>
|
|
||||||
{notification.message && (
|
|
||||||
<p className="text-gray-500 text-sm mt-1 line-clamp-2">{notification.message}</p>
|
|
||||||
)}
|
|
||||||
<div className="flex items-center gap-2 mt-2">
|
<div className="flex items-center gap-2 mt-2">
|
||||||
{notification.date && (
|
{notification.date && (
|
||||||
<span className="text-xs text-gray-400 flex items-center gap-1">
|
<span className="text-xs text-gray-400 flex items-center gap-1">
|
||||||
@ -176,9 +144,7 @@ export default function NotificationsPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!notification.read && (
|
{!notification.read && <span className="w-2.5 h-2.5 bg-amber-500 rounded-full shrink-0 mt-2" />}
|
||||||
<span className="w-2.5 h-2.5 bg-amber-500 rounded-full shrink-0 mt-2" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -5,33 +5,35 @@ import { motion, AnimatePresence } from 'framer-motion';
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Home, Search, Rocket, ArrowLeft, ArrowRight } from 'lucide-react';
|
import { Home, Search, Rocket, ArrowLeft, ArrowRight } from 'lucide-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const STORAGE_KEY = 'sweethome_onboarding_completed';
|
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() {
|
export default function OnboardingPage() {
|
||||||
|
const { t, i18n } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [currentStep, setCurrentStep] = useState(0);
|
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);
|
const [direction, setDirection] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -74,7 +76,7 @@ export default function OnboardingPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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 flex items-center justify-center p-4 relative overflow-hidden">
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
{[...Array(30)].map((_, i) => (
|
{[...Array(30)].map((_, i) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
@ -142,7 +144,7 @@ export default function OnboardingPage() {
|
|||||||
transition={{ type: 'spring', stiffness: 200, delay: 0.1 }}
|
transition={{ type: 'spring', stiffness: 200, delay: 0.1 }}
|
||||||
className="w-24 h-24 mx-auto bg-white/20 rounded-3xl flex items-center justify-center backdrop-blur-sm"
|
className="w-24 h-24 mx-auto bg-white/20 rounded-3xl flex items-center justify-center backdrop-blur-sm"
|
||||||
>
|
>
|
||||||
<IconComponent className="w-12 h-12 text-white" />
|
<IconComponent className="w-12 h-12 text-amber-50" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -152,7 +154,7 @@ export default function OnboardingPage() {
|
|||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 0.15 }}
|
transition={{ delay: 0.15 }}
|
||||||
className="text-3xl font-bold text-white mb-4"
|
className="text-3xl font-bold text-amber-50 mb-4"
|
||||||
>
|
>
|
||||||
{step.title}
|
{step.title}
|
||||||
</motion.h1>
|
</motion.h1>
|
||||||
@ -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"
|
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" />
|
<ArrowRight className="w-5 h-5" />
|
||||||
<span>السابق</span>
|
<span>{t('onboarding.prev')}</span>
|
||||||
</motion.button>
|
</motion.button>
|
||||||
) : (
|
) : (
|
||||||
<div />
|
<div />
|
||||||
@ -212,7 +214,7 @@ export default function OnboardingPage() {
|
|||||||
onClick={nextStep}
|
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`}
|
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" />
|
<ArrowLeft className="w-5 h-5" />
|
||||||
</motion.button>
|
</motion.button>
|
||||||
) : (
|
) : (
|
||||||
@ -223,7 +225,7 @@ export default function OnboardingPage() {
|
|||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
className="px-6 py-3 rounded-xl bg-white text-gray-900 font-bold hover:bg-gray-100 transition-all"
|
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>
|
</motion.button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/login" onClick={handleFinish}>
|
<Link href="/login" onClick={handleFinish}>
|
||||||
@ -232,7 +234,7 @@ export default function OnboardingPage() {
|
|||||||
whileTap={{ scale: 0.95 }}
|
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"
|
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>
|
</motion.button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,57 +1,50 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from "framer-motion";
|
||||||
import { useRouter } from 'next/navigation';
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import { DollarSign, TrendingUp, Calendar, Loader2, Star } from "lucide-react";
|
||||||
DollarSign,
|
import toast, { Toaster } from "react-hot-toast";
|
||||||
TrendingUp,
|
import AuthService from "@/app/services/AuthService";
|
||||||
Calendar,
|
import { getOwnerStatistics } from "@/app/utils/api";
|
||||||
Users,
|
import Loading from "@/app/loading";
|
||||||
Building,
|
|
||||||
Download,
|
|
||||||
Loader2,
|
|
||||||
ArrowLeft,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import toast, { Toaster } from 'react-hot-toast';
|
|
||||||
import AuthService from '@/app/services/AuthService';
|
|
||||||
import { getOwnerStatistics } from '@/app/utils/api';
|
|
||||||
|
|
||||||
const StatCard = ({ title, value, icon: Icon, color, subtitle }) => (
|
const StatCard = ({ title, value, icon: Icon, color, subtitle, isNA }) => (
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-all">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-all"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div className={`w-12 h-12 ${color} rounded-xl flex items-center justify-center`}>
|
<div className={`w-12 h-12 ${color} rounded-xl flex items-center justify-center`}>
|
||||||
<Icon className="w-6 h-6 text-white" />
|
<Icon className="w-6 h-6 text-white" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-sm text-gray-500 mb-1">{title}</h3>
|
<h3 className="text-sm text-gray-500 mb-1">{title}</h3>
|
||||||
<div className="text-2xl font-bold text-gray-900">{value}</div>
|
<div className="text-2xl font-bold text-gray-900">{isNA ? "N/A" : value}</div>
|
||||||
{subtitle && <p className="text-xs text-gray-400 mt-1">{subtitle}</p>}
|
{subtitle && <p className="text-xs text-gray-400 mt-1">{subtitle}</p>}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default function OwnerAccountBookPage() {
|
export default function OwnerAccountBookPage() {
|
||||||
const router = useRouter();
|
const { t, i18n } = useTranslation();
|
||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [stats, setStats] = useState({
|
const [stats, setStats] = useState({
|
||||||
totalRevenue: 0,
|
totalRevenue: null,
|
||||||
totalReservations: 0,
|
totalReservations: null,
|
||||||
activeProperties: 0,
|
activeProperties: null,
|
||||||
|
financialRevenue: null,
|
||||||
|
financialCommission: null,
|
||||||
|
financialBalance: null,
|
||||||
|
directRevenue: null,
|
||||||
|
directCommission: null,
|
||||||
|
directBalance: null,
|
||||||
|
directRating: null,
|
||||||
|
directPostponements: null,
|
||||||
});
|
});
|
||||||
const [transactions, setTransactions] = useState([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (AuthService.isGuest()) {
|
if (AuthService.isGuest()) {
|
||||||
router.push('/auth/choose-role');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!AuthService.isOwner()) {
|
if (!AuthService.isOwner()) {
|
||||||
router.push('/');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,171 +59,82 @@ export default function OwnerAccountBookPage() {
|
|||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
try {
|
try {
|
||||||
const data = await getOwnerStatistics();
|
const data = await getOwnerStatistics();
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
setStats({
|
setStats({
|
||||||
totalRevenue: data.totalRevenue ?? 0,
|
totalRevenue: data.totalRevenue ?? null,
|
||||||
totalReservations: data.totalReservations ?? 0,
|
totalReservations: data.totalReservations ?? null,
|
||||||
activeProperties: data.activeProperties ?? 0,
|
activeProperties: data.activeProperties ?? null,
|
||||||
|
financialRevenue: data.financialRevenue ?? data.totalRevenue ?? null,
|
||||||
|
financialCommission: data.financialCommission ?? null,
|
||||||
|
financialBalance: data.financialBalance ?? null,
|
||||||
|
directRevenue: data.directRevenue ?? null,
|
||||||
|
directCommission: data.directCommission ?? null,
|
||||||
|
directBalance: data.directBalance ?? null,
|
||||||
|
directRating: data.directRating ?? null,
|
||||||
|
directPostponements: data.directPostponements ?? null,
|
||||||
});
|
});
|
||||||
if (data.transactions) {
|
|
||||||
setTransactions(data.transactions);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('تعذر تحميل إحصائيات الحساب');
|
toast.error(t("accountBook.loadError"));
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [router]);
|
}, []);
|
||||||
|
|
||||||
const formatCurrency = (amount) => {
|
const formatCurrency = (amount) => {
|
||||||
const num = Number(amount) || 0;
|
const num = Number(amount);
|
||||||
return num.toLocaleString() + ' ل.س';
|
if (isNaN(num)) return "N/A";
|
||||||
|
return num.toLocaleString() + " " + t("currency-syp-suffix");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExport = () => {
|
const formatNumber = (val) => {
|
||||||
toast.success('جاري تصدير البيانات...');
|
const num = Number(val);
|
||||||
|
if (isNaN(num)) return "N/A";
|
||||||
|
return num.toLocaleString();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatRating = (val) => {
|
||||||
|
const num = Number(val);
|
||||||
|
if (isNaN(num)) return "N/A";
|
||||||
|
return num.toFixed(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isNA = (val) => val === null || val === undefined || val === "";
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return <Loading />;
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
|
||||||
<div className="text-center">
|
|
||||||
<Loader2 className="w-12 h-12 text-amber-500 animate-spin mx-auto mb-4" />
|
|
||||||
<p className="text-gray-600">جاري تحميل بيانات الحساب...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">
|
<div className="min-h-screen bg-gray-50 py-8" dir={i18n.language === "ar" ? "rtl" : "ltr"}>
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
<div className="container mx-auto px-4 max-w-7xl">
|
<div className="container mx-auto px-4 max-w-7xl">
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="mb-8">
|
||||||
initial={{ opacity: 0, y: -20 }}
|
<h1 className="text-3xl font-bold text-gray-900">{t("accountBook")}</h1>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<p className="text-gray-600">{t("financialRecords")}</p>
|
||||||
className="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4"
|
|
||||||
>
|
|
||||||
<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 mb-2">دفتر الحسابات</h1>
|
|
||||||
<p className="text-gray-600">
|
|
||||||
مرحباً {user?.name}، نظرة عامة على حساباتك
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={handleExport}
|
|
||||||
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" />
|
|
||||||
تصدير
|
|
||||||
</button>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="mb-8">
|
||||||
<StatCard
|
<h2 className="text-xl font-bold text-gray-900 mb-4">{t("accountBook.ownerFinancialOverview")}</h2>
|
||||||
title="إجمالي الإيرادات"
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
|
||||||
value={formatCurrency(stats.totalRevenue)}
|
<StatCard title={t("accountBook.revenue")} value={formatCurrency(stats.financialRevenue)} icon={TrendingUp} color="bg-emerald-500" isNA={isNA(stats.financialRevenue)} />
|
||||||
icon={DollarSign}
|
<StatCard title={t("accountBook.commission")} value={formatCurrency(stats.financialCommission)} icon={DollarSign} color="bg-orange-500" isNA={isNA(stats.financialCommission)} />
|
||||||
color="bg-green-500"
|
<StatCard title={t("accountBook.remainingBalance")} value={formatCurrency(stats.financialBalance)} icon={DollarSign} color="bg-blue-500" isNA={isNA(stats.financialBalance)} />
|
||||||
/>
|
</div>
|
||||||
<StatCard
|
</motion.div>
|
||||||
title="إجمالي الحجوزات"
|
|
||||||
value={stats.totalReservations}
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.1 }} className="mb-8">
|
||||||
icon={Calendar}
|
<h2 className="text-xl font-bold text-gray-900 mb-4">{t("accountBook.directPropertyStats")}</h2>
|
||||||
color="bg-blue-500"
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-6">
|
||||||
/>
|
<StatCard title={t("accountBook.revenue")} value={formatCurrency(stats.directRevenue)} icon={TrendingUp} color="bg-emerald-500" isNA={isNA(stats.directRevenue)} />
|
||||||
<StatCard
|
<StatCard title={t("accountBook.commission")} value={formatCurrency(stats.directCommission)} icon={DollarSign} color="bg-orange-500" isNA={isNA(stats.directCommission)} />
|
||||||
title="العقارات النشطة"
|
<StatCard title={t("accountBook.remainingBalance")} value={formatCurrency(stats.directBalance)} icon={DollarSign} color="bg-blue-500" isNA={isNA(stats.directBalance)} />
|
||||||
value={stats.activeProperties}
|
<StatCard title={t("accountBook.overallRating")} value={formatRating(stats.directRating)} icon={Star} color="bg-purple-500" isNA={isNA(stats.directRating)} />
|
||||||
icon={Building}
|
<StatCard title={t("accountBook.postponements")} value={formatNumber(stats.directPostponements)} icon={Calendar} color="bg-red-500" isNA={isNA(stats.directPostponements)} />
|
||||||
color="bg-amber-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.2 }}
|
|
||||||
className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden"
|
|
||||||
>
|
|
||||||
<div className="px-6 py-4 border-b border-gray-200">
|
|
||||||
<h2 className="text-lg font-bold text-gray-900">المعاملات المالية</h2>
|
|
||||||
</div>
|
</div>
|
||||||
{transactions.length > 0 ? (
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<table className="min-w-full divide-y divide-gray-200 text-sm">
|
|
||||||
<thead className="bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th className="px-4 py-3 text-center text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
|
||||||
التاريخ
|
|
||||||
</th>
|
|
||||||
<th className="px-4 py-3 text-center text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
|
||||||
البيان
|
|
||||||
</th>
|
|
||||||
<th className="px-4 py-3 text-center text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
|
||||||
المبلغ
|
|
||||||
</th>
|
|
||||||
<th className="px-4 py-3 text-center text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
|
||||||
الحالة
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="bg-white divide-y divide-gray-100">
|
|
||||||
{transactions.map((tx, idx) => (
|
|
||||||
<tr
|
|
||||||
key={tx.id || idx}
|
|
||||||
className={`hover:bg-amber-50/40 transition-colors ${
|
|
||||||
idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<td className="px-4 py-3 whitespace-nowrap text-center text-gray-700">
|
|
||||||
{tx.date}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 whitespace-nowrap text-center text-gray-800 font-medium">
|
|
||||||
{tx.description}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 whitespace-nowrap text-center font-mono font-semibold text-gray-800">
|
|
||||||
{formatCurrency(tx.amount)}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 whitespace-nowrap text-center">
|
|
||||||
<span
|
|
||||||
className={`inline-flex items-center px-2 py-1 rounded-lg text-xs font-medium ${
|
|
||||||
tx.status === 'completed'
|
|
||||||
? 'bg-green-100 text-green-800'
|
|
||||||
: tx.status === 'pending'
|
|
||||||
? 'bg-yellow-100 text-yellow-800'
|
|
||||||
: 'bg-red-100 text-red-800'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{tx.status === 'completed'
|
|
||||||
? 'مكتمل'
|
|
||||||
: tx.status === 'pending'
|
|
||||||
? 'قيد الانتظار'
|
|
||||||
: 'ملغي'}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="p-12 text-center">
|
|
||||||
<DollarSign className="w-12 h-12 text-gray-300 mx-auto mb-4" />
|
|
||||||
<p className="text-gray-500">لا توجد معاملات مالية بعد</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from "framer-motion";
|
||||||
import { AlertTriangle, RefreshCw, Home } from 'lucide-react';
|
import { AlertTriangle, RefreshCw, Home } from "lucide-react";
|
||||||
import Link from 'next/link';
|
import Link from "next/link";
|
||||||
|
|
||||||
export default function Error({ error, reset }) {
|
export default function Error({ error, reset }) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
|
|
||||||
export default function Loading() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center">
|
|
||||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-center">
|
|
||||||
<div className="w-14 h-14 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
|
||||||
<p className="text-gray-400 text-lg">جاري التحميل...</p>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,14 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
|
|
||||||
export default function Loading() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center">
|
|
||||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-center">
|
|
||||||
<div className="w-14 h-14 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
|
||||||
<p className="text-gray-400 text-lg">جاري التحميل...</p>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
Calendar,
|
Calendar,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
@ -39,13 +40,14 @@ import toast, { Toaster } from 'react-hot-toast';
|
|||||||
import AuthService from '../../services/AuthService';
|
import AuthService from '../../services/AuthService';
|
||||||
|
|
||||||
const MonthlyCalendar = ({ properties, selectedPropertyId, onDateClick, onPropertySelect }) => {
|
const MonthlyCalendar = ({ properties, selectedPropertyId, onDateClick, onPropertySelect }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [currentMonth, setCurrentMonth] = useState(new Date());
|
const [currentMonth, setCurrentMonth] = useState(new Date());
|
||||||
const [selectedDate, setSelectedDate] = useState(null);
|
const [selectedDate, setSelectedDate] = useState(null);
|
||||||
const [viewType, setViewType] = useState('grid');
|
const [viewType, setViewType] = useState('grid');
|
||||||
|
|
||||||
const monthNames = [
|
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(
|
const daysInMonth = new Date(
|
||||||
@ -76,17 +78,17 @@ const MonthlyCalendar = ({ properties, selectedPropertyId, onDateClick, onProper
|
|||||||
const totalProperties = properties.length;
|
const totalProperties = properties.length;
|
||||||
const bookedCount = properties.filter(p => isDateBookedForProperty(date, p)).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 === 0) return { status: 'all_available', label: t('calendar.allAvailable'), color: 'bg-green-100 text-green-800' };
|
||||||
if (bookedCount === totalProperties) return { status: 'all_booked', label: 'جميع العقارات محجوزة', color: 'bg-red-100 text-red-800' };
|
if (bookedCount === totalProperties) return { status: 'all_booked', label: t('calendar.allBooked'), color: 'bg-red-100 text-red-800' };
|
||||||
return { status: 'partial', label: `${bookedCount}/${totalProperties} محجوز`, color: 'bg-yellow-100 text-yellow-800' };
|
return { status: 'partial', label: t('calendar.bookedCount', { count: bookedCount, total: totalProperties }), color: 'bg-yellow-100 text-yellow-800' };
|
||||||
} else {
|
} else {
|
||||||
const property = properties.find(p => p.id === selectedPropertyId);
|
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);
|
const isBooked = isDateBookedForProperty(date, property);
|
||||||
return {
|
return {
|
||||||
status: isBooked ? 'booked' : 'available',
|
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'
|
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())}
|
onClick={() => setCurrentMonth(new Date())}
|
||||||
className="px-4 py-2 bg-amber-500 text-white rounded-xl text-sm hover:bg-amber-600 transition-colors"
|
className="px-4 py-2 bg-amber-500 text-white rounded-xl text-sm hover:bg-amber-600 transition-colors"
|
||||||
>
|
>
|
||||||
اليوم
|
{t('calendar.today')}
|
||||||
</button>
|
</button>
|
||||||
<div className="flex border border-gray-200 rounded-xl overflow-hidden">
|
<div className="flex border border-gray-200 rounded-xl overflow-hidden">
|
||||||
<button
|
<button
|
||||||
@ -192,7 +194,7 @@ const MonthlyCalendar = ({ properties, selectedPropertyId, onDateClick, onProper
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-7 gap-1 p-4 bg-gray-50 border-b border-gray-200">
|
<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">
|
<div key={index} className="text-center text-sm font-medium text-gray-600 py-2">
|
||||||
{day}
|
{day}
|
||||||
</div>
|
</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 flex-wrap gap-4 justify-center text-xs">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-3 h-3 bg-green-100 rounded" />
|
<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>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-3 h-3 bg-red-100 rounded" />
|
<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>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-3 h-3 bg-yellow-100 rounded" />
|
<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>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-3 h-3 ring-2 ring-amber-500 rounded" />
|
<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>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-3 h-3 ring-2 ring-blue-500 rounded" />
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -234,8 +236,9 @@ const MonthlyCalendar = ({ properties, selectedPropertyId, onDateClick, onProper
|
|||||||
};
|
};
|
||||||
|
|
||||||
const PropertyCalendarList = ({ properties, selectedDate, onPropertyClick }) => {
|
const PropertyCalendarList = ({ properties, selectedDate, onPropertyClick }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const formatCurrency = (amount) => {
|
const formatCurrency = (amount) => {
|
||||||
return amount?.toLocaleString() + ' ل.س';
|
return amount?.toLocaleString() + ' ' + t('calendar.syp');
|
||||||
};
|
};
|
||||||
|
|
||||||
const isDateBooked = (property, date) => {
|
const isDateBooked = (property, date) => {
|
||||||
@ -264,8 +267,8 @@ const PropertyCalendarList = ({ properties, selectedDate, onPropertyClick }) =>
|
|||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-12 text-center">
|
<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" />
|
<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>
|
<h3 className="text-lg font-bold text-gray-700 mb-2">{t('calendar.selectDate')}</h3>
|
||||||
<p className="text-gray-500">اضغط على أي يوم في التقويم لعرض حالة العقارات في ذلك التاريخ</p>
|
<p className="text-gray-500">{t('calendar.clickDayHint')}</p>
|
||||||
</div>
|
</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">
|
<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">
|
<h3 className="text-lg font-bold text-gray-900 flex items-center gap-2">
|
||||||
<CalendarDays className="w-5 h-5 text-amber-500" />
|
<CalendarDays className="w-5 h-5 text-amber-500" />
|
||||||
حالة العقارات في تاريخ: {formattedDate}
|
{t('calendar.statusForDate')} {formattedDate}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -306,7 +309,7 @@ const PropertyCalendarList = ({ properties, selectedDate, onPropertyClick }) =>
|
|||||||
<span className={`px-2 py-1 rounded-lg text-xs font-medium ${
|
<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 ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800'
|
||||||
}`}>
|
}`}>
|
||||||
{isBooked ? 'محجوز' : 'متاح'}
|
{isBooked ? t('calendar.booked') : t('calendar.available')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 text-gray-500 text-sm mb-2">
|
<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 flex-wrap gap-3 text-sm text-gray-600">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Bed className="w-4 h-4" />
|
<Bed className="w-4 h-4" />
|
||||||
<span>{property.bedrooms} غرف</span>
|
<span>{property.bedrooms} {t('calendar.rooms')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Bath className="w-4 h-4" />
|
<Bath className="w-4 h-4" />
|
||||||
<span>{property.bathrooms} حمامات</span>
|
<span>{property.bathrooms} {t('calendar.bathrooms')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Square className="w-4 h-4" />
|
<Square className="w-4 h-4" />
|
||||||
<span>{property.area} م²</span>
|
<span>{property.area} {t('calendar.sqm')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="text-lg font-bold text-amber-600">{formatCurrency(property.price)}</div>
|
<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 && (
|
{isBooked && booking && (
|
||||||
<div className="mt-2 text-xs text-gray-500">
|
<div className="mt-2 text-xs text-gray-500">
|
||||||
<div>مستأجر: {booking.tenantName || 'غير معروف'}</div>
|
<div>{t('calendar.tenant')}: {booking.tenantName || t('calendar.unknown')}</div>
|
||||||
<div>من: {booking.startDate} إلى {booking.endDate}</div>
|
<div>{t('calendar.from')} {booking.startDate} {t('calendar.to')} {booking.endDate}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -349,10 +352,11 @@ const PropertyCalendarList = ({ properties, selectedDate, onPropertyClick }) =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
const PropertyDetailsModal = ({ property, isOpen, onClose }) => {
|
const PropertyDetailsModal = ({ property, isOpen, onClose }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
if (!isOpen || !property) return null;
|
if (!isOpen || !property) return null;
|
||||||
|
|
||||||
const formatCurrency = (amount) => {
|
const formatCurrency = (amount) => {
|
||||||
return amount?.toLocaleString() + ' ل.س';
|
return amount?.toLocaleString() + ' ' + t('calendar.syp');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -385,7 +389,7 @@ const PropertyDetailsModal = ({ property, isOpen, onClose }) => {
|
|||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
{property.images && property.images.length > 0 && (
|
{property.images && property.images.length > 0 && (
|
||||||
<div>
|
<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">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
{property.images.slice(0, 4).map((image, index) => (
|
{property.images.slice(0, 4).map((image, index) => (
|
||||||
<div key={index} className="relative h-32 rounded-lg overflow-hidden bg-gray-100">
|
<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">
|
<div className="bg-gray-50 p-3 rounded-xl text-center">
|
||||||
<Bed className="w-5 h-5 text-amber-500 mx-auto mb-1" />
|
<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-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>
|
||||||
<div className="bg-gray-50 p-3 rounded-xl text-center">
|
<div className="bg-gray-50 p-3 rounded-xl text-center">
|
||||||
<Bath className="w-5 h-5 text-amber-500 mx-auto mb-1" />
|
<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-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>
|
||||||
<div className="bg-gray-50 p-3 rounded-xl text-center">
|
<div className="bg-gray-50 p-3 rounded-xl text-center">
|
||||||
<Square className="w-5 h-5 text-amber-500 mx-auto mb-1" />
|
<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-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>
|
||||||
<div className="bg-gray-50 p-3 rounded-xl text-center">
|
<div className="bg-gray-50 p-3 rounded-xl text-center">
|
||||||
<DollarSign className="w-5 h-5 text-amber-500 mx-auto mb-1" />
|
<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-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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{property.features && property.features.length > 0 && (
|
{property.features && property.features.length > 0 && (
|
||||||
<div>
|
<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">
|
<div className="flex flex-wrap gap-2">
|
||||||
{property.features.map((feature, index) => (
|
{property.features.map((feature, index) => (
|
||||||
<span key={index} className="px-2 py-1 bg-gray-100 text-gray-700 rounded-lg text-xs">
|
<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>
|
<div>
|
||||||
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2">
|
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2">
|
||||||
<Clock className="w-4 h-4 text-amber-500" />
|
<Clock className="w-4 h-4 text-amber-500" />
|
||||||
الحجوزات القادمة
|
{t('calendar.upcomingBookings')}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{property.bookings.slice(0, 3).map((booking, index) => (
|
{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 key={index} className="bg-gray-50 p-3 rounded-lg flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-900">{booking.startDate} - {booking.endDate}</p>
|
<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>
|
</div>
|
||||||
<span className="text-sm font-bold text-amber-600">{formatCurrency(booking.totalAmount)}</span>
|
<span className="text-sm font-bold text-amber-600">{formatCurrency(booking.totalAmount)}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -458,13 +462,13 @@ const PropertyDetailsModal = ({ property, isOpen, onClose }) => {
|
|||||||
href={`/owner/properties/edit?id=${property.id}`}
|
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"
|
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>
|
</Link>
|
||||||
<button
|
<button
|
||||||
onClick={() => window.location.href = `/owner/bookings?property=${property.id}`}
|
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"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@ -473,6 +477,7 @@ const PropertyDetailsModal = ({ property, isOpen, onClose }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function OwnerCalendarPage() {
|
export default function OwnerCalendarPage() {
|
||||||
|
const { t, i18n } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
const [properties, setProperties] = useState([]);
|
const [properties, setProperties] = useState([]);
|
||||||
@ -509,44 +514,44 @@ export default function OwnerCalendarPage() {
|
|||||||
const mockProperties = [
|
const mockProperties = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
title: 'فيلا فاخرة في المزة',
|
title: t('calendar.mockVillaTitle'),
|
||||||
location: 'دمشق، المزة',
|
location: t('calendar.mockVillaLocation'),
|
||||||
bedrooms: 5,
|
bedrooms: 5,
|
||||||
bathrooms: 4,
|
bathrooms: 4,
|
||||||
area: 450,
|
area: 450,
|
||||||
price: 500000,
|
price: 500000,
|
||||||
features: ['مسبح', 'حديقة خاصة', 'موقف سيارات', 'أمن 24/7'],
|
features: [t('swimmingPool'), t('privateGarden'), t('parking'), t('propertyService.security247')],
|
||||||
images: ['/villa1.jpg'],
|
images: ['/villa1.jpg'],
|
||||||
status: 'available',
|
status: 'available',
|
||||||
bookings: [
|
bookings: [
|
||||||
{ startDate: '2024-03-10', endDate: '2024-03-15', 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: 'سارة أحمد' }
|
{ startDate: '2024-03-20', endDate: '2024-03-25', totalAmount: 2500000, tenantName: t('calendar.mockTenant2') }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: 'شقة حديثة في الشهباء',
|
title: t('calendar.mockApartmentTitle'),
|
||||||
location: 'حلب، الشهباء',
|
location: t('calendar.mockApartmentLocation'),
|
||||||
bedrooms: 3,
|
bedrooms: 3,
|
||||||
bathrooms: 2,
|
bathrooms: 2,
|
||||||
area: 180,
|
area: 180,
|
||||||
price: 250000,
|
price: 250000,
|
||||||
features: ['مطبخ مجهز', 'بلكونة', 'موقف سيارات', 'مصعد'],
|
features: [t('equippedKitchen'), t('balcony'), t('parking'), t('propertyService.elevator')],
|
||||||
images: ['/apartment1.jpg'],
|
images: ['/apartment1.jpg'],
|
||||||
status: 'available',
|
status: 'available',
|
||||||
bookings: [
|
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,
|
id: 3,
|
||||||
title: 'بيت عائلي في بابا عمرو',
|
title: t('calendar.mockHouseTitle'),
|
||||||
location: 'حمص، بابا عمرو',
|
location: t('calendar.mockHouseLocation'),
|
||||||
bedrooms: 4,
|
bedrooms: 4,
|
||||||
bathrooms: 3,
|
bathrooms: 3,
|
||||||
area: 300,
|
area: 300,
|
||||||
price: 350000,
|
price: 350000,
|
||||||
features: ['حديقة كبيرة', 'موقف سيارات', 'مدفأة', 'كراج'],
|
features: [t('largeGarden'), t('parking'), t('fireplace'), t('garage')],
|
||||||
images: ['/house1.jpg'],
|
images: ['/house1.jpg'],
|
||||||
status: 'booked',
|
status: 'booked',
|
||||||
bookings: []
|
bookings: []
|
||||||
@ -586,14 +591,14 @@ export default function OwnerCalendarPage() {
|
|||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Loader2 className="w-12 h-12 text-amber-500 animate-spin mx-auto mb-4" />
|
<Loader2 className="w-12 h-12 text-amber-500 animate-spin mx-auto mb-4" />
|
||||||
<p className="text-gray-600">جاري تحميل التقويم...</p>
|
<p className="text-gray-600">{t('calendar.loading')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
|
||||||
<PropertyDetailsModal
|
<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"
|
className="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4"
|
||||||
>
|
>
|
||||||
<div>
|
<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('calendar.pageTitle')}</h1>
|
||||||
<p className="text-gray-600">مرحباً {user?.name}، تتبع حالة عقاراتك عبر التقويم</p>
|
<p className="text-gray-600">{t('calendar.welcomeMessage', { name: user?.name })}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-3">
|
<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"
|
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" />
|
<Filter className="w-5 h-5" />
|
||||||
فلترة العقارات
|
{t('calendar.filterProperties')}
|
||||||
<ChevronDown className={`w-4 h-4 transition-transform ${showFilters ? 'rotate-180' : ''}`} />
|
<ChevronDown className={`w-4 h-4 transition-transform ${showFilters ? 'rotate-180' : ''}`} />
|
||||||
</button>
|
</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">
|
{/* <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" />
|
<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-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>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
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" />
|
<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-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>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
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" />
|
<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-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>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
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" />
|
<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-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>
|
</motion.div>
|
||||||
</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="bg-white rounded-2xl shadow-sm border border-gray-200 p-4">
|
||||||
<div className="flex flex-wrap gap-3 items-center">
|
<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
|
<select
|
||||||
value={selectedPropertyId}
|
value={selectedPropertyId}
|
||||||
onChange={(e) => setSelectedPropertyId(e.target.value)}
|
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"
|
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) => (
|
{properties.map((property) => (
|
||||||
<option key={property.id} value={property.id}>{property.title}</option>
|
<option key={property.id} value={property.id}>{property.title}</option>
|
||||||
))}
|
))}
|
||||||
@ -697,7 +702,7 @@ export default function OwnerCalendarPage() {
|
|||||||
onClick={() => setSelectedPropertyId('all')}
|
onClick={() => setSelectedPropertyId('all')}
|
||||||
className="px-3 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
|
className="px-3 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
|
||||||
>
|
>
|
||||||
إعادة تعيين
|
{t('calendar.reset')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -729,7 +734,7 @@ export default function OwnerCalendarPage() {
|
|||||||
className="mt-6 text-center text-sm text-gray-500"
|
className="mt-6 text-center text-sm text-gray-500"
|
||||||
>
|
>
|
||||||
<AlertCircle className="w-4 h-4 inline ml-1" />
|
<AlertCircle className="w-4 h-4 inline ml-1" />
|
||||||
اضغط على أي عقار لعرض التفاصيل الكاملة
|
{t('calendar.clickPropertyHint')}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
|
|
||||||
export default function Loading() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center">
|
|
||||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-center">
|
|
||||||
<div className="w-14 h-14 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
|
||||||
<p className="text-gray-400 text-lg">جاري التحميل...</p>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -298,9 +298,11 @@ import { useRouter } from 'next/navigation';
|
|||||||
import { Download, Loader2 } from 'lucide-react';
|
import { Download, Loader2 } from 'lucide-react';
|
||||||
import toast, { Toaster } from 'react-hot-toast';
|
import toast, { Toaster } from 'react-hot-toast';
|
||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import AuthService from '@/app/services/AuthService';
|
import AuthService from '@/app/services/AuthService';
|
||||||
|
|
||||||
export default function OwnerProfitsPage() {
|
export default function OwnerProfitsPage() {
|
||||||
|
const { t, i18n } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
@ -404,29 +406,29 @@ export default function OwnerProfitsPage() {
|
|||||||
const handleExportReport = () => {
|
const handleExportReport = () => {
|
||||||
try {
|
try {
|
||||||
const exportData = tableData.map((row) => ({
|
const exportData = tableData.map((row) => ({
|
||||||
'العقار': row.property,
|
[t('profits.colProperty')]: row.property,
|
||||||
'رقم الحجز': row.bookingNumber,
|
[t('profits.colBookingNumber')]: row.bookingNumber,
|
||||||
'من تاريخ': row.fromDate,
|
[t('profits.colFromDate')]: row.fromDate,
|
||||||
'حتى تاريخ': row.toDate,
|
[t('profits.colToDate')]: row.toDate,
|
||||||
'العروض المستلم': row.amountReceived,
|
[t('profits.colAmountReceived')]: row.amountReceived,
|
||||||
'عمولة المنصة': row.platformCommission,
|
[t('profits.colPlatformCommission')]: row.platformCommission,
|
||||||
'ربح المنصة (5%)': row.platformProfit,
|
[t('profits.colPlatformProfit')]: row.platformProfit,
|
||||||
'المستحق للمالك': row.ownerDue,
|
[t('profits.colOwnerDue')]: row.ownerDue,
|
||||||
'تم التحويل للمالك': row.transferredToOwner,
|
[t('profits.colTransferred')]: row.transferredToOwner,
|
||||||
'رقم وصل التحويل': row.transferReceipt,
|
[t('profits.colTransferReceipt')]: row.transferReceipt,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
exportData.push({
|
exportData.push({
|
||||||
'العقار': 'الإجمالي العام',
|
[t('profits.colProperty')]: t('profits.totalGeneral'),
|
||||||
'رقم الحجز': '',
|
[t('profits.colBookingNumber')]: '',
|
||||||
'من تاريخ': '',
|
[t('profits.colFromDate')]: '',
|
||||||
'حتى تاريخ': '',
|
[t('profits.colToDate')]: '',
|
||||||
'العروض المستلم': totals.totalAmountReceived,
|
[t('profits.colAmountReceived')]: totals.totalAmountReceived,
|
||||||
'عمولة المنصة': totals.totalCommission,
|
[t('profits.colPlatformCommission')]: totals.totalCommission,
|
||||||
'ربح المنصة (5%)': totals.totalPlatformProfit,
|
[t('profits.colPlatformProfit')]: totals.totalPlatformProfit,
|
||||||
'المستحق للمالك': totals.totalOwnerDue,
|
[t('profits.colOwnerDue')]: totals.totalOwnerDue,
|
||||||
'تم التحويل للمالك': totals.totalTransferred,
|
[t('profits.colTransferred')]: totals.totalTransferred,
|
||||||
'رقم وصل التحويل': '—',
|
[t('profits.colTransferReceipt')]: '—',
|
||||||
});
|
});
|
||||||
|
|
||||||
const worksheet = XLSX.utils.json_to_sheet(exportData);
|
const worksheet = XLSX.utils.json_to_sheet(exportData);
|
||||||
@ -445,14 +447,14 @@ export default function OwnerProfitsPage() {
|
|||||||
worksheet['!cols'] = colWidths;
|
worksheet['!cols'] = colWidths;
|
||||||
|
|
||||||
const workbook = XLSX.utils.book_new();
|
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) {
|
} catch (error) {
|
||||||
console.error('خطأ في التصدير:', error);
|
console.error(t('profits.exportErrorLog'), error);
|
||||||
toast.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="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Loader2 className="w-12 h-12 text-amber-500 animate-spin mx-auto mb-4" />
|
<Loader2 className="w-12 h-12 text-amber-500 animate-spin mx-auto mb-4" />
|
||||||
<p className="text-gray-600">جاري تحميل بيانات الأرباح...</p>
|
<p className="text-gray-600">{t('profits.loading')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">
|
<div className="min-h-screen bg-gray-50 py-8" dir={i18n.language === 'ar' ? 'rtl' : 'ltr'}>
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
<div className="container mx-auto px-4 max-w-7xl">
|
<div className="container mx-auto px-4 max-w-7xl">
|
||||||
<motion.div
|
<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"
|
className="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4"
|
||||||
>
|
>
|
||||||
<div>
|
<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">
|
<p className="text-gray-600">
|
||||||
مرحباً {user?.name}
|
{t('profits.greeting')} {user?.name}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<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"
|
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" />
|
<Download className="w-5 h-5" />
|
||||||
تصدير التقرير
|
{t('profits.exportReport')}
|
||||||
</button>
|
</button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@ -500,18 +502,18 @@ export default function OwnerProfitsPage() {
|
|||||||
<table className="min-w-full divide-y divide-gray-200 text-sm">
|
<table className="min-w-full divide-y divide-gray-200 text-sm">
|
||||||
<thead className="bg-gray-800 text-gray-100">
|
<thead className="bg-gray-800 text-gray-100">
|
||||||
<tr>
|
<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">{t('profits.colProperty')}</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.colBookingNumber')}</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.colFromDate')}</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.colToDate')}</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.colAmountReceived')}</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.colPlatformCommission')}</th>
|
||||||
<th className="px-4 py-4 text-center text-xs font-semibold uppercase tracking-wider bg-amber-50 text-amber-800">
|
<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>
|
||||||
<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">تم التحويل للمالك</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">رقم وصل التحويل</th>
|
<th className="px-4 py-4 text-center text-xs font-semibold uppercase tracking-wider">{t('profits.colTransferReceipt')}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-100">
|
<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">
|
<tfoot className="bg-gray-100 border-t-2 border-gray-300">
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan="4" className="px-4 py-4 text-right font-bold text-gray-800">
|
<td colSpan="4" className="px-4 py-4 text-right font-bold text-gray-800">
|
||||||
الإجمالي العام
|
{t('profits.totalGeneral')}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-4 text-center font-bold font-mono text-gray-800">
|
<td className="px-4 py-4 text-center font-bold font-mono text-gray-800">
|
||||||
{totals.totalAmountReceived}
|
{totals.totalAmountReceived}
|
||||||
@ -582,8 +584,8 @@ export default function OwnerProfitsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-gray-50 px-6 py-3 text-xs text-gray-500 border-t border-gray-200">
|
<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> ملاحظة:
|
<span className="inline-flex items-center gap-1"></span> {t('profits.note')}
|
||||||
<strong> ربح المنصة </strong> يُحتسب تلقائياً بنسبة <strong className="text-amber-600">5%</strong> من قيمة «العروض المستلم».
|
<strong> {t('profits.colPlatformProfit')} </strong>{t('profits.noteMiddle')}<strong className="text-amber-600">5%</strong>{t('profits.noteEnd', { colName: t('profits.colAmountReceived') })}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user