fixd larg lines in HomePage , add cookies rather than localStorage,make canvas background Login page
This commit is contained in:
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,7 +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 BottomNav from './components/BottomNav';
|
import BottomNav from "./components/BottomNav";
|
||||||
import {
|
import {
|
||||||
Globe,
|
Globe,
|
||||||
LogIn,
|
LogIn,
|
||||||
@ -16,25 +16,16 @@ 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,
|
||||||
Moon,
|
|
||||||
Sun,
|
|
||||||
FileText,
|
FileText,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
@ -43,8 +34,9 @@ import AuthService from "./services/AuthService";
|
|||||||
import { UserRole, UserRoleLabels } from "./enums/UserRole";
|
import { UserRole, UserRoleLabels } from "./enums/UserRole";
|
||||||
import "./i18n/config";
|
import "./i18n/config";
|
||||||
import NotificationHandler from "./components/NotificationHandler";
|
import NotificationHandler from "./components/NotificationHandler";
|
||||||
import { useTheme } from '@/app/contexts/ThemeContext';
|
|
||||||
import { useNotifications } from '@/app/contexts/NotificationsContext';
|
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();
|
||||||
@ -56,7 +48,7 @@ export default function ClientLayout({ children }) {
|
|||||||
const [isMounted, setIsMounted] = useState(false);
|
const [isMounted, setIsMounted] = useState(false);
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
const menuRef = useRef(null);
|
const menuRef = useRef(null);
|
||||||
const { theme, toggleTheme } = useTheme();
|
|
||||||
const { unreadCount } = useNotifications();
|
const { unreadCount } = useNotifications();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -128,13 +120,7 @@ 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 isHomePage = pathname === "/";
|
||||||
@ -154,64 +140,52 @@ 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" suppressHydrationWarning>{t("loading")}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col">
|
<div className="min-h-screen flex flex-col">
|
||||||
{!isAuthPage && !isAuthenticated && (
|
{!isAuthPage && !isAuthenticated && (
|
||||||
<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"}`}>
|
<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 hidden md:block ${isHomePage ? "text-white" : "text-gray-800"}`}>
|
<span className={`text-3xl font-bold hidden md:block ${isHomePage ? "text-white" : "text-gray-800"}`}>
|
||||||
{t("brandNamePart1")}
|
{t("brandNamePart1")}
|
||||||
<span className={isHomePage ? "text-amber-400" : "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 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"}`}>
|
<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>
|
</svg>
|
||||||
<span className="text-sm font-semibold">{t("downloadApp")}</span>
|
<span className="text-sm font-semibold">{t("downloadApp")}</span>
|
||||||
<svg className="w-4 h-4 transition-transform group-hover:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7"/></svg>
|
<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>
|
||||||
@ -222,7 +196,7 @@ export default function ClientLayout({ children }) {
|
|||||||
<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>
|
||||||
@ -234,9 +208,12 @@ export default function ClientLayout({ children }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NavLink light={isHomePage} href="/">{t("home")}</NavLink>
|
<NavLink light={isHomePage} href="/">
|
||||||
<NavLink light={isHomePage} href="/properties">{t("ourProperties")}</NavLink>
|
{t("home")}
|
||||||
|
</NavLink>
|
||||||
|
<NavLink light={isHomePage} href="/properties">
|
||||||
|
{t("ourProperties")}
|
||||||
|
</NavLink>
|
||||||
|
|
||||||
{isOwner && (
|
{isOwner && (
|
||||||
<>
|
<>
|
||||||
@ -311,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 || t("userLabel")}
|
<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">
|
|
||||||
{t(UserRoleLabels[user?.role] ? `userRole.${user?.role.toLowerCase()}` : 'visitor')}
|
|
||||||
</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">{t("profile")}</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>
|
||||||
{t("viewAndEditInfo")}
|
|
||||||
</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">{t("favorites")}</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>
|
||||||
{t("savedProperties")}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
@ -365,37 +322,23 @@ 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">{t("myBookedProperties")}</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>
|
||||||
{t("viewAndRateBooked")}
|
|
||||||
</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">{t("myRatings")}</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>
|
||||||
{t("ratingsReceived")}
|
|
||||||
</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">{t("settings")}</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>
|
||||||
{t("accountAndSecurity")}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
@ -411,9 +354,7 @@ 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">{t("myProperties")}</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>
|
||||||
{t("manageYourProperties")}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
@ -425,9 +366,7 @@ 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">{t("addProperty")}</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>
|
||||||
{t("addNewProperty")}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
@ -439,9 +378,7 @@ 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">{t("reservations")}</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>
|
||||||
{t("manageReservations")}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
@ -453,9 +390,7 @@ 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">{t("calendar")}</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>
|
||||||
{t("propertyAvailability")}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
@ -467,9 +402,7 @@ 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">{t("profits")}</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>
|
||||||
{t("statisticsAndProfits")}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
@ -481,9 +414,7 @@ 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">{t("accountBook")}</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>
|
||||||
{t("financialRecords")}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
@ -501,9 +432,7 @@ 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">{t("myBookings")}</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>
|
||||||
{t("viewBookings")}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
@ -515,9 +444,7 @@ 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">{t("myBookedProperties")}</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>
|
||||||
{t("viewAndRateBooked")}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
@ -529,23 +456,15 @@ 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">{t("myRatings")}</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>
|
||||||
{t("ratingsReceived")}
|
|
||||||
</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">{t("reports")}</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>
|
||||||
{t("submitAndTrackReports")}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
@ -553,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">{t("logout")}</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>
|
||||||
{t("endSession")}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -574,21 +488,10 @@ export default function ClientLayout({ children }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="md:hidden flex items-center gap-3">
|
<div className="md:hidden flex items-center gap-3">
|
||||||
<motion.button
|
|
||||||
whileHover={{ scale: 1.1 }}
|
|
||||||
whileTap={{ scale: 0.9 }}
|
|
||||||
onClick={toggleTheme}
|
|
||||||
className="flex items-center justify-center w-10 h-10 bg-gray-100 hover:bg-gray-200 rounded-full transition-colors"
|
|
||||||
aria-label="Toggle theme"
|
|
||||||
>
|
|
||||||
{theme === 'dark' ? <Sun className="w-5 h-5 text-gray-700" /> : <Moon className="w-5 h-5 text-gray-700" />}
|
|
||||||
</motion.button>
|
|
||||||
<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" />
|
||||||
@ -609,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>
|
||||||
@ -637,53 +536,50 @@ export default function ClientLayout({ children }) {
|
|||||||
{/* Download App - Mobile */}
|
{/* Download App - Mobile */}
|
||||||
<div className={`border-t my-2 ${isHomePage ? "border-white/10" : "border-gray-200"}`}></div>
|
<div className={`border-t my-2 ${isHomePage ? "border-white/10" : "border-gray-200"}`}></div>
|
||||||
<p className={`px-3 py-1 text-xs font-medium ${isHomePage ? "text-white/50" : "text-gray-400"}`}>{t("downloadApp")}</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 transition-colors ${isHomePage ? "text-green-400 hover:bg-white/10" : "text-green-600 hover:bg-green-50"}`}>
|
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">{t("android")} - {t("downloadAPK")}</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">{t("ios")} - {t("comingSoon")}</span>
|
<span className="text-sm">
|
||||||
|
{t("ios")} - {t("comingSoon")}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={`border-t my-2 ${isHomePage ? "border-white/10" : "border-gray-200"}`}></div>
|
<div className={`border-t my-2 ${isHomePage ? "border-white/10" : "border-gray-200"}`}></div>
|
||||||
|
|
||||||
{isOwner && (
|
{isOwner && (
|
||||||
<>
|
<>
|
||||||
<MobileNavLink light={isHomePage} href="/owner/properties"
|
<MobileNavLink light={isHomePage} href="/owner/properties" onClick={closeMobileMenu}>
|
||||||
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")}
|
{t("myProperties")}
|
||||||
</span>
|
</span>
|
||||||
</MobileNavLink>
|
</MobileNavLink>
|
||||||
<MobileNavLink light={isHomePage}
|
<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")}
|
{t("reservations")}
|
||||||
</span>
|
</span>
|
||||||
</MobileNavLink>
|
</MobileNavLink>
|
||||||
<MobileNavLink light={isHomePage}
|
<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")}
|
{t("calendar")}
|
||||||
</span>
|
</span>
|
||||||
</MobileNavLink>
|
</MobileNavLink>
|
||||||
<MobileNavLink light={isHomePage}
|
<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")}
|
{t("profits")}
|
||||||
@ -701,10 +597,7 @@ export default function ClientLayout({ children }) {
|
|||||||
{t("login")}
|
{t("login")}
|
||||||
</span>
|
</span>
|
||||||
</MobileNavLink>
|
</MobileNavLink>
|
||||||
<MobileNavLink light={isHomePage}
|
<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")}
|
{t("createAccount")}
|
||||||
@ -722,7 +615,7 @@ export default function ClientLayout({ children }) {
|
|||||||
<div className="fixed inset-0 -z-10">
|
<div className="fixed inset-0 -z-10">
|
||||||
<motion.div
|
<motion.div
|
||||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||||
style={{ backgroundImage: 'url(/hero.jpg)' }}
|
style={{ backgroundImage: "url(/hero.jpg)" }}
|
||||||
initial={{ scale: 1.1 }}
|
initial={{ scale: 1.1 }}
|
||||||
animate={{ scale: 1 }}
|
animate={{ scale: 1 }}
|
||||||
transition={{ duration: 1.5, ease: "easeOut" }}
|
transition={{ duration: 1.5, ease: "easeOut" }}
|
||||||
@ -732,110 +625,91 @@ export default function ClientLayout({ children }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<main
|
<main
|
||||||
className={`${!isAuthPage && !isProfilePage && !isAuthenticated ? "pt-20" : ""} flex-1 flex flex-col ${isHomePage ? "bg-transparent" : "bg-gray-50"} ${currentLanguage === "ar" ? "text-right" : "text-left"} ${isHomePage ? "relative" : ""} ${isAuthenticated && !isAuthPage ? "pb-20" : ""}`}
|
className={`${!isAuthPage && !isProfilePage && !isAuthenticated ? "pt-20" : ""} flex-1 flex flex-col ${isHomePage ? "bg-transparent" : "bg-gray-50"} ${currentLanguage === "ar" ? "text-right" : "text-left"} ${isHomePage ? "relative" : ""} ${isAuthenticated && !isAuthPage ? "pb-20" : ""}`}
|
||||||
>
|
>
|
||||||
{!isAuthPage && isAuthenticated && (
|
{!isAuthPage && isAuthenticated && (
|
||||||
<div className={`flex items-center justify-between px-4 py-2 sticky top-0 z-20 bg-transparent backdrop-blur-3xl ${isHomePage ? "" : "border-b border-gray-200/20"}`}>
|
<div className={`flex items-center 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">
|
<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')}`}>
|
<span
|
||||||
{!isAuthenticated ? t('visitor') : isOwner ? t('owner') : t('customer')}
|
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"}`}
|
||||||
</span>
|
>
|
||||||
</div>
|
{!isAuthenticated ? t("visitor") : isOwner ? t("owner") : t("customer")}
|
||||||
<div className="flex items-center gap-3">
|
</span>
|
||||||
<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={toggleTheme} className="relative group flex items-center justify-center w-9 h-9 rounded-full transition-colors hover:bg-white/10 nav-icon">
|
|
||||||
{theme === 'dark' ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
|
|
||||||
<div className="absolute -bottom-8 left-1/2 -translate-x-1/2 bg-gray-800 text-white text-xs rounded-lg px-2 py-1 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none shadow-lg">
|
|
||||||
{theme === 'dark' ? t('lightMode') : t('darkMode')}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<button onClick={() => changeLanguage(currentLanguage === 'ar' ? 'en' : 'ar')} className="relative group flex items-center justify-center w-9 h-9 rounded-full transition-colors hover:bg-white/10 nav-icon">
|
|
||||||
<Globe className="w-5 h-5" />
|
|
||||||
<div className="absolute -bottom-8 left-1/2 -translate-x-1/2 bg-gray-800 text-white text-xs rounded-lg px-2 py-1 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none shadow-lg">
|
|
||||||
{currentLanguage === 'ar' ? t('switchToEnglish') : t('switchToArabic')}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex-1">
|
|
||||||
{children}
|
|
||||||
</div>
|
</div>
|
||||||
{isAuthenticated && !isAuthPage && (
|
<div className="flex items-center gap-3">
|
||||||
<BottomNav isOwner={isOwner} isOwnerOrAgent={isOwnerOrAgent} />
|
<Link
|
||||||
)}
|
href="/favorites"
|
||||||
</main>
|
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>
|
||||||
@ -846,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" />
|
||||||
|
|||||||
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -109,13 +109,7 @@ export default function AccountVerificationPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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 },
|
||||||
@ -131,7 +125,7 @@ export default function AccountVerificationPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">
|
<div className="min-h-screen bg-gray-900 py-8" dir="rtl">
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
|
||||||
<div className="container mx-auto px-4 max-w-2xl">
|
<div className="container mx-auto px-4 max-w-2xl">
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -37,7 +37,7 @@ export default function ChooseRolePage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div dir={i18n.language === 'ar' ? 'rtl' : 'ltr'} className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center p-4 relative overflow-hidden">
|
<div dir={i18n.language === 'ar' ? 'rtl' : 'ltr'} className="min-h-screen flex items-center justify-center p-4 relative overflow-hidden">
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
{[...Array(20)].map((_, i) => (
|
{[...Array(20)].map((_, i) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
@ -88,7 +88,7 @@ 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}
|
||||||
>
|
>
|
||||||
{t('chooseRole.welcomeToSweetHome')}
|
{t('chooseRole.welcomeToSweetHome')}
|
||||||
|
|||||||
@ -1,24 +1,24 @@
|
|||||||
'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 { useTranslation } from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
import { ShieldAlert, LogOut, MessageSquare, Send, Loader2 } from 'lucide-react';
|
import { ShieldAlert, LogOut, MessageSquare, Send, Loader2 } from "lucide-react";
|
||||||
import toast, { Toaster } from 'react-hot-toast';
|
import toast, { Toaster } from "react-hot-toast";
|
||||||
import AuthService from '../services/AuthService';
|
import AuthService from "../services/AuthService";
|
||||||
import { sendGeneralReport } from '../utils/api';
|
import { sendGeneralReport } from "../utils/api";
|
||||||
|
|
||||||
export default function BlockedPage() {
|
export default function BlockedPage() {
|
||||||
const { t } = useTranslation();
|
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) => {
|
||||||
@ -30,7 +30,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(t('blocked.fillAllFields'));
|
toast.error(t("blocked.fillAllFields"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,36 +39,26 @@ 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(t('blocked.reportSent'));
|
toast.success(t("blocked.reportSent"));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(t('blocked.sendError'));
|
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-900 flex items-center justify-center p-4" dir="rtl">
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, y: 24 }} animate={{ opacity: 1, y: 0 }} className="w-full max-w-5xl">
|
||||||
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">{t('blocked.title')}</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>
|
||||||
{t('blocked.description')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-6">
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
@ -82,10 +72,8 @@ 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">{t('blocked.logoutTitle')}</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>
|
||||||
{t('blocked.logoutDesc')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@ -94,45 +82,40 @@ export default function BlockedPage() {
|
|||||||
className="mt-8 w-full bg-red-600 hover:bg-red-700 text-white rounded-2xl py-4 font-bold transition-colors flex items-center justify-center gap-3"
|
className="mt-8 w-full bg-red-600 hover:bg-red-700 text-white rounded-2xl py-4 font-bold transition-colors flex items-center justify-center gap-3"
|
||||||
>
|
>
|
||||||
<LogOut className="w-5 h-5" />
|
<LogOut className="w-5 h-5" />
|
||||||
{t('blocked.logoutButton')}
|
{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, y: 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">{t('blocked.contactSupportTitle')}</h2>
|
<h2 className="text-2xl font-bold text-gray-900">{t("blocked.contactSupportTitle")}</h2>
|
||||||
<p className="text-gray-600 mt-1">{t('blocked.contactSupportDesc')}</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">{t('subject')}</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={t('blocked.subjectPlaceholder')}
|
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">{t('blocked.messageLabel')}</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={t('blocked.messagePlaceholder')}
|
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>
|
||||||
@ -145,17 +128,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')}
|
{t("blocked.sending")}
|
||||||
</>
|
</>
|
||||||
) : isSent ? (
|
) : isSent ? (
|
||||||
<>
|
<>
|
||||||
<Send className="w-5 h-5" />
|
<Send className="w-5 h-5" />
|
||||||
{t('blocked.sentButton')}
|
{t("blocked.sentButton")}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Send className="w-5 h-5" />
|
<Send className="w-5 h-5" />
|
||||||
{t('blocked.sendButton')}
|
{t("blocked.sendButton")}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
@ -165,4 +148,4 @@ export default function BlockedPage() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,7 +54,7 @@ export default function ChangePasswordPage() {
|
|||||||
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 bg-gray-900 flex items-center justify-center p-4">
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
113
app/components/Background.js
Normal file
113
app/components/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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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} ل.س`;
|
||||||
@ -97,4 +97,4 @@ export default function PropertyMapWithMarkers({ properties = [], onPropertyClic
|
|||||||
<div ref={mapRef} className="w-full h-full z-0" />
|
<div ref={mapRef} className="w-full h-full z-0" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
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,321 +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: 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') }
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleTabClick = (tab) => {
|
const handleTabClick = (tab) => {
|
||||||
setActiveTab(tab);
|
setActiveTab(tab);
|
||||||
if ((tab === 'rent' || tab === 'sell') && !isAuthenticated) {
|
|
||||||
setShowLoginDialog(true);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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>
|
|
||||||
{activeTab === 'rent' && (
|
|
||||||
<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">
|
|
||||||
{t("identityTypeLabel")}
|
|
||||||
</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">{t("ownerSourceLabel")}</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>
|
|
||||||
|
|
||||||
{activeTab === 'rent' && (
|
return (
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-white mb-2">{t("rentTypeLabel")}</label>
|
|
||||||
<select
|
|
||||||
value={filters.rentPeriod}
|
|
||||||
onChange={(e) => setFilters({ ...filters, rentPeriod: e.target.value })}
|
|
||||||
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={`${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`}>
|
|
||||||
<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">{t("availableToday")}</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">{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
|
<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"}`}
|
||||||
>
|
>
|
||||||
{t("close")}
|
{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 */}
|
||||||
{t("login")}
|
<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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
73
app/components/home/HomeClient.jsx
Normal file
73
app/components/home/HomeClient.jsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
"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();
|
||||||
|
console.log(isOwner, name, isAuthenticated, role);
|
||||||
|
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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -5,7 +5,7 @@ import { createContext, useContext, useState, useEffect } from "react";
|
|||||||
const ThemeContext = createContext();
|
const ThemeContext = createContext();
|
||||||
|
|
||||||
export function ThemeProvider({ children }) {
|
export function ThemeProvider({ children }) {
|
||||||
const [theme, setTheme] = useState("light");
|
const [theme, setTheme] = useState("dark");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const stored = localStorage.getItem("theme");
|
const stored = localStorage.getItem("theme");
|
||||||
|
|||||||
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,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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -66,7 +66,7 @@ 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} />
|
||||||
<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>
|
||||||
|
|||||||
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,17 +1,33 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
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: "#08172f" }}>
|
||||||
<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-36 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-[3] 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-sm tracking-wide select-none">Sweet Home</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<p className="text-amber-400 text-sm font-bold tracking-wide animate-pulse">... جاري التحميل</p>
|
||||||
</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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -6,33 +6,10 @@ import { motion, AnimatePresence } from "framer-motion";
|
|||||||
import toast, { Toaster } from "react-hot-toast";
|
import toast, { Toaster } from "react-hot-toast";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import {
|
import { Mail, Lock, Eye, EyeOff, ArrowLeft, LogIn, CheckCircle, Loader2, Home, Shield, Phone, KeyRound } from "lucide-react";
|
||||||
Mail,
|
import { loginWithEmail, loginWithPhone, sendEmailOTP, sendPhoneOTP, verifyEmail, verifyPhone, getOwnerByUserId, getCustomerByUserId } from "../utils/api";
|
||||||
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";
|
import AuthService from "../services/AuthService";
|
||||||
|
import Background from "../components/Background";
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -57,12 +34,8 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
const newErrors = {};
|
const newErrors = {};
|
||||||
|
|
||||||
if (!formData.credential) {
|
if (!formData.credential) {
|
||||||
newErrors.credential =
|
newErrors.credential = loginMethod === "email" ? t("emailRequired") : t("phoneRequired");
|
||||||
loginMethod === "email"
|
|
||||||
? t("emailRequired")
|
|
||||||
: t("phoneRequired");
|
|
||||||
// } else if (loginMethod === 'email' && !isEmail(formData.credential)) {
|
// } else if (loginMethod === 'email' && !isEmail(formData.credential)) {
|
||||||
// newErrors.credential = 'البريد الإلكتروني غير صالح';
|
// newErrors.credential = 'البريد الإلكتروني غير صالح';
|
||||||
// } else if (loginMethod === 'phone' && !isPhoneNumber(formData.credential)) {
|
// } else if (loginMethod === 'phone' && !isPhoneNumber(formData.credential)) {
|
||||||
@ -90,10 +63,7 @@ export default function LoginPage() {
|
|||||||
const result = await loginFn(formData.credential, formData.password);
|
const result = await loginFn(formData.credential, formData.password);
|
||||||
|
|
||||||
if (result.status === 200) {
|
if (result.status === 200) {
|
||||||
const token =
|
const token = typeof result.data === "string" ? result.data : result.data?.token || result.data?.accessToken;
|
||||||
typeof result.data === "string"
|
|
||||||
? result.data
|
|
||||||
: result.data?.token || result.data?.accessToken;
|
|
||||||
AuthService.addToken(token);
|
AuthService.addToken(token);
|
||||||
|
|
||||||
// Fetch user profile to get full name
|
// Fetch user profile to get full name
|
||||||
@ -105,10 +75,7 @@ export default function LoginPage() {
|
|||||||
const profile = await fetchFn(authUser.id);
|
const profile = await fetchFn(authUser.id);
|
||||||
if (profile) {
|
if (profile) {
|
||||||
AuthService.cacheUser({
|
AuthService.cacheUser({
|
||||||
name:
|
name: profile.fullName || profile.name || `${profile.firstName || ""} ${profile.lastName || ""}`.trim(),
|
||||||
profile.fullName ||
|
|
||||||
profile.name ||
|
|
||||||
`${profile.firstName || ""} ${profile.lastName || ""}`.trim(),
|
|
||||||
email: profile.email || authUser.email,
|
email: profile.email || authUser.email,
|
||||||
phone: profile.phone || profile.phoneNumber || authUser.phone,
|
phone: profile.phone || profile.phoneNumber || authUser.phone,
|
||||||
});
|
});
|
||||||
@ -129,10 +96,7 @@ export default function LoginPage() {
|
|||||||
router.push("/");
|
router.push("/");
|
||||||
}, 1500);
|
}, 1500);
|
||||||
} else if (result.status === 206) {
|
} else if (result.status === 206) {
|
||||||
const tempToken =
|
const tempToken = typeof result.data === "string" ? result.data : result.data?.token || result.data?.accessToken;
|
||||||
typeof result.data === "string"
|
|
||||||
? result.data
|
|
||||||
: result.data?.token || result.data?.accessToken;
|
|
||||||
if (tempToken) {
|
if (tempToken) {
|
||||||
AuthService.addToken(tempToken);
|
AuthService.addToken(tempToken);
|
||||||
}
|
}
|
||||||
@ -169,12 +133,9 @@ export default function LoginPage() {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.error("[Login] Unexpected status:", result.status, result.data);
|
console.error("[Login] Unexpected status:", result.status, result.data);
|
||||||
toast.error(
|
toast.error(result.data?.message || result.data || t("invalidCredentials"), {
|
||||||
result.data?.message || result.data || t("invalidCredentials"),
|
style: { background: "#fee2e2", color: "#991b1b" },
|
||||||
{
|
});
|
||||||
style: { background: "#fee2e2", color: "#991b1b" },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[Login] Error:", err);
|
console.error("[Login] Error:", err);
|
||||||
@ -202,10 +163,7 @@ export default function LoginPage() {
|
|||||||
const result = await verifyFn(otpCode);
|
const result = await verifyFn(otpCode);
|
||||||
|
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
const finalToken =
|
const finalToken = typeof result.data === "string" ? result.data : result.data?.token || result.data?.accessToken;
|
||||||
typeof result.data === "string"
|
|
||||||
? result.data
|
|
||||||
: result.data?.token || result.data?.accessToken;
|
|
||||||
if (finalToken && typeof finalToken === "string") {
|
if (finalToken && typeof finalToken === "string") {
|
||||||
AuthService.addToken(finalToken);
|
AuthService.addToken(finalToken);
|
||||||
}
|
}
|
||||||
@ -286,9 +244,9 @@ export default function LoginPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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 className="min-h-screen bg-gradient-to-br flex items-center justify-center p-4 relative overflow-hidden">
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
<Background />
|
||||||
{/* Particles */}
|
{/* Particles */}
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
{particles.map((p) => (
|
{particles.map((p) => (
|
||||||
@ -328,82 +286,40 @@ export default function LoginPage() {
|
|||||||
transition={{ duration: 15, repeat: Infinity }}
|
transition={{ duration: 15, repeat: Infinity }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<motion.div
|
<motion.div variants={containerVariants} initial="hidden" animate="visible" className="relative w-full max-w-md z-10">
|
||||||
variants={containerVariants}
|
|
||||||
initial="hidden"
|
|
||||||
animate="visible"
|
|
||||||
className="relative w-full max-w-md z-10"
|
|
||||||
>
|
|
||||||
{/* Back link */}
|
{/* Back link */}
|
||||||
<motion.div variants={itemVariants} className="absolute -top-16 left-0">
|
<motion.div variants={itemVariants} className="absolute -top-16 left-0">
|
||||||
<Link
|
<Link href="/" className="group flex items-center gap-2 text-gray-400 hover:text-white transition-colors">
|
||||||
href="/"
|
<motion.div whileHover={{ x: -5 }} transition={{ type: "spring", stiffness: 400 }}>
|
||||||
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" />
|
<ArrowLeft className="w-4 h-4" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<span>{t("backToHome")}</span>
|
<span>{t("backToHome")}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div variants={itemVariants} className="bg-white/10 backdrop-blur-2xl rounded-3xl shadow-2xl border border-white/10 overflow-hidden">
|
||||||
variants={itemVariants}
|
|
||||||
className="bg-white/10 backdrop-blur-2xl rounded-3xl shadow-2xl border border-white/10 overflow-hidden"
|
|
||||||
>
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="bg-gradient-to-r from-amber-500 to-amber-600 p-8 text-center relative overflow-hidden">
|
<div className="bg-gradient-to-r from-amber-500 to-amber-600 p-8 text-center relative overflow-hidden">
|
||||||
<motion.div
|
<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" />
|
||||||
initial={{ scale: 0 }}
|
<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" />
|
||||||
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
|
<motion.div initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} transition={{ delay: 0.2 }} className="relative z-10">
|
||||||
initial={{ y: 20, opacity: 0 }}
|
|
||||||
animate={{ y: 0, opacity: 1 }}
|
|
||||||
transition={{ delay: 0.2 }}
|
|
||||||
className="relative z-10"
|
|
||||||
>
|
|
||||||
<motion.div
|
<motion.div
|
||||||
animate={{ rotate: [0, 10, -10, 0] }}
|
animate={{ rotate: [0, 10, -10, 0] }}
|
||||||
transition={{ duration: 2, repeat: Infinity }}
|
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"
|
className="w-20 h-20 mx-auto mb-4 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-sm"
|
||||||
>
|
>
|
||||||
{step === "otp" ? (
|
{step === "otp" ? <KeyRound className="w-10 h-10 text-white" /> : <Home className="w-10 h-10 text-white" />}
|
||||||
<KeyRound className="w-10 h-10 text-white" />
|
|
||||||
) : (
|
|
||||||
<Home className="w-10 h-10 text-white" />
|
|
||||||
)}
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<h1 className="text-3xl font-bold text-white mb-2">SweetHome</h1>
|
<h1 className="text-3xl font-bold text-white mb-2">SweetHome</h1>
|
||||||
<p className="text-amber-100">
|
<p className="text-amber-100">{step === "otp" ? t("enterOTP") : t("welcomeBack")}</p>
|
||||||
{step === "otp" ? t("enterOTP") : t("welcomeBack")}
|
|
||||||
</p>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-8">
|
<div className="p-8">
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
{step === "login" ? (
|
{step === "login" ? (
|
||||||
<motion.form
|
<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">
|
||||||
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 */}
|
{/* Login method tabs */}
|
||||||
<div className="flex gap-2 bg-white/5 p-1 rounded-xl">
|
<div className="flex gap-2 bg-white/5 p-1 rounded-xl">
|
||||||
<button
|
<button
|
||||||
@ -413,9 +329,7 @@ export default function LoginPage() {
|
|||||||
setFormData({ ...formData, credential: "" });
|
setFormData({ ...formData, credential: "" });
|
||||||
}}
|
}}
|
||||||
className={`flex-1 py-2.5 rounded-lg text-sm font-medium transition-all flex items-center justify-center gap-2 ${
|
className={`flex-1 py-2.5 rounded-lg text-sm font-medium transition-all flex items-center justify-center gap-2 ${
|
||||||
loginMethod === "email"
|
loginMethod === "email" ? "bg-amber-500 text-white shadow-lg" : "text-gray-400 hover:text-white"
|
||||||
? "bg-amber-500 text-white shadow-lg"
|
|
||||||
: "text-gray-400 hover:text-white"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Mail className="w-4 h-4" />
|
<Mail className="w-4 h-4" />
|
||||||
@ -428,9 +342,7 @@ export default function LoginPage() {
|
|||||||
setFormData({ ...formData, credential: "" });
|
setFormData({ ...formData, credential: "" });
|
||||||
}}
|
}}
|
||||||
className={`flex-1 py-2.5 rounded-lg text-sm font-medium transition-all flex items-center justify-center gap-2 ${
|
className={`flex-1 py-2.5 rounded-lg text-sm font-medium transition-all flex items-center justify-center gap-2 ${
|
||||||
loginMethod === "phone"
|
loginMethod === "phone" ? "bg-amber-500 text-white shadow-lg" : "text-gray-400 hover:text-white"
|
||||||
? "bg-amber-500 text-white shadow-lg"
|
|
||||||
: "text-gray-400 hover:text-white"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Phone className="w-4 h-4" />
|
<Phone className="w-4 h-4" />
|
||||||
@ -440,21 +352,13 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
{/* Credential input */}
|
{/* Credential input */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
<label className="block text-sm font-medium text-gray-300 mb-2">{loginMethod === "email" ? t("emailLabel") : t("phoneLabel")}</label>
|
||||||
{loginMethod === "email"
|
|
||||||
? t("emailLabel")
|
|
||||||
: t("phoneLabel")}
|
|
||||||
</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">
|
||||||
{loginMethod === "email" ? (
|
{loginMethod === "email" ? (
|
||||||
<Mail
|
<Mail className={`w-5 h-5 transition-colors ${errors.credential ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
|
||||||
className={`w-5 h-5 transition-colors ${errors.credential ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<Phone
|
<Phone className={`w-5 h-5 transition-colors ${errors.credential ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
|
||||||
className={`w-5 h-5 transition-colors ${errors.credential ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
@ -462,25 +366,15 @@ export default function LoginPage() {
|
|||||||
// type={loginMethod === 'email' ? 'email' : 'tel'}
|
// type={loginMethod === 'email' ? 'email' : 'tel'}
|
||||||
value={formData.credential}
|
value={formData.credential}
|
||||||
onChange={(e) => handleCredentialChange(e.target.value)}
|
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 ${
|
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
|
errors.credential ? "border-red-500" : "border-gray-700"
|
||||||
? "border-red-500"
|
|
||||||
: "border-gray-700"
|
|
||||||
}`}
|
}`}
|
||||||
placeholder={
|
placeholder={loginMethod === "email" ? t("emailPlaceholder") : t("phonePlaceholder")}
|
||||||
loginMethod === "email"
|
|
||||||
? t("emailPlaceholder")
|
|
||||||
: t("phonePlaceholder")
|
|
||||||
}
|
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{errors.credential && (
|
{errors.credential && (
|
||||||
<motion.p
|
<motion.p initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} className="text-red-500 text-sm mt-1">
|
||||||
initial={{ opacity: 0, y: -10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="text-red-500 text-sm mt-1"
|
|
||||||
>
|
|
||||||
{errors.credential}
|
{errors.credential}
|
||||||
</motion.p>
|
</motion.p>
|
||||||
)}
|
)}
|
||||||
@ -488,14 +382,10 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
{/* Password */}
|
{/* Password */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
<label className="block text-sm font-medium text-gray-300 mb-2">{t("password")}</label>
|
||||||
{t("password")}
|
|
||||||
</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">
|
||||||
<Lock
|
<Lock className={`w-5 h-5 transition-colors ${errors.password ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
|
||||||
className={`w-5 h-5 transition-colors ${errors.password ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type={showPassword ? "text" : "password"}
|
type={showPassword ? "text" : "password"}
|
||||||
@ -505,19 +395,14 @@ export default function LoginPage() {
|
|||||||
...formData,
|
...formData,
|
||||||
password: e.target.value,
|
password: e.target.value,
|
||||||
});
|
});
|
||||||
if (errors.password)
|
if (errors.password) setErrors({ ...errors, password: null });
|
||||||
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 ${
|
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"
|
errors.password ? "border-red-500" : "border-gray-700"
|
||||||
}`}
|
}`}
|
||||||
placeholder={t("passwordPlaceholder")}
|
placeholder={t("passwordPlaceholder")}
|
||||||
/>
|
/>
|
||||||
<button
|
<button type="button" onClick={() => setShowPassword(!showPassword)} className="absolute inset-y-0 left-0 pl-3 flex items-center">
|
||||||
type="button"
|
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
|
||||||
className="absolute inset-y-0 left-0 pl-3 flex items-center"
|
|
||||||
>
|
|
||||||
{showPassword ? (
|
{showPassword ? (
|
||||||
<EyeOff className="w-5 h-5 text-gray-400 hover:text-gray-300 transition-colors" />
|
<EyeOff className="w-5 h-5 text-gray-400 hover:text-gray-300 transition-colors" />
|
||||||
) : (
|
) : (
|
||||||
@ -526,11 +411,7 @@ export default function LoginPage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{errors.password && (
|
{errors.password && (
|
||||||
<motion.p
|
<motion.p initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} className="text-red-500 text-sm mt-1">
|
||||||
initial={{ opacity: 0, y: -10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="text-red-500 text-sm mt-1"
|
|
||||||
>
|
|
||||||
{errors.password}
|
{errors.password}
|
||||||
</motion.p>
|
</motion.p>
|
||||||
)}
|
)}
|
||||||
@ -550,14 +431,9 @@ export default function LoginPage() {
|
|||||||
}
|
}
|
||||||
className="w-4 h-4 rounded border-gray-600 bg-white/5 text-amber-500 focus:ring-amber-500 focus:ring-offset-0"
|
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 className="text-sm text-gray-400 group-hover:text-white transition-colors">{t("rememberMe")}</span>
|
||||||
{t("rememberMe")}
|
|
||||||
</span>
|
|
||||||
</label>
|
</label>
|
||||||
<Link
|
<Link href="/forgot-password" className="text-sm text-amber-400 hover:text-amber-300 transition-colors">
|
||||||
href="/forgot-password"
|
|
||||||
className="text-sm text-amber-400 hover:text-amber-300 transition-colors"
|
|
||||||
>
|
|
||||||
{t("forgotPassword")}
|
{t("forgotPassword")}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@ -592,14 +468,7 @@ export default function LoginPage() {
|
|||||||
</motion.form>
|
</motion.form>
|
||||||
) : (
|
) : (
|
||||||
/* OTP Verification Step */
|
/* OTP Verification Step */
|
||||||
<motion.form
|
<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">
|
||||||
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">
|
<div className="text-center mb-4">
|
||||||
<Shield className="w-12 h-12 text-amber-500 mx-auto mb-3" />
|
<Shield className="w-12 h-12 text-amber-500 mx-auto mb-3" />
|
||||||
<p className="text-gray-300 text-sm">
|
<p className="text-gray-300 text-sm">
|
||||||
@ -611,9 +480,7 @@ export default function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
<label className="block text-sm font-medium text-gray-300 mb-2">{t("otpLabel")}</label>
|
||||||
{t("otpLabel")}
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={otpCode}
|
value={otpCode}
|
||||||
@ -629,11 +496,7 @@ export default function LoginPage() {
|
|||||||
dir="ltr"
|
dir="ltr"
|
||||||
/>
|
/>
|
||||||
{otpError && (
|
{otpError && (
|
||||||
<motion.p
|
<motion.p initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} className="text-red-500 text-sm mt-1 text-center">
|
||||||
initial={{ opacity: 0, y: -10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="text-red-500 text-sm mt-1 text-center"
|
|
||||||
>
|
|
||||||
{otpError}
|
{otpError}
|
||||||
</motion.p>
|
</motion.p>
|
||||||
)}
|
)}
|
||||||
@ -678,11 +541,7 @@ export default function LoginPage() {
|
|||||||
>
|
>
|
||||||
← {t("back")}
|
← {t("back")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button" onClick={resendOTP} className="text-amber-400 hover:text-amber-300 transition-colors">
|
||||||
type="button"
|
|
||||||
onClick={resendOTP}
|
|
||||||
className="text-amber-400 hover:text-amber-300 transition-colors"
|
|
||||||
>
|
|
||||||
{t("resendCode")}
|
{t("resendCode")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -690,41 +549,26 @@ export default function LoginPage() {
|
|||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
<motion.p
|
<motion.p variants={itemVariants} className="text-center text-gray-400 mt-6">
|
||||||
variants={itemVariants}
|
|
||||||
className="text-center text-gray-400 mt-6"
|
|
||||||
>
|
|
||||||
{t("noAccount")}{" "}
|
{t("noAccount")}{" "}
|
||||||
<Link
|
<Link href="/auth/choose-role" className="text-amber-400 hover:text-amber-300 font-medium transition-colors">
|
||||||
href="/auth/choose-role"
|
|
||||||
className="text-amber-400 hover:text-amber-300 font-medium transition-colors"
|
|
||||||
>
|
|
||||||
{t("createNewAccount")}
|
{t("createNewAccount")}
|
||||||
</Link>
|
</Link>
|
||||||
</motion.p>
|
</motion.p>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.p
|
<motion.p variants={itemVariants} className="text-center text-gray-500 text-xs mt-4">
|
||||||
variants={itemVariants}
|
|
||||||
className="text-center text-gray-500 text-xs mt-4"
|
|
||||||
>
|
|
||||||
{t("agreeTerms")}{" "}
|
{t("agreeTerms")}{" "}
|
||||||
<Link
|
<Link href="/terms" className="text-amber-400 hover:text-amber-300 transition-colors">
|
||||||
href="/terms"
|
|
||||||
className="text-amber-400 hover:text-amber-300 transition-colors"
|
|
||||||
>
|
|
||||||
{t("termsOfUse")}
|
{t("termsOfUse")}
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
{t("and")}{" "}
|
{t("and")}{" "}
|
||||||
<Link
|
<Link href="/privacy" className="text-amber-400 hover:text-amber-300 transition-colors">
|
||||||
href="/privacy"
|
|
||||||
className="text-amber-400 hover:text-amber-300 transition-colors"
|
|
||||||
>
|
|
||||||
{t("privacyPolicy")}
|
{t("privacyPolicy")}
|
||||||
</Link>
|
</Link>
|
||||||
</motion.p>
|
</motion.p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,32 +1,22 @@
|
|||||||
'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 { useTranslation } from 'react-i18next';
|
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";
|
||||||
Loader2,
|
|
||||||
Star,
|
|
||||||
} 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, isNA }) => (
|
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">{isNA ? 'N/A' : 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>
|
||||||
);
|
);
|
||||||
@ -65,156 +55,93 @@ 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 ?? null,
|
totalRevenue: data.totalRevenue ?? null,
|
||||||
totalReservations: data.totalReservations ?? null,
|
totalReservations: data.totalReservations ?? null,
|
||||||
activeProperties: data.activeProperties ?? null,
|
activeProperties: data.activeProperties ?? null,
|
||||||
|
|
||||||
|
financialRevenue: data.financialRevenue ?? data.totalRevenue ?? null,
|
||||||
financialRevenue:
|
|
||||||
data.financialRevenue ?? data.totalRevenue ?? null,
|
|
||||||
|
|
||||||
financialCommission: data.financialCommission ?? null,
|
financialCommission: data.financialCommission ?? null,
|
||||||
financialBalance: data.financialBalance ?? null,
|
financialBalance: data.financialBalance ?? null,
|
||||||
directRevenue: data.directRevenue ?? null,
|
directRevenue: data.directRevenue ?? null,
|
||||||
directCommission: data.directCommission ?? null,
|
directCommission: data.directCommission ?? null,
|
||||||
directBalance: data.directBalance ?? null,
|
directBalance: data.directBalance ?? null,
|
||||||
directRating: data.directRating ?? null,
|
directRating: data.directRating ?? null,
|
||||||
directPostponements: data.directPostponements ?? null,
|
directPostponements: data.directPostponements ?? null,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error(t("accountBook.loadError"));
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
toast.error(t('accountBook.loadError'));
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetchData();
|
fetchData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const formatCurrency = (amount) => {
|
const formatCurrency = (amount) => {
|
||||||
const num = Number(amount);
|
const num = Number(amount);
|
||||||
if (isNaN(num)) return 'N/A';
|
if (isNaN(num)) return "N/A";
|
||||||
return num.toLocaleString() + ' ' + t('currency-syp-suffix');
|
return num.toLocaleString() + " " + t("currency-syp-suffix");
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatNumber = (val) => {
|
const formatNumber = (val) => {
|
||||||
const num = Number(val);
|
const num = Number(val);
|
||||||
if (isNaN(num)) return 'N/A';
|
if (isNaN(num)) return "N/A";
|
||||||
return num.toLocaleString();
|
return num.toLocaleString();
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatRating = (val) => {
|
const formatRating = (val) => {
|
||||||
const num = Number(val);
|
const num = Number(val);
|
||||||
if (isNaN(num)) return 'N/A';
|
if (isNaN(num)) return "N/A";
|
||||||
return num.toFixed(1);
|
return num.toFixed(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isNA = (val) => val === null || val === undefined || val === '';
|
const isNA = (val) => val === null || val === undefined || val === "";
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Loader2 className="w-12 h-12 text-amber-500 animate-spin mx-auto mb-4" />
|
<Loader2 className="w-12 h-12 text-amber-500 animate-spin mx-auto mb-4" />
|
||||||
<p className="text-gray-600">{t('accountBook.loading')}</p>
|
<p className="text-gray-600">{t("accountBook.loading")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 py-8" dir={i18n.language === 'ar' ? 'rtl' : 'ltr'}>
|
<div className="min-h-screen bg-gray-50 py-8" dir={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="mb-8"
|
|
||||||
>
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900">{t('accountBook')}</h1>
|
|
||||||
<p className="text-gray-600">{t('financialRecords')}</p>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="mb-8">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
<h2 className="text-xl font-bold text-gray-900 mb-4">{t("accountBook.ownerFinancialOverview")}</h2>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="mb-8"
|
|
||||||
>
|
|
||||||
<h2 className="text-xl font-bold text-gray-900 mb-4">{t('accountBook.ownerFinancialOverview')}</h2>
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
|
||||||
<StatCard
|
<StatCard title={t("accountBook.revenue")} value={formatCurrency(stats.financialRevenue)} icon={TrendingUp} color="bg-emerald-500" isNA={isNA(stats.financialRevenue)} />
|
||||||
title={t('accountBook.revenue')}
|
<StatCard title={t("accountBook.commission")} value={formatCurrency(stats.financialCommission)} icon={DollarSign} color="bg-orange-500" isNA={isNA(stats.financialCommission)} />
|
||||||
value={formatCurrency(stats.financialRevenue)}
|
<StatCard title={t("accountBook.remainingBalance")} value={formatCurrency(stats.financialBalance)} icon={DollarSign} color="bg-blue-500" isNA={isNA(stats.financialBalance)} />
|
||||||
icon={TrendingUp}
|
|
||||||
color="bg-emerald-500"
|
|
||||||
isNA={isNA(stats.financialRevenue)}
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
title={t('accountBook.commission')}
|
|
||||||
value={formatCurrency(stats.financialCommission)}
|
|
||||||
icon={DollarSign}
|
|
||||||
color="bg-orange-500"
|
|
||||||
isNA={isNA(stats.financialCommission)}
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
title={t('accountBook.remainingBalance')}
|
|
||||||
value={formatCurrency(stats.financialBalance)}
|
|
||||||
icon={DollarSign}
|
|
||||||
color="bg-blue-500"
|
|
||||||
isNA={isNA(stats.financialBalance)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.1 }} className="mb-8">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
<h2 className="text-xl font-bold text-gray-900 mb-4">{t("accountBook.directPropertyStats")}</h2>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.1 }}
|
|
||||||
className="mb-8"
|
|
||||||
>
|
|
||||||
<h2 className="text-xl font-bold text-gray-900 mb-4">{t('accountBook.directPropertyStats')}</h2>
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-6">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-6">
|
||||||
<StatCard
|
<StatCard title={t("accountBook.revenue")} value={formatCurrency(stats.directRevenue)} icon={TrendingUp} color="bg-emerald-500" isNA={isNA(stats.directRevenue)} />
|
||||||
title={t('accountBook.revenue')}
|
<StatCard title={t("accountBook.commission")} value={formatCurrency(stats.directCommission)} icon={DollarSign} color="bg-orange-500" isNA={isNA(stats.directCommission)} />
|
||||||
value={formatCurrency(stats.directRevenue)}
|
<StatCard title={t("accountBook.remainingBalance")} value={formatCurrency(stats.directBalance)} icon={DollarSign} color="bg-blue-500" isNA={isNA(stats.directBalance)} />
|
||||||
icon={TrendingUp}
|
<StatCard title={t("accountBook.overallRating")} value={formatRating(stats.directRating)} icon={Star} color="bg-purple-500" isNA={isNA(stats.directRating)} />
|
||||||
color="bg-emerald-500"
|
<StatCard title={t("accountBook.postponements")} value={formatNumber(stats.directPostponements)} icon={Calendar} color="bg-red-500" isNA={isNA(stats.directPostponements)} />
|
||||||
isNA={isNA(stats.directRevenue)}
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
title={t('accountBook.commission')}
|
|
||||||
value={formatCurrency(stats.directCommission)}
|
|
||||||
icon={DollarSign}
|
|
||||||
color="bg-orange-500"
|
|
||||||
isNA={isNA(stats.directCommission)}
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
title={t('accountBook.remainingBalance')}
|
|
||||||
value={formatCurrency(stats.directBalance)}
|
|
||||||
icon={DollarSign}
|
|
||||||
color="bg-blue-500"
|
|
||||||
isNA={isNA(stats.directBalance)}
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
title={t('accountBook.overallRating')}
|
|
||||||
value={formatRating(stats.directRating)}
|
|
||||||
icon={Star}
|
|
||||||
color="bg-purple-500"
|
|
||||||
isNA={isNA(stats.directRating)}
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
title={t('accountBook.postponements')}
|
|
||||||
value={formatNumber(stats.directPostponements)}
|
|
||||||
icon={Calendar}
|
|
||||||
color="bg-red-500"
|
|
||||||
isNA={isNA(stats.directPostponements)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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,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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
605
app/page.js
605
app/page.js
@ -1,600 +1,9 @@
|
|||||||
'use client';
|
import HomeClient from "./components/home/HomeClient";
|
||||||
|
|
||||||
import { useState, useRef, useEffect } from 'react';
|
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import {
|
|
||||||
ShieldCheck,
|
|
||||||
Lock,
|
|
||||||
Zap,
|
|
||||||
Star,
|
|
||||||
Rocket,
|
|
||||||
Search,
|
|
||||||
MapPin,
|
|
||||||
Home,
|
|
||||||
DollarSign,
|
|
||||||
ChevronDown,
|
|
||||||
Shield,
|
|
||||||
Award,
|
|
||||||
Sparkles,
|
|
||||||
UserCircle,
|
|
||||||
LogOut,
|
|
||||||
Calendar,
|
|
||||||
Building,
|
|
||||||
PlusCircle,
|
|
||||||
Heart,
|
|
||||||
MessageCircle
|
|
||||||
} from 'lucide-react';
|
|
||||||
import HeroSearch from './components/home/HeroSearch';
|
|
||||||
import PropertyMapWithMarkers from './components/PropertyMapWithMarkers';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import Image from 'next/image';
|
|
||||||
import { getRentProperties, getSaleProperties } from './utils/api';
|
|
||||||
import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from './enums';
|
|
||||||
import AuthService from './services/AuthService';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
function mapApiProperty(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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: "الصفحة الرئيسية | عقاراتك",
|
||||||
|
description: "ابحث عن العقارات والبيوت للإيجار والبيع بأسهل الطرق.",
|
||||||
|
};
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const { t } = useTranslation();
|
return <HomeClient />;
|
||||||
const mapSectionRef = useRef(null);
|
}
|
||||||
const [searchFilters, setSearchFilters] = useState(null);
|
|
||||||
const [showMap, setShowMap] = useState(false);
|
|
||||||
const [filteredProperties, setFilteredProperties] = useState([]);
|
|
||||||
const [isScrolling, setIsScrolling] = useState(false);
|
|
||||||
const [user, setUser] = useState(null);
|
|
||||||
const [showUserMenu, setShowUserMenu] = useState(false);
|
|
||||||
const menuRef = useRef(null);
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const [allProperties, setAllProperties] = useState([]);
|
|
||||||
const [rentProperties, setRentProperties] = useState([]);
|
|
||||||
const [saleProperties, setSaleProperties] = useState([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const authUser = AuthService.getUser();
|
|
||||||
if (authUser) {
|
|
||||||
setUser({
|
|
||||||
name: authUser.name || authUser.email,
|
|
||||||
email: authUser.email,
|
|
||||||
role: AuthService.isOwner() ? 'owner' : 'customer',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setUser(null);
|
|
||||||
}
|
|
||||||
}, [pathname]);
|
|
||||||
|
|
||||||
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 {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchProperties();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (searchFilters) {
|
|
||||||
applyFilters(searchFilters);
|
|
||||||
}
|
|
||||||
}, [rentProperties, saleProperties, searchFilters]);
|
|
||||||
|
|
||||||
const logout = () => {
|
|
||||||
AuthService.deleteToken();
|
|
||||||
setUser(null);
|
|
||||||
setShowUserMenu(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
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 > 50) return false; break;
|
|
||||||
case '500-1000': if (priceUSD < 51 || priceUSD > 100) return false; break;
|
|
||||||
case '1000-2000': if (priceUSD < 101 || priceUSD > 200) return false; break;
|
|
||||||
case '2000-3000': if (priceUSD < 201 || priceUSD > 300) return false; break;
|
|
||||||
case '3000+': if (priceUSD < 301) 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);
|
|
||||||
|
|
||||||
if (!showMap) {
|
|
||||||
setShowMap(true);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (mapSectionRef.current) {
|
|
||||||
setIsScrolling(true);
|
|
||||||
mapSectionRef.current.scrollIntoView({
|
|
||||||
behavior: 'smooth',
|
|
||||||
block: 'center'
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => setIsScrolling(false), 1000);
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
} else {
|
|
||||||
if (mapSectionRef.current) {
|
|
||||||
setIsScrolling(true);
|
|
||||||
mapSectionRef.current.scrollIntoView({
|
|
||||||
behavior: 'smooth',
|
|
||||||
block: 'center'
|
|
||||||
});
|
|
||||||
setTimeout(() => setIsScrolling(false), 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetSearch = () => {
|
|
||||||
setShowMap(false);
|
|
||||||
setSearchFilters(null);
|
|
||||||
setFilteredProperties([]);
|
|
||||||
window.scrollTo({
|
|
||||||
top: 0,
|
|
||||||
behavior: 'smooth'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getUserInitial = () => {
|
|
||||||
if (user?.name) {
|
|
||||||
return user.name.charAt(0).toUpperCase();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isOwner = user?.role === 'owner';
|
|
||||||
|
|
||||||
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">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{!isOwner && <HeroSearch onSearch={applyFilters} isAuthenticated={!!user} />}
|
|
||||||
|
|
||||||
{isOwner && (
|
|
||||||
<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")} {user?.name}!
|
|
||||||
</h2>
|
|
||||||
<p className="text-amber-50/80 mb-4">
|
|
||||||
{t("ownerDescription")}
|
|
||||||
</p>
|
|
||||||
<Link
|
|
||||||
href="/owner/properties"
|
|
||||||
className="inline-flex items-center gap-2 bg-amber-500 text-white px-6 py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors"
|
|
||||||
>
|
|
||||||
<Building className="w-5 h-5" />
|
|
||||||
{t("manageMyProperties")}
|
|
||||||
</Link>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!showMap && !isOwner && (
|
|
||||||
<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-white" 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>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{!isOwner && (<>
|
|
||||||
<AnimatePresence mode="wait">
|
|
||||||
{showMap && (
|
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</>)}
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.6 }}
|
|
||||||
className="text-center mb-12"
|
|
||||||
>
|
|
||||||
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4 tracking-tight">
|
|
||||||
{t('whyChooseUsTitle')}
|
|
||||||
</h2>
|
|
||||||
<p className="text-gray-600 max-w-2xl mx-auto text-lg">
|
|
||||||
{t('whyChooseUsSubtitle')}
|
|
||||||
</p>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
|
|
||||||
<motion.div
|
|
||||||
className="group bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100"
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.5, delay: 0.1 }}
|
|
||||||
whileHover={{ y: -4 }}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-4 mb-4">
|
|
||||||
<div className="w-12 h-12 bg-amber-100 rounded-xl flex items-center justify-center group-hover:bg-amber-200 transition-colors duration-300">
|
|
||||||
<ShieldCheck className="w-6 h-6 text-amber-600" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-lg font-bold text-gray-900">
|
|
||||||
{t('feature1Title')}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<p className="text-gray-600 text-sm leading-relaxed">
|
|
||||||
{t('feature1Description')}
|
|
||||||
</p>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
className="group bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100"
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.5, delay: 0.2 }}
|
|
||||||
whileHover={{ y: -4 }}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-4 mb-4">
|
|
||||||
<div className="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center group-hover:bg-blue-200 transition-colors duration-300">
|
|
||||||
<Lock className="w-6 h-6 text-blue-600" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-lg font-bold text-gray-900">
|
|
||||||
{t('feature2Title')}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<p className="text-gray-600 text-sm leading-relaxed">
|
|
||||||
{t('feature2Description')}
|
|
||||||
</p>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
className="group bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100"
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.5, delay: 0.3 }}
|
|
||||||
whileHover={{ y: -4 }}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-4 mb-4">
|
|
||||||
<div className="w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center group-hover:bg-green-200 transition-colors duration-300">
|
|
||||||
<Zap className="w-6 h-6 text-green-600" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-lg font-bold text-gray-900">
|
|
||||||
{t('feature3Title')}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<p className="text-gray-600 text-sm leading-relaxed">
|
|
||||||
{t('feature3Description')}
|
|
||||||
</p>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{filteredProperties.length > 0 && searchFilters && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.4, type: "spring" }}
|
|
||||||
className="flex flex-wrap items-center justify-center gap-3"
|
|
||||||
>
|
|
||||||
<div className="bg-white px-4 py-2 rounded-full shadow-sm border border-gray-200 text-sm">
|
|
||||||
<span className="text-gray-600">{t('filterPropertyTypeLabel')}</span>
|
|
||||||
<span className="font-bold text-gray-900">
|
|
||||||
{searchFilters.propertyType === 'all' ? t('all') :
|
|
||||||
searchFilters.propertyType === 'apartment' ? t('buildingType.apartment') :
|
|
||||||
searchFilters.propertyType === 'villa' ? t('buildingType.villa') : t('house')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white px-4 py-2 rounded-full shadow-sm border border-gray-200 text-sm">
|
|
||||||
<span className="text-gray-600">{t('filterPriceRangeLabel')}</span>
|
|
||||||
<span className="font-bold text-gray-900">
|
|
||||||
{searchFilters.priceRange === 'all' ? t('allPrices') :
|
|
||||||
searchFilters.priceRange === '0-500' ? t('priceRangeUnder500') :
|
|
||||||
searchFilters.priceRange === '500-1000' ? t('priceRange50to100') :
|
|
||||||
searchFilters.priceRange === '1000-2000' ? t('priceRange100to200') :
|
|
||||||
searchFilters.priceRange === '2000-3000' ? t('priceRange200to300') : t('priceRangeOver300')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white px-4 py-2 rounded-full shadow-sm border border-gray-200 text-sm">
|
|
||||||
<span className="text-gray-600">{t('filterSourceLabel')}</span>
|
|
||||||
<span className="font-bold text-gray-900">
|
|
||||||
{searchFilters.ownerSource === 'all' ? t('all') :
|
|
||||||
searchFilters.ownerSource === 'owner' ? t('fromOwner') : t('fromAgency')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white px-4 py-2 rounded-full shadow-sm border border-gray-200 text-sm">
|
|
||||||
<span className="text-gray-600">{t('filterRentTypeLabel')}</span>
|
|
||||||
<span className="font-bold text-gray-900">
|
|
||||||
{searchFilters.rentPeriod === 'all' ? t('all') :
|
|
||||||
searchFilters.rentPeriod === 'daily' ? t('dailyRent') : t('monthlyRent')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{searchFilters.availableToday && (
|
|
||||||
<div className="bg-white px-4 py-2 rounded-full shadow-sm border border-gray-200 text-sm">
|
|
||||||
<span className="font-bold text-gray-900">{t('availableFromToday')}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
|
|
||||||
export default function Loading() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 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-500 text-lg">جاري التحميل...</p>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
|
|
||||||
export default function Loading() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 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-500 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-gray-50 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-500 text-lg">جاري التحميل...</p>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import PropertyDetail from './PropertyDetail';
|
import PropertyDetail from "./PropertyDetail";
|
||||||
|
|
||||||
// Server-side API fetch for metadata (runs at request time on server)
|
// Server-side API fetch for metadata (runs at request time on server)
|
||||||
async function fetchPropertyForMeta(id) {
|
async function fetchPropertyForMeta(id) {
|
||||||
@ -10,7 +10,7 @@ async function fetchPropertyForMeta(id) {
|
|||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
const json = JSON.parse(text);
|
const json = JSON.parse(text);
|
||||||
const items = Array.isArray(json?.data) ? json.data : Array.isArray(json) ? json : [];
|
const items = Array.isArray(json?.data) ? json.data : Array.isArray(json) ? json : [];
|
||||||
return items.find(p => p.id == id) || items[0] || null;
|
return items.find((p) => p.id == id) || items[0] || null;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -19,26 +19,28 @@ async function fetchPropertyForMeta(id) {
|
|||||||
function mapProperty(item) {
|
function mapProperty(item) {
|
||||||
const info = item.propertyInformation || item.PropertyInformation || {};
|
const info = item.propertyInformation || item.PropertyInformation || {};
|
||||||
let details = {};
|
let details = {};
|
||||||
if (typeof info.detailsJSON === 'object' && info.detailsJSON) {
|
if (typeof info.detailsJSON === "object" && info.detailsJSON) {
|
||||||
details = info.detailsJSON;
|
details = info.detailsJSON;
|
||||||
} else if (typeof info.DetailsJSON === 'object' && info.DetailsJSON) {
|
} else if (typeof info.DetailsJSON === "object" && info.DetailsJSON) {
|
||||||
details = info.DetailsJSON;
|
details = info.DetailsJSON;
|
||||||
} else {
|
} else {
|
||||||
try { details = JSON.parse(info.detailsJSON || info.DetailsJSON || '{}'); } catch {}
|
try {
|
||||||
|
details = JSON.parse(info.detailsJSON || info.DetailsJSON || "{}");
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const price = item.monthlyRent || item.MonthlyRent || item.dailyRent || item.DailyRent || 0;
|
const price = item.monthlyRent || item.MonthlyRent || item.dailyRent || item.DailyRent || 0;
|
||||||
const priceUnit = item.monthlyRent || item.MonthlyRent ? 'monthly' : 'daily';
|
const priceUnit = item.monthlyRent || item.MonthlyRent ? "monthly" : "daily";
|
||||||
const buildingType = info.buildingType ?? info.BuildingType ?? 0;
|
const buildingType = info.buildingType ?? info.BuildingType ?? 0;
|
||||||
const type = { 0: 'apartment', 1: 'villa', 2: 'house' }[buildingType] || 'apartment';
|
const type = { 0: "apartment", 1: "villa", 2: "house" }[buildingType] || "apartment";
|
||||||
const typeLabel = { 0: 'apartment', 1: 'villa', 2: 'house' }[buildingType] || 'apartment';
|
const typeLabel = { 0: "apartment", 1: "villa", 2: "house" }[buildingType] || "apartment";
|
||||||
const address = info.address || info.Address || '';
|
const address = info.address || info.Address || "";
|
||||||
const bedrooms = info.numberOfBedRooms || info.NumberOfBedRooms || 0;
|
const bedrooms = info.numberOfBedRooms || info.NumberOfBedRooms || 0;
|
||||||
const bathrooms = info.numberOfBathRooms || info.NumberOfBathRooms || 0;
|
const bathrooms = info.numberOfBathRooms || info.NumberOfBathRooms || 0;
|
||||||
const area = info.space || info.Space || 0;
|
const area = info.space || info.Space || 0;
|
||||||
const desc = info.description || info.Description || '';
|
const desc = info.description || info.Description || "";
|
||||||
const images = info.images || info.Images || [];
|
const images = info.images || info.Images || [];
|
||||||
const firstImage = Array.isArray(images) && images[0] ? images[0] : '';
|
const firstImage = Array.isArray(images) && images[0] ? images[0] : "";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: `${typeLabel} في ${address}`,
|
title: `${typeLabel} في ${address}`,
|
||||||
@ -60,21 +62,22 @@ export async function generateMetadata({ params }) {
|
|||||||
|
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
return {
|
return {
|
||||||
title: 'SweetHome - عقار',
|
title: "SweetHome - عقار",
|
||||||
description: 'اكتشف أفضل العقارات للإيجار',
|
description: "اكتشف أفضل العقارات للإيجار",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const p = mapProperty(raw);
|
const p = mapProperty(raw);
|
||||||
const priceStr = `${p.price.toLocaleString()} ل.س / ${p.priceUnit === 'daily' ? 'يوم' : 'شهر'}`;
|
const priceStr = `${p.price.toLocaleString()} ل.س / ${p.priceUnit === "daily" ? "يوم" : "شهر"}`;
|
||||||
const propertyImage = p.image
|
const propertyImage = p.image ? (p.image.startsWith("http") ? p.image : `http://45.93.137.91${p.image}`) : "";
|
||||||
? (p.image.startsWith('http') ? p.image : `http://45.93.137.91${p.image}`)
|
|
||||||
: '';
|
|
||||||
const logoUrl = `http://45.93.137.91/logo.png`;
|
const logoUrl = `http://45.93.137.91/logo.png`;
|
||||||
|
|
||||||
// Use property image if available, otherwise logo
|
// Use property image if available, otherwise logo
|
||||||
const ogImages = propertyImage
|
const ogImages = propertyImage
|
||||||
? [{ url: propertyImage, width: 1200, height: 630 }, { url: logoUrl, width: 512, height: 512 }]
|
? [
|
||||||
|
{ url: propertyImage, width: 1200, height: 630 },
|
||||||
|
{ url: logoUrl, width: 512, height: 512 },
|
||||||
|
]
|
||||||
: [{ url: logoUrl, width: 512, height: 512 }];
|
: [{ url: logoUrl, width: 512, height: 512 }];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -85,14 +88,14 @@ export async function generateMetadata({ params }) {
|
|||||||
description: p.description,
|
description: p.description,
|
||||||
images: ogImages,
|
images: ogImages,
|
||||||
url: `http://45.93.137.91/property/${id}`,
|
url: `http://45.93.137.91/property/${id}`,
|
||||||
type: 'website',
|
type: "website",
|
||||||
siteName: 'SweetHome',
|
siteName: "SweetHome",
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: 'summary_large_image',
|
card: "summary_large_image",
|
||||||
title: `${p.title} - ${priceStr}`,
|
title: `${p.title} - ${priceStr}`,
|
||||||
description: p.description,
|
description: p.description,
|
||||||
images: ogImages.map(i => i.url),
|
images: ogImages.map((i) => i.url),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -979,7 +979,7 @@ export default function AgentRegisterPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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 className="min-h-screen bg-gradient-to-br flex items-center justify-center p-4 relative overflow-hidden">
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
<div className="absolute inset-0 overflow-hidden">{backgroundElements}</div>
|
<div className="absolute inset-0 overflow-hidden">{backgroundElements}</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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -229,7 +229,7 @@ export default function OwnerRegisterPage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
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 className="min-h-screen bg-gradient-to-br flex items-center justify-center p-4 relative overflow-hidden">
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
<div className="absolute inset-0 overflow-hidden">{backgroundElements}</div>
|
<div className="absolute inset-0 overflow-hidden">{backgroundElements}</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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -205,7 +205,7 @@ export default function TenantRegisterPage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
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 className="min-h-screen bg-gradient-to-br flex items-center justify-center p-4 relative overflow-hidden">
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
<div className="absolute inset-0 overflow-hidden">{backgroundElements}</div>
|
<div className="absolute inset-0 overflow-hidden">{backgroundElements}</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import Cookies from "js-cookie";
|
||||||
/**
|
/**
|
||||||
* AuthService
|
* AuthService
|
||||||
* Manages authentication tokens and user role detection via JWT decoding.
|
* Manages authentication tokens and user role detection via JWT decoding.
|
||||||
@ -20,22 +21,28 @@
|
|||||||
* isAuthenticated() — check if token exists
|
* isAuthenticated() — check if token exists
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const TOKEN_KEY = 'auth_token';
|
const TOKEN_KEY = "auth_token";
|
||||||
const USER_KEY = 'cached_user';
|
const USER_KEY = "cached_user";
|
||||||
|
|
||||||
const AuthService = Object.freeze({
|
const AuthService = Object.freeze({
|
||||||
addToken(token) {
|
addToken(token) {
|
||||||
if (!token || typeof token !== 'string') return;
|
if (!token || typeof token !== "string") return;
|
||||||
localStorage.setItem(TOKEN_KEY, token);
|
Cookies.set(TOKEN_KEY, token, {
|
||||||
|
expires: 7,
|
||||||
|
secure: true,
|
||||||
|
sameSite: "lax",
|
||||||
|
path: "/",
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getToken() {
|
getToken() {
|
||||||
return localStorage.getItem(TOKEN_KEY);
|
if (typeof window === "undefined") return null;
|
||||||
|
return Cookies.get(TOKEN_KEY);
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteToken() {
|
deleteToken() {
|
||||||
localStorage.removeItem(TOKEN_KEY);
|
|
||||||
localStorage.removeItem(USER_KEY);
|
localStorage.removeItem(USER_KEY);
|
||||||
|
Cookies.remove(TOKEN_KEY);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -67,7 +74,7 @@ const AuthService = Object.freeze({
|
|||||||
const token = this.getToken();
|
const token = this.getToken();
|
||||||
if (!token) return null;
|
if (!token) return null;
|
||||||
try {
|
try {
|
||||||
const payload = token.split('.')[1];
|
const payload = token.split(".")[1];
|
||||||
return JSON.parse(atob(payload));
|
return JSON.parse(atob(payload));
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
@ -85,10 +92,10 @@ const AuthService = Object.freeze({
|
|||||||
const cached = this.getCachedUser();
|
const cached = this.getCachedUser();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: payload['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'] || payload.sub || null,
|
id: payload["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"] || payload.sub || null,
|
||||||
name: cached?.name || payload['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'] || null,
|
name: cached?.name || payload["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"] || null,
|
||||||
email: cached?.email || payload['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'] || null,
|
email: cached?.email || payload["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"] || null,
|
||||||
phone: cached?.phone || payload['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/mobilephone'] || null,
|
phone: cached?.phone || payload["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/mobilephone"] || null,
|
||||||
roles: this.getRoles(),
|
roles: this.getRoles(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -112,9 +119,9 @@ const AuthService = Object.freeze({
|
|||||||
getRoles() {
|
getRoles() {
|
||||||
const payload = this.decodeToken();
|
const payload = this.decodeToken();
|
||||||
if (!payload) return [];
|
if (!payload) return [];
|
||||||
const roles = payload['http://schemas.microsoft.com/ws/2008/06/identity/claims/role'];
|
const roles = payload["http://schemas.microsoft.com/ws/2008/06/identity/claims/role"];
|
||||||
if (Array.isArray(roles)) return roles;
|
if (Array.isArray(roles)) return roles;
|
||||||
if (typeof roles === 'string') return [roles];
|
if (typeof roles === "string") return [roles];
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -123,7 +130,7 @@ const AuthService = Object.freeze({
|
|||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
isOwner() {
|
isOwner() {
|
||||||
return this.getRoles().includes('Owner');
|
return this.getRoles().includes("Owner");
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -131,7 +138,7 @@ const AuthService = Object.freeze({
|
|||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
isAgent() {
|
isAgent() {
|
||||||
return this.getRoles().includes('RealEstateAgent');
|
return this.getRoles().includes("RealEstateAgent");
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -160,4 +167,3 @@ const AuthService = Object.freeze({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default AuthService;
|
export default AuthService;
|
||||||
|
|
||||||
|
|||||||
@ -1,16 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import {
|
import { MessageCircle, Mail, Phone, MapPin, Send, Loader2 } from "lucide-react";
|
||||||
MessageCircle,
|
|
||||||
Mail,
|
|
||||||
Phone,
|
|
||||||
MapPin,
|
|
||||||
Send,
|
|
||||||
Loader2,
|
|
||||||
} from "lucide-react";
|
|
||||||
import toast, { Toaster } from "react-hot-toast";
|
import toast, { Toaster } from "react-hot-toast";
|
||||||
import { submitReport } from "../utils/api";
|
import { submitReport } from "../utils/api";
|
||||||
import AuthService from "../services/AuthService";
|
import AuthService from "../services/AuthService";
|
||||||
@ -24,84 +17,62 @@ export default function SupportPage() {
|
|||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!subject.trim() || !body.trim()) {
|
if (!subject.trim() || !body.trim()) {
|
||||||
toast.error(t('support.fillAllFields'));
|
toast.error(t("support.fillAllFields"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!AuthService.isAuthenticated()) {
|
if (!AuthService.isAuthenticated()) {
|
||||||
toast.error(t('auth.loginRequired'));
|
toast.error(t("auth.loginRequired"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
await submitReport(subject, body);
|
await submitReport(subject, body);
|
||||||
toast.success(t('support.submittedSuccess'));
|
toast.success(t("support.submittedSuccess"));
|
||||||
setSubject("");
|
setSubject("");
|
||||||
setBody("");
|
setBody("");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(t('support.submitError'));
|
toast.error(t("support.submitError"));
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12" dir={i18n.language === "ar" ? "rtl" : "ltr"}>
|
||||||
className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12"
|
|
||||||
dir={i18n.language === 'ar' ? 'rtl' : 'ltr'}
|
|
||||||
>
|
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
<div className="container mx-auto px-4 max-w-5xl">
|
<div className="container mx-auto px-4 max-w-5xl">
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="text-center mb-12">
|
||||||
initial={{ opacity: 0, y: -20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="text-center mb-12"
|
|
||||||
>
|
|
||||||
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-amber-100">
|
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-amber-100">
|
||||||
<MessageCircle className="w-10 h-10 text-amber-600" />
|
<MessageCircle className="w-10 h-10 text-amber-600" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-4xl font-bold text-gray-900 mb-4">
|
<h1 className="text-4xl font-bold text-gray-900 mb-4">{t("support.customerService")}</h1>
|
||||||
{t('support.customerService')}
|
<p className="text-lg text-gray-600 max-w-2xl mx-auto">{t("support.hereToHelp")}</p>
|
||||||
</h1>
|
|
||||||
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
|
||||||
{t('support.hereToHelp')}
|
|
||||||
</p>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-3 gap-8">
|
<div className="grid md:grid-cols-3 gap-8">
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: 0.1 }} className="md:col-span-2">
|
||||||
initial={{ opacity: 0, x: -20 }}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
transition={{ delay: 0.1 }}
|
|
||||||
className="md:col-span-2"
|
|
||||||
>
|
|
||||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-8">
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-8">
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-6">
|
<h2 className="text-2xl font-bold text-gray-900 mb-6">{t("support.sendMessage")}</h2>
|
||||||
{t('support.sendMessage')}
|
|
||||||
</h2>
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
<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("support.subjectLabel")}</label>
|
||||||
{t('support.subjectLabel')}
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={subject}
|
value={subject}
|
||||||
onChange={(e) => setSubject(e.target.value)}
|
onChange={(e) => setSubject(e.target.value)}
|
||||||
placeholder={t('support.subjectPlaceholder')}
|
placeholder={t("support.subjectPlaceholder")}
|
||||||
className="w-full px-4 py-3 bg-white border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400"
|
className="w-full px-4 py-3 bg-white border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<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("support.messageLabel")}</label>
|
||||||
{t('support.messageLabel')}
|
|
||||||
</label>
|
|
||||||
<textarea
|
<textarea
|
||||||
value={body}
|
value={body}
|
||||||
onChange={(e) => setBody(e.target.value)}
|
onChange={(e) => setBody(e.target.value)}
|
||||||
rows={6}
|
rows={6}
|
||||||
placeholder={t('support.messagePlaceholder')}
|
placeholder={t("support.messagePlaceholder")}
|
||||||
className="w-full px-4 py-3 bg-white border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400 resize-none"
|
className="w-full px-4 py-3 bg-white border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400 resize-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -110,37 +81,24 @@ export default function SupportPage() {
|
|||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
className="w-full py-3 bg-amber-500 text-white rounded-xl font-medium hover:bg-amber-600 transition-colors flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed shadow-sm"
|
className="w-full py-3 bg-amber-500 text-white rounded-xl font-medium hover:bg-amber-600 transition-colors flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed shadow-sm"
|
||||||
>
|
>
|
||||||
{isSubmitting ? (
|
{isSubmitting ? <Loader2 className="w-5 h-5 animate-spin" /> : <Send className="w-5 h-5" />}
|
||||||
<Loader2 className="w-5 h-5 animate-spin" />
|
{isSubmitting ? t("support.sending") : t("support.send")}
|
||||||
) : (
|
|
||||||
<Send className="w-5 h-5" />
|
|
||||||
)}
|
|
||||||
{isSubmitting ? t('support.sending') : t('support.send')}
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: 0.2 }} className="space-y-6">
|
||||||
initial={{ opacity: 0, x: 20 }}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
transition={{ delay: 0.2 }}
|
|
||||||
className="space-y-6"
|
|
||||||
>
|
|
||||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
|
||||||
<h3 className="text-lg font-bold text-gray-900 mb-4">
|
<h3 className="text-lg font-bold text-gray-900 mb-4">{t("support.contactInfo")}</h3>
|
||||||
{t('support.contactInfo')}
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-10 h-10 bg-blue-100 rounded-xl flex items-center justify-center shrink-0">
|
<div className="w-10 h-10 bg-blue-100 rounded-xl flex items-center justify-center shrink-0">
|
||||||
<Mail className="w-5 h-5 text-blue-600" />
|
<Mail className="w-5 h-5 text-blue-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-500">{t('support.email')}</p>
|
<p className="text-sm text-gray-500">{t("support.email")}</p>
|
||||||
<p className="font-medium text-gray-900">
|
<p className="font-medium text-gray-900">support@sweethome.com</p>
|
||||||
support@sweethome.com
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@ -148,7 +106,7 @@ export default function SupportPage() {
|
|||||||
<Phone className="w-5 h-5 text-green-600" />
|
<Phone className="w-5 h-5 text-green-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-500">{t('support.phone')}</p>
|
<p className="text-sm text-gray-500">{t("support.phone")}</p>
|
||||||
<p className="font-medium text-gray-900" dir="ltr">
|
<p className="font-medium text-gray-900" dir="ltr">
|
||||||
+963 11 234 5678
|
+963 11 234 5678
|
||||||
</p>
|
</p>
|
||||||
@ -159,25 +117,23 @@ export default function SupportPage() {
|
|||||||
<MapPin className="w-5 h-5 text-amber-600" />
|
<MapPin className="w-5 h-5 text-amber-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-500">{t('support.address')}</p>
|
<p className="text-sm text-gray-500">{t("support.address")}</p>
|
||||||
<p className="font-medium text-gray-900">{t('support.addressValue')}</p>
|
<p className="font-medium text-gray-900">{t("support.addressValue")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-amber-50 rounded-2xl border border-amber-200 p-6">
|
<div className="bg-amber-50 rounded-2xl border border-amber-200 p-6">
|
||||||
<h3 className="text-lg font-bold text-amber-800 mb-2">
|
<h3 className="text-lg font-bold text-amber-800 mb-2">{t("support.workingHours")}</h3>
|
||||||
{t('support.workingHours')}
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-2 text-amber-700">
|
<div className="space-y-2 text-amber-700">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span>{t('support.weekdays')}</span>
|
<span>{t("support.weekdays")}</span>
|
||||||
<span>9:00 - 18:00</span>
|
<span>9:00 - 18:00</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span>{t('support.friday')}</span>
|
<span>{t("support.friday")}</span>
|
||||||
<span>{t('support.closed')}</span>
|
<span>{t("support.closed")}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
167
app/utils/api.js
167
app/utils/api.js
@ -1,3 +1,4 @@
|
|||||||
|
import { Toast } from "flowbite-react";
|
||||||
import AuthService from "../services/AuthService";
|
import AuthService from "../services/AuthService";
|
||||||
|
|
||||||
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";
|
||||||
@ -20,10 +21,7 @@ export function isApiBlockedError(error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function redirectToBlockedPage() {
|
function redirectToBlockedPage() {
|
||||||
if (
|
if (typeof window !== "undefined" && window.location.pathname !== "/blocked") {
|
||||||
typeof window !== "undefined" &&
|
|
||||||
window.location.pathname !== "/blocked"
|
|
||||||
) {
|
|
||||||
window.location.replace("/blocked");
|
window.location.replace("/blocked");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -50,12 +48,7 @@ async function apiFetch(endpoint, options = {}) {
|
|||||||
const hasBody = options.body != null;
|
const hasBody = options.body != null;
|
||||||
const bodyIsFormData = isFormData(options.body);
|
const bodyIsFormData = isFormData(options.body);
|
||||||
|
|
||||||
if (
|
if (hasBody && !bodyIsFormData && !headers["Content-Type"] && !headers["content-type"]) {
|
||||||
hasBody &&
|
|
||||||
!bodyIsFormData &&
|
|
||||||
!headers["Content-Type"] &&
|
|
||||||
!headers["content-type"]
|
|
||||||
) {
|
|
||||||
headers["Content-Type"] = "application/json";
|
headers["Content-Type"] = "application/json";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,10 +58,7 @@ async function apiFetch(endpoint, options = {}) {
|
|||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
...options,
|
...options,
|
||||||
headers,
|
headers,
|
||||||
body:
|
body: hasBody && !bodyIsFormData && typeof options.body !== "string" ? JSON.stringify(options.body) : options.body,
|
||||||
hasBody && !bodyIsFormData && typeof options.body !== "string"
|
|
||||||
? JSON.stringify(options.body)
|
|
||||||
: options.body,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
assertNotBlocked(res);
|
assertNotBlocked(res);
|
||||||
@ -91,10 +81,9 @@ async function apiFetch(endpoint, options = {}) {
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof TypeError && error.message === 'Failed to fetch') {
|
if (error instanceof TypeError && error.message === "Failed to fetch") {
|
||||||
throw new Error(
|
|
||||||
`تعذر الاتصال بالخادم. تحقق من اتصالك بالإنترنت أو حاول مرة أخرى لاحقاً. (${API_BASE}${endpoint})`,
|
throw new Error(`تعذر الاتصال بالخادم. تحقق من اتصالك بالإنترنت أو حاول مرة أخرى لاحقاً. (${API_BASE}${endpoint})`);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@ -137,8 +126,7 @@ async function authFetch(endpoint, body, token = null) {
|
|||||||
data = text;
|
data = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message =
|
const message = typeof data === "object" && data?.message ? data.message : null;
|
||||||
typeof data === "object" && data?.message ? data.message : null;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: res.status,
|
status: res.status,
|
||||||
@ -176,8 +164,7 @@ async function reportFetch(endpoint, body) {
|
|||||||
data = text;
|
data = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message =
|
const message = typeof data === "object" && data?.message ? data.message : null;
|
||||||
typeof data === "object" && data?.message ? data.message : null;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: res.status,
|
status: res.status,
|
||||||
@ -200,9 +187,7 @@ export async function getRentPropertyLocations(params = {}) {
|
|||||||
if (params.maxOffset != null) qs.set("maxOffset", params.maxOffset);
|
if (params.maxOffset != null) qs.set("maxOffset", params.maxOffset);
|
||||||
if (params.minOffset != null) qs.set("minOffset", params.minOffset);
|
if (params.minOffset != null) qs.set("minOffset", params.minOffset);
|
||||||
const query = qs.toString();
|
const query = qs.toString();
|
||||||
return apiFetch(
|
return apiFetch(`/RentProperties/GetRentPropertiesLocations${query ? `?${query}` : ""}`);
|
||||||
`/RentProperties/GetRentPropertiesLocations${query ? `?${query}` : ""}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSaleProperties() {
|
export async function getSaleProperties() {
|
||||||
@ -227,19 +212,13 @@ export async function getTopRecommendations(count = 10) {
|
|||||||
return apiFetch(`/Recommendations/GetTopRecommendations?count=${count}`);
|
return apiFetch(`/Recommendations/GetTopRecommendations?count=${count}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAvailableDateRanges(
|
export async function getAvailableDateRanges(propertyId, fromDate = null, toDate = null) {
|
||||||
propertyId,
|
|
||||||
fromDate = null,
|
|
||||||
toDate = null,
|
|
||||||
) {
|
|
||||||
const qs = new URLSearchParams();
|
const qs = new URLSearchParams();
|
||||||
if (fromDate) qs.set("fromDate", fromDate);
|
if (fromDate) qs.set("fromDate", fromDate);
|
||||||
if (toDate) qs.set("toDate", toDate);
|
if (toDate) qs.set("toDate", toDate);
|
||||||
const query = qs.toString();
|
const query = qs.toString();
|
||||||
|
|
||||||
return apiFetch(
|
return apiFetch(`/Reservations/GetAvailableDates/available/${propertyId}${query ? `?${query}` : ""}`);
|
||||||
`/Reservations/GetAvailableDates/available/${propertyId}${query ? `?${query}` : ""}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getReservations() {
|
export async function getReservations() {
|
||||||
@ -250,18 +229,12 @@ export async function getReservation(id) {
|
|||||||
return apiFetch(`/Reservations/GetReservation?id=${id}`);
|
return apiFetch(`/Reservations/GetReservation?id=${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkAvailability(
|
export async function checkAvailability(propertyId, fromDate = null, toDate = null) {
|
||||||
propertyId,
|
|
||||||
fromDate = null,
|
|
||||||
toDate = null,
|
|
||||||
) {
|
|
||||||
const qs = new URLSearchParams();
|
const qs = new URLSearchParams();
|
||||||
if (fromDate) qs.set("fromDate", fromDate);
|
if (fromDate) qs.set("fromDate", fromDate);
|
||||||
if (toDate) qs.set("toDate", toDate);
|
if (toDate) qs.set("toDate", toDate);
|
||||||
const query = qs.toString();
|
const query = qs.toString();
|
||||||
return apiFetch(
|
return apiFetch(`/Reservations/GetAvailable/${propertyId}${query ? `?${query}` : ""}`);
|
||||||
`/Reservations/GetAvailable/${propertyId}${query ? `?${query}` : ""}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function bookReservation(propertyInfoId, startDate, endDate) {
|
export async function bookReservation(propertyInfoId, startDate, endDate) {
|
||||||
@ -297,18 +270,12 @@ export async function getMyRentListings() {
|
|||||||
|
|
||||||
export function buildRentPropertyPayload(data = {}) {
|
export function buildRentPropertyPayload(data = {}) {
|
||||||
const propertyInformation = data?.propertyInformation || {};
|
const propertyInformation = data?.propertyInformation || {};
|
||||||
|
|
||||||
const rawCityValue =
|
const rawCityValue = data?.city ?? data?.governorate ?? propertyInformation?.city ?? propertyInformation?.governorate ?? 1;
|
||||||
data?.city ??
|
|
||||||
data?.governorate ??
|
|
||||||
propertyInformation?.city ??
|
|
||||||
propertyInformation?.governorate ??
|
|
||||||
1;
|
|
||||||
|
|
||||||
const cityInt = parseInt(rawCityValue, 10) || 1;
|
const cityInt = parseInt(rawCityValue, 10) || 1;
|
||||||
|
|
||||||
const documentTypeValue =
|
const documentTypeValue = data?.documentType ?? propertyInformation?.documentType ?? 1;
|
||||||
data?.documentType ?? propertyInformation?.documentType ?? 1;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
@ -483,40 +450,23 @@ async function multipartAuthFetch(endpoint, formData) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addOwner(
|
export async function addOwner(data, frontImage = null, backImage = null, licenseImage = null) {
|
||||||
data,
|
|
||||||
frontImage = null,
|
|
||||||
backImage = null,
|
|
||||||
licenseImage = null,
|
|
||||||
) {
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
formData.append("FirstName", data.firstName || data.FirstName || "");
|
formData.append("FirstName", data.firstName || data.FirstName || "");
|
||||||
formData.append("LastName", data.lastName || data.LastName || "");
|
formData.append("LastName", data.lastName || data.LastName || "");
|
||||||
formData.append("Email", data.email || data.Email || "");
|
formData.append("Email", data.email || data.Email || "");
|
||||||
|
|
||||||
const phoneValue =
|
const phoneValue = data.phone || data.phoneNumber || data.Phone || data.PhoneNumber || "";
|
||||||
data.phone || data.phoneNumber || data.Phone || data.PhoneNumber || "";
|
const whatsappValue = data.whatsAppNumber || data.whatsapp || data.WhatsAppNumber || data.WhatsApp || "";
|
||||||
const whatsappValue =
|
|
||||||
data.whatsAppNumber ||
|
|
||||||
data.whatsapp ||
|
|
||||||
data.WhatsAppNumber ||
|
|
||||||
data.WhatsApp ||
|
|
||||||
"";
|
|
||||||
|
|
||||||
formData.append("PhoneNumber", phoneValue);
|
formData.append("PhoneNumber", phoneValue);
|
||||||
formData.append("Phone", phoneValue);
|
formData.append("Phone", phoneValue);
|
||||||
formData.append("WhatsAppNumber", whatsappValue);
|
formData.append("WhatsAppNumber", whatsappValue);
|
||||||
|
|
||||||
formData.append(
|
formData.append("NationalNumber", data.nationalNumber || data.NationalNumber || "");
|
||||||
"NationalNumber",
|
|
||||||
data.nationalNumber || data.NationalNumber || "",
|
|
||||||
);
|
|
||||||
formData.append("Password", data.password || data.Password || "");
|
formData.append("Password", data.password || data.Password || "");
|
||||||
formData.append(
|
formData.append("Type", String(data.type ?? data.ownerType ?? data.Type ?? 0));
|
||||||
"Type",
|
|
||||||
String(data.type ?? data.ownerType ?? data.Type ?? 0),
|
|
||||||
);
|
|
||||||
formData.append("Language", String(data.language ?? data.Language ?? 1));
|
formData.append("Language", String(data.language ?? data.Language ?? 1));
|
||||||
|
|
||||||
if (frontImage) formData.append("FrontIdCarImagePath", frontImage);
|
if (frontImage) formData.append("FrontIdCarImagePath", frontImage);
|
||||||
@ -573,20 +523,12 @@ export async function sendPhoneOTP() {
|
|||||||
|
|
||||||
export async function verifyEmail(code) {
|
export async function verifyEmail(code) {
|
||||||
const token = AuthService.getToken();
|
const token = AuthService.getToken();
|
||||||
return authFetch(
|
return authFetch(`/Auth/VerifyEmail?code=${encodeURIComponent(code)}`, {}, token);
|
||||||
`/Auth/VerifyEmail?code=${encodeURIComponent(code)}`,
|
|
||||||
{},
|
|
||||||
token,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function verifyPhone(code) {
|
export async function verifyPhone(code) {
|
||||||
const token = AuthService.getToken();
|
const token = AuthService.getToken();
|
||||||
return authFetch(
|
return authFetch(`/Auth/VerifyPhoneNumber?code=${encodeURIComponent(code)}`, {}, token);
|
||||||
`/Auth/VerifyPhoneNumber?code=${encodeURIComponent(code)}`,
|
|
||||||
{},
|
|
||||||
token,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isEmail(value) {
|
export function isEmail(value) {
|
||||||
@ -622,14 +564,9 @@ export async function confirmDepositPayment(bookingId) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function adminConfirmDeposit(
|
export async function adminConfirmDeposit(reservationId, adminId, comment = null) {
|
||||||
reservationId,
|
|
||||||
adminId,
|
|
||||||
comment = null,
|
|
||||||
) {
|
|
||||||
const token = AuthService.getToken();
|
const token = AuthService.getToken();
|
||||||
const normalizedComment =
|
const normalizedComment = typeof comment === "string" && comment.trim() ? comment.trim() : null;
|
||||||
typeof comment === "string" && comment.trim() ? comment.trim() : null;
|
|
||||||
const payload = {
|
const payload = {
|
||||||
reservationId,
|
reservationId,
|
||||||
adminId,
|
adminId,
|
||||||
@ -657,8 +594,7 @@ export async function adminConfirmDeposit(
|
|||||||
data = text;
|
data = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message =
|
const message = typeof data === "object" && data?.message ? data.message : null;
|
||||||
typeof data === "object" && data?.message ? data.message : null;
|
|
||||||
|
|
||||||
return { status: res.status, data, ok: res.ok, message };
|
return { status: res.status, data, ok: res.ok, message };
|
||||||
}
|
}
|
||||||
@ -698,15 +634,9 @@ export async function getMyTransaction() {
|
|||||||
export async function payDeposit(data) {
|
export async function payDeposit(data) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
formData.append(
|
formData.append("ReservationId", String(data.reservationId ?? data.ReservationId ?? ""));
|
||||||
"ReservationId",
|
|
||||||
String(data.reservationId ?? data.ReservationId ?? "")
|
|
||||||
);
|
|
||||||
|
|
||||||
formData.append(
|
formData.append("PaymentTypeId", String(data.paymentTypeId ?? data.PaymentTypeId ?? ""));
|
||||||
"PaymentTypeId",
|
|
||||||
String(data.paymentTypeId ?? data.PaymentTypeId ?? "")
|
|
||||||
);
|
|
||||||
|
|
||||||
if (data.comment) {
|
if (data.comment) {
|
||||||
formData.append("Comment", data.comment);
|
formData.append("Comment", data.comment);
|
||||||
@ -725,9 +655,7 @@ export async function payDeposit(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getOwnerContactInformation(propertyInformationId) {
|
export async function getOwnerContactInformation(propertyInformationId) {
|
||||||
return apiFetch(
|
return apiFetch(`/Owner/GetOwnerContactInformation?propertyInformationId=${propertyInformationId}`);
|
||||||
`/Owner/GetOwnerContactInformation?propertyInformationId=${propertyInformationId}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getOwnerStatistics() {
|
export async function getOwnerStatistics() {
|
||||||
@ -766,12 +694,9 @@ export async function registerRealEstateAgent(formData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function changePassword(oldPassword, newPassword) {
|
export async function changePassword(oldPassword, newPassword) {
|
||||||
return apiFetch(
|
return apiFetch(`/User/ChangePassword?oldPassword=${encodeURIComponent(oldPassword)}&newPassword=${encodeURIComponent(newPassword)}`, {
|
||||||
`/User/ChangePassword?oldPassword=${encodeURIComponent(oldPassword)}&newPassword=${encodeURIComponent(newPassword)}`,
|
method: "PUT",
|
||||||
{
|
});
|
||||||
method: "PUT",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function requestForgetPasswordOtp(email) {
|
export async function requestForgetPasswordOtp(email) {
|
||||||
@ -781,12 +706,9 @@ export async function requestForgetPasswordOtp(email) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function verifyForgetPasswordOtp(email, code, newPassword) {
|
export async function verifyForgetPasswordOtp(email, code, newPassword) {
|
||||||
return apiFetch(
|
return apiFetch(`/User/VerifyForgetPasswordOTP?email=${encodeURIComponent(email)}&code=${encodeURIComponent(code)}&newPassword=${encodeURIComponent(newPassword)}`, {
|
||||||
`/User/VerifyForgetPasswordOTP?email=${encodeURIComponent(email)}&code=${encodeURIComponent(code)}&newPassword=${encodeURIComponent(newPassword)}`,
|
method: "POST",
|
||||||
{
|
});
|
||||||
method: "POST",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resetPassword(token) {
|
export async function resetPassword(token) {
|
||||||
@ -794,12 +716,9 @@ export async function resetPassword(token) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteMyAccount(password) {
|
export async function deleteMyAccount(password) {
|
||||||
return apiFetch(
|
return apiFetch(`/User/DeleteMyAccount?password=${encodeURIComponent(password)}`, {
|
||||||
`/User/DeleteMyAccount?password=${encodeURIComponent(password)}`,
|
method: "DELETE",
|
||||||
{
|
});
|
||||||
method: "DELETE",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setFCMToken(token, deviceType = 2) {
|
export async function setFCMToken(token, deviceType = 2) {
|
||||||
@ -815,9 +734,7 @@ export async function filterRentProperties(params = {}) {
|
|||||||
if (v != null && v !== "") qs.set(k, v);
|
if (v != null && v !== "") qs.set(k, v);
|
||||||
});
|
});
|
||||||
const query = qs.toString();
|
const query = qs.toString();
|
||||||
return apiFetch(
|
return apiFetch(`/RentProperties/FilterRentProperties${query ? `?${query}` : ""}`);
|
||||||
`/RentProperties/FilterRentProperties${query ? `?${query}` : ""}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendGeneralReport(subject, reportBody) {
|
export async function sendGeneralReport(subject, reportBody) {
|
||||||
@ -867,4 +784,4 @@ export async function addOrUpdateTerms(terms) {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: terms,
|
body: terms,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
7
package-lock.json
generated
7
package-lock.json
generated
@ -16,6 +16,7 @@
|
|||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"i18next": "^25.8.0",
|
"i18next": "^25.8.0",
|
||||||
"i18next-browser-languagedetector": "^8.2.0",
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
|
"js-cookie": "^3.0.8",
|
||||||
"jspdf": "^4.2.1",
|
"jspdf": "^4.2.1",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"lucide-react": "^0.563.0",
|
"lucide-react": "^0.563.0",
|
||||||
@ -3085,6 +3086,12 @@
|
|||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/js-cookie": {
|
||||||
|
"version": "3.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.8.tgz",
|
||||||
|
"integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"i18next": "^25.8.0",
|
"i18next": "^25.8.0",
|
||||||
"i18next-browser-languagedetector": "^8.2.0",
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
|
"js-cookie": "^3.0.8",
|
||||||
"jspdf": "^4.2.1",
|
"jspdf": "^4.2.1",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"lucide-react": "^0.563.0",
|
"lucide-react": "^0.563.0",
|
||||||
|
|||||||
BIN
public/hero.jpg
BIN
public/hero.jpg
Binary file not shown.
|
Before Width: | Height: | Size: 14 MiB After Width: | Height: | Size: 2.3 MiB |
Reference in New Issue
Block a user