Compare commits

...

35 Commits

Author SHA1 Message Date
5592d9fbd2 fixed the logic and build the ownerResirvation page
All checks were successful
Build frontend / build (push) Successful in 1m36s
2026-08-13 13:58:07 +03:00
4b53669a35 add comission
All checks were successful
Build frontend / build (push) Successful in 1m25s
2026-08-12 17:42:51 +03:00
00dde588cb fixed getSaleProperiesById and make login type is email
All checks were successful
Build frontend / build (push) Successful in 1m46s
2026-08-11 16:04:09 +03:00
4924e7c2f3 fixed payment ,colors , add Mosque distance and fixed some elemnent
All checks were successful
Build frontend / build (push) Successful in 1m7s
2026-08-11 02:29:54 +03:00
9d3d02c88a fix icons
All checks were successful
Build frontend / build (push) Successful in 1m27s
2026-08-10 22:27:28 +03:00
1fa2fabb38 fixed NavButtom responsive
All checks were successful
Build frontend / build (push) Successful in 1m1s
2026-08-08 14:48:12 +03:00
793ec51c9a fixed NavButtom
All checks were successful
Build frontend / build (push) Successful in 59s
2026-08-08 14:22:34 +03:00
e3e9aa81a8 fixed middleware
All checks were successful
Build frontend / build (push) Successful in 1m36s
2026-08-08 13:45:58 +03:00
989a7f580f fixed middleware
All checks were successful
Build frontend / build (push) Successful in 53s
2026-08-07 00:55:23 +03:00
f7afef03d4 Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 56s
2026-08-07 00:38:19 +03:00
f09561ef00 Update .gitea/workflows/deployer.yaml
Some checks failed
Build frontend / build (push) Has been cancelled
changed the repository name to amer2004/SweetHome
2026-08-06 21:38:13 +00:00
e42b0421bd fixed middleware 2026-08-07 00:37:33 +03:00
9602154ba4 Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 1m24s
2026-08-07 00:11:52 +03:00
9b4f9979b6 restored the git action 2026-08-07 00:06:40 +03:00
b9a4c6157a edited middle ware and calender component 2026-08-06 23:51:50 +03:00
4d1b744516 add Middleware + fixed all pages and fixed add propertise 2026-08-06 22:26:35 +03:00
7da4bf9f2b fixed owner and customer register make reuseable component and completed it 2026-08-05 23:27:34 +03:00
7973c15e3f fixd larg lines in HomePage , add cookies rather than localStorage,make canvas background Login page 2026-08-04 12:51:47 +03:00
4ebb75ddd9 first commit 2026-08-01 15:03:30 +03:00
30693c6495 fixed some changes in the ccode
All checks were successful
Build frontend / build (push) Successful in 2m13s
2026-07-26 00:09:45 +03:00
491f10a1e0 fixed some changes in the ccode
All checks were successful
Build frontend / build (push) Successful in 1m25s
2026-07-25 14:58:54 +03:00
f91dfaacad fixed some changes in the ccode
All checks were successful
Build frontend / build (push) Successful in 55s
2026-07-25 14:54:27 +03:00
b4bb3ec279 fixed some changes in the ccode
All checks were successful
Build frontend / build (push) Successful in 1m10s
2026-07-25 13:26:48 +03:00
3ee4a93365 edited the edit property and need to test
All checks were successful
Build frontend / build (push) Successful in 1m36s
2026-07-25 12:48:37 +03:00
689170ebce fixed the app dialog
All checks were successful
Build frontend / build (push) Successful in 1m49s
2026-07-21 10:36:05 +03:00
46ecf74572 edited the payment dialog
All checks were successful
Build frontend / build (push) Successful in 1m10s
2026-07-21 01:15:16 +03:00
66db009562 edited the payment dialog
All checks were successful
Build frontend / build (push) Successful in 1m2s
2026-07-21 01:08:29 +03:00
83a06e8007 edited the payment dialog
All checks were successful
Build frontend / build (push) Successful in 1m0s
2026-07-21 00:43:49 +03:00
4166a5d652 it must be fixed
All checks were successful
Build frontend / build (push) Successful in 1m28s
2026-07-20 22:12:51 +03:00
ca6b12bfb9 fixing add property
All checks were successful
Build frontend / build (push) Successful in 57s
2026-07-16 10:56:59 -07:00
2374e80380 GetPlatformNumber​ api
All checks were successful
Build frontend / build (push) Successful in 1m32s
2026-07-16 09:40:51 -07:00
747e42dc00 Edit owner
All checks were successful
Build frontend / build (push) Successful in 1m9s
2026-07-15 20:03:38 +03:00
038df6397d Edit add property
All checks were successful
Build frontend / build (push) Successful in 58s
2026-07-15 19:42:59 +03:00
b5f5fabfc8 Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 1m18s
2026-07-15 19:25:49 +03:00
8ed623bbe2 Edit haram payment 2026-07-15 19:25:39 +03:00
108 changed files with 9678 additions and 10389 deletions

View File

@ -10,7 +10,7 @@ jobs:
- name: Clone repository - name: Clone repository
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
repository: Rahaf/SweetHome repository: amer2004/SweetHome
github-server-url: http://45.93.137.91:3000 github-server-url: http://45.93.137.91:3000
- name: Stopping server - name: Stopping server

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"i18n-ally-next.localesPaths": ["app/i18n"]
}

View File

@ -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,61 +140,49 @@ 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
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> 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={`flex justify-between items-center h-20 ${currentLanguage === "ar" ? "flex-row-reverse" : ""}`}
> >
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div 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" />
@ -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" }}
@ -737,105 +630,86 @@ export default function ClientLayout({ children }) {
{!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"}`}
>
{!isAuthenticated ? t("visitor") : isOwner ? t("owner") : t("customer")}
</span> </span>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/favorites" className={`relative group flex items-center justify-center w-9 h-9 rounded-full transition-colors hover:bg-white/10 ${pathname === '/favorites' ? 'text-amber-400' : 'nav-icon'}`}> <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" /> <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"> <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")} {t("favorites")}
</div> </div>
</Link> </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'}`}> <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" /> <Bell className="w-5 h-5" />
{isMounted && unreadCount > 0 && ( {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"> <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>
{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"> <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")} {t("notifications")}
</div> </div>
</Link> </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'}`}> <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" /> <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"> <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")} {t("settings")}
</div> </div>
</Link> </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" />} <button
<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"> onClick={() => changeLanguage(currentLanguage === "ar" ? "en" : "ar")}
{theme === 'dark' ? t('lightMode') : t('darkMode')} className="relative group flex items-center justify-center w-9 h-9 rounded-full transition-colors hover:bg-white/10 nav-icon"
</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" /> <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"> <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')} {currentLanguage === "ar" ? t("switchToEnglish") : t("switchToArabic")}
</div> </div>
</button> </button>
</div> </div>
</div> </div>
)} )}
<div className="flex-1"> <div className="flex-1">{children}</div>
{children} {isAuthenticated && !isAuthPage && <BottomNav isOwner={isOwner} isOwnerOrAgent={isOwnerOrAgent} />}
</div>
{isAuthenticated && !isAuthPage && (
<BottomNav isOwner={isOwner} isOwnerOrAgent={isOwnerOrAgent} />
)}
</main> </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" />

View 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,
};
}

View File

@ -0,0 +1,20 @@
import toast from "react-hot-toast";
export const handleImageUpload = (side, file,t,setIdImagePreviews,setIdImages) => {
if (!file) return;
if (!file.type.startsWith("image/")) {
toast.error(t("register.selectValidImage"));
return;
}
if (file.size > 5 * 1024 * 1024) {
toast.error(t("register.imageSizeLimit"));
return;
}
const reader = new FileReader();
reader.onloadend = () => {
setIdImagePreviews((prev) => ({ ...prev, [side]: reader.result }));
};
reader.readAsDataURL(file);
setIdImages((prev) => ({ ...prev, [side]: file }));
toast.success(t("register.imageUploadSuccess"), { style: { background: "#dcfce7", color: "#166534" } });
};

View File

@ -1,24 +1,15 @@
'use client'; "use client";
import { useState, useEffect } from 'react'; import { useState, useEffect } from "react";
import { useRouter } from 'next/navigation'; import { useRouter } from "next/navigation";
import Link from 'next/link'; import Link from "next/link";
import { motion } from 'framer-motion'; import { motion } from "framer-motion";
import { useTranslation } from 'react-i18next'; import { useTranslation } from "react-i18next";
import { import { Mail, Phone, Shield, ChevronLeft, Loader2, CheckCircle, XCircle, Send, Key } from "lucide-react";
Mail, import toast, { Toaster } from "react-hot-toast";
Phone, import AuthService from "../services/AuthService";
Shield, import { sendEmailOTP, sendPhoneOTP, verifyEmail, verifyPhone } from "../utils/api";
ChevronLeft, import InteractiveBackground from "../components/Animation/Background";
Loader2,
CheckCircle,
XCircle,
Send,
Key
} from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
import AuthService from '../services/AuthService';
import { sendEmailOTP, sendPhoneOTP, verifyEmail, verifyPhone } from '../utils/api';
export default function AccountVerificationPage() { export default function AccountVerificationPage() {
const router = useRouter(); const router = useRouter();
@ -26,8 +17,8 @@ export default function AccountVerificationPage() {
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [emailCode, setEmailCode] = useState(''); const [emailCode, setEmailCode] = useState("");
const [phoneCode, setPhoneCode] = useState(''); const [phoneCode, setPhoneCode] = useState("");
const [sendingEmailOTP, setSendingEmailOTP] = useState(false); const [sendingEmailOTP, setSendingEmailOTP] = useState(false);
const [sendingPhoneOTP, setSendingPhoneOTP] = useState(false); const [sendingPhoneOTP, setSendingPhoneOTP] = useState(false);
const [verifyingEmail, setVerifyingEmail] = useState(false); const [verifyingEmail, setVerifyingEmail] = useState(false);
@ -43,7 +34,7 @@ export default function AccountVerificationPage() {
setUser(authUser); setUser(authUser);
setIsLoading(false); setIsLoading(false);
} else { } else {
router.push('/login'); router.push("/login");
} }
}, [router]); }, [router]);
@ -51,10 +42,10 @@ export default function AccountVerificationPage() {
setSendingEmailOTP(true); setSendingEmailOTP(true);
try { try {
await sendEmailOTP(); await sendEmailOTP();
toast.success(t('accountVerification.emailOtpSent')); toast.success(t("accountVerification.emailOtpSent"));
setShowEmailInput(true); setShowEmailInput(true);
} catch (err) { } catch (err) {
toast.error(err.message || t('accountVerification.otpSendFailed')); toast.error(err.message || t("accountVerification.otpSendFailed"));
} finally { } finally {
setSendingEmailOTP(false); setSendingEmailOTP(false);
} }
@ -62,7 +53,7 @@ export default function AccountVerificationPage() {
const handleVerifyEmail = async () => { const handleVerifyEmail = async () => {
if (!emailCode.trim()) { if (!emailCode.trim()) {
toast.error(t('accountVerification.otpRequired')); toast.error(t("accountVerification.otpRequired"));
return; return;
} }
setVerifyingEmail(true); setVerifyingEmail(true);
@ -70,9 +61,9 @@ export default function AccountVerificationPage() {
await verifyEmail(emailCode.trim()); await verifyEmail(emailCode.trim());
setEmailVerified(true); setEmailVerified(true);
setShowEmailInput(false); setShowEmailInput(false);
toast.success(t('accountVerification.emailVerifiedSuccess')); toast.success(t("accountVerification.emailVerifiedSuccess"));
} catch (err) { } catch (err) {
toast.error(err.message || t('accountVerification.otpInvalid')); toast.error(err.message || t("accountVerification.otpInvalid"));
} finally { } finally {
setVerifyingEmail(false); setVerifyingEmail(false);
} }
@ -82,10 +73,10 @@ export default function AccountVerificationPage() {
setSendingPhoneOTP(true); setSendingPhoneOTP(true);
try { try {
await sendPhoneOTP(); await sendPhoneOTP();
toast.success(t('accountVerification.phoneOtpSent')); toast.success(t("accountVerification.phoneOtpSent"));
setShowPhoneInput(true); setShowPhoneInput(true);
} catch (err) { } catch (err) {
toast.error(err.message || t('accountVerification.otpSendFailed')); toast.error(err.message || t("accountVerification.otpSendFailed"));
} finally { } finally {
setSendingPhoneOTP(false); setSendingPhoneOTP(false);
} }
@ -93,7 +84,7 @@ export default function AccountVerificationPage() {
const handleVerifyPhone = async () => { const handleVerifyPhone = async () => {
if (!phoneCode.trim()) { if (!phoneCode.trim()) {
toast.error(t('accountVerification.otpRequired')); toast.error(t("accountVerification.otpRequired"));
return; return;
} }
setVerifyingPhone(true); setVerifyingPhone(true);
@ -101,80 +92,56 @@ export default function AccountVerificationPage() {
await verifyPhone(phoneCode.trim()); await verifyPhone(phoneCode.trim());
setPhoneVerified(true); setPhoneVerified(true);
setShowPhoneInput(false); setShowPhoneInput(false);
toast.success(t('accountVerification.phoneVerifiedSuccess')); toast.success(t("accountVerification.phoneVerifiedSuccess"));
} catch (err) { } catch (err) {
toast.error(err.message || t('accountVerification.otpInvalid')); toast.error(err.message || t("accountVerification.otpInvalid"));
} finally { } finally {
setVerifyingPhone(false); setVerifyingPhone(false);
} }
}; };
if (isLoading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<Loader2 className="w-12 h-12 text-amber-500 animate-spin" />
</div>
);
}
const containerVariants = { const containerVariants = {
hidden: { opacity: 0 }, hidden: { opacity: 0 },
visible: { visible: {
opacity: 1, opacity: 1,
transition: { staggerChildren: 0.1 } transition: { staggerChildren: 0.1 },
} },
}; };
const itemVariants = { const itemVariants = {
hidden: { opacity: 0, y: 20 }, hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 } visible: { opacity: 1, y: 0 },
}; };
return ( return (
<div className="min-h-screen bg-gray-50 py-8" dir="rtl"> <div className="min-h-screen py-8" dir="rtl">
<Toaster position="top-center" reverseOrder={false} /> <Toaster position="top-center" reverseOrder={false} />
<InteractiveBackground />
<div className="container mx-auto px-4 max-w-2xl"> <div className="relative z-10 container mx-auto px-4 max-w-2xl">
<motion.div <motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="flex items-center gap-3 mb-8">
initial={{ opacity: 0, y: -20 }} <Link href="/settings" className="p-2 rounded-xl hover:bg-gray-200 transition-colors text-gray-600">
animate={{ opacity: 1, y: 0 }}
className="flex items-center gap-3 mb-8"
>
<Link
href="/settings"
className="p-2 rounded-xl hover:bg-gray-200 transition-colors text-gray-600"
>
<ChevronLeft className="w-5 h-5" /> <ChevronLeft className="w-5 h-5" />
</Link> </Link>
<h1 className="text-2xl font-bold text-gray-900">{t('accountVerification.title')}</h1> <h1 className="text-2xl font-bold text-gray-900">{t("accountVerification.title")}</h1>
</motion.div> </motion.div>
<motion.div <motion.div variants={containerVariants} initial="hidden" animate="visible" className="space-y-6">
variants={containerVariants}
initial="hidden"
animate="visible"
className="space-y-6"
>
<motion.div variants={itemVariants} className="bg-white rounded-2xl shadow-sm overflow-hidden"> <motion.div variants={itemVariants} className="bg-white rounded-2xl shadow-sm overflow-hidden">
<div className="px-6 py-4 border-b border-gray-100 flex items-center gap-3"> <div className="px-6 py-4 border-b border-gray-100 flex items-center gap-3">
<Mail className="w-5 h-5 text-amber-600" /> <Mail className="w-5 h-5 text-amber-600" />
<h2 className="text-lg font-semibold text-gray-800">{t('accountVerification.emailSection')}</h2> <h2 className="text-lg font-semibold text-gray-800">{t("accountVerification.emailSection")}</h2>
{emailVerified && ( {emailVerified && (
<span className="flex items-center gap-1 text-xs text-green-600 bg-green-50 px-2 py-0.5 rounded-full"> <span className="flex items-center gap-1 text-xs text-green-600 bg-green-50 px-2 py-0.5 rounded-full">
<CheckCircle className="w-3 h-3" /> <CheckCircle className="w-3 h-3" />
{t('accountVerification.verified')} {t("accountVerification.verified")}
</span> </span>
)} )}
</div> </div>
<div className="p-6"> <div className="p-6">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<div> <div>
<p className="text-sm text-gray-600">{user?.email || t('accountVerification.emailNotRegistered')}</p> <p className="text-sm text-gray-600">{user?.email || t("accountVerification.emailNotRegistered")}</p>
<p className="text-xs text-gray-400 mt-1"> <p className="text-xs text-gray-400 mt-1">{emailVerified ? t("accountVerification.emailVerifiedDesc") : t("accountVerification.emailVerifyDesc")}</p>
{emailVerified
? t('accountVerification.emailVerifiedDesc')
: t('accountVerification.emailVerifyDesc')}
</p>
</div> </div>
{!emailVerified && !showEmailInput && ( {!emailVerified && !showEmailInput && (
<button <button
@ -182,29 +149,21 @@ export default function AccountVerificationPage() {
disabled={sendingEmailOTP} disabled={sendingEmailOTP}
className="flex items-center gap-2 px-4 py-2 bg-amber-500 text-white rounded-xl hover:bg-amber-600 transition-colors text-sm disabled:opacity-50" className="flex items-center gap-2 px-4 py-2 bg-amber-500 text-white rounded-xl hover:bg-amber-600 transition-colors text-sm disabled:opacity-50"
> >
{sendingEmailOTP ? ( {sendingEmailOTP ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />}
<Loader2 className="w-4 h-4 animate-spin" /> {sendingEmailOTP ? t("accountVerification.sending") : t("accountVerification.sendOtp")}
) : (
<Send className="w-4 h-4" />
)}
{sendingEmailOTP ? t('accountVerification.sending') : t('accountVerification.sendOtp')}
</button> </button>
)} )}
</div> </div>
{showEmailInput && ( {showEmailInput && (
<motion.div <motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} className="space-y-3">
initial={{ opacity: 0, height: 0 }} <p className="text-xs text-gray-500">{t("accountVerification.enter6DigitCode")}</p>
animate={{ opacity: 1, height: 'auto' }}
className="space-y-3"
>
<p className="text-xs text-gray-500">{t('accountVerification.enter6DigitCode')}</p>
<div className="flex gap-2"> <div className="flex gap-2">
<input <input
type="text" type="text"
value={emailCode} value={emailCode}
onChange={(e) => setEmailCode(e.target.value)} onChange={(e) => setEmailCode(e.target.value)}
placeholder={t('accountVerification.otpPlaceholder')} placeholder={t("accountVerification.otpPlaceholder")}
maxLength={6} maxLength={6}
className="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none text-center text-lg tracking-widest" className="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none text-center text-lg tracking-widest"
/> />
@ -213,20 +172,12 @@ export default function AccountVerificationPage() {
disabled={verifyingEmail} disabled={verifyingEmail}
className="px-6 py-3 bg-green-500 text-white rounded-xl hover:bg-green-600 transition-colors text-sm font-medium disabled:opacity-50 flex items-center gap-2" className="px-6 py-3 bg-green-500 text-white rounded-xl hover:bg-green-600 transition-colors text-sm font-medium disabled:opacity-50 flex items-center gap-2"
> >
{verifyingEmail ? ( {verifyingEmail ? <Loader2 className="w-4 h-4 animate-spin" /> : <Key className="w-4 h-4" />}
<Loader2 className="w-4 h-4 animate-spin" /> {t("accountVerification.verify")}
) : (
<Key className="w-4 h-4" />
)}
{t('accountVerification.verify')}
</button> </button>
</div> </div>
<button <button onClick={handleSendEmailOTP} disabled={sendingEmailOTP} className="text-xs text-amber-600 hover:text-amber-700">
onClick={handleSendEmailOTP} {t("accountVerification.resendCode")}
disabled={sendingEmailOTP}
className="text-xs text-amber-600 hover:text-amber-700"
>
{t('accountVerification.resendCode')}
</button> </button>
</motion.div> </motion.div>
)} )}
@ -234,7 +185,7 @@ export default function AccountVerificationPage() {
{emailVerified && ( {emailVerified && (
<div className="flex items-center gap-2 text-green-600 bg-green-50 px-4 py-3 rounded-xl"> <div className="flex items-center gap-2 text-green-600 bg-green-50 px-4 py-3 rounded-xl">
<CheckCircle className="w-5 h-5" /> <CheckCircle className="w-5 h-5" />
<span className="text-sm font-medium">{t('accountVerification.emailVerifiedSuccess')}</span> <span className="text-sm font-medium">{t("accountVerification.emailVerifiedSuccess")}</span>
</div> </div>
)} )}
</div> </div>
@ -243,23 +194,19 @@ export default function AccountVerificationPage() {
<motion.div variants={itemVariants} className="bg-white rounded-2xl shadow-sm overflow-hidden"> <motion.div variants={itemVariants} className="bg-white rounded-2xl shadow-sm overflow-hidden">
<div className="px-6 py-4 border-b border-gray-100 flex items-center gap-3"> <div className="px-6 py-4 border-b border-gray-100 flex items-center gap-3">
<Phone className="w-5 h-5 text-amber-600" /> <Phone className="w-5 h-5 text-amber-600" />
<h2 className="text-lg font-semibold text-gray-800">{t('accountVerification.phoneSection')}</h2> <h2 className="text-lg font-semibold text-gray-800">{t("accountVerification.phoneSection")}</h2>
{phoneVerified && ( {phoneVerified && (
<span className="flex items-center gap-1 text-xs text-green-600 bg-green-50 px-2 py-0.5 rounded-full"> <span className="flex items-center gap-1 text-xs text-green-600 bg-green-50 px-2 py-0.5 rounded-full">
<CheckCircle className="w-3 h-3" /> <CheckCircle className="w-3 h-3" />
{t('accountVerification.verified')} {t("accountVerification.verified")}
</span> </span>
)} )}
</div> </div>
<div className="p-6"> <div className="p-6">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<div> <div>
<p className="text-sm text-gray-600">{user?.phone || t('accountVerification.phoneNotRegistered')}</p> <p className="text-sm text-gray-600">{user?.phone || t("accountVerification.phoneNotRegistered")}</p>
<p className="text-xs text-gray-400 mt-1"> <p className="text-xs text-gray-400 mt-1">{phoneVerified ? t("accountVerification.phoneVerifiedDesc") : t("accountVerification.phoneVerifyDesc")}</p>
{phoneVerified
? t('accountVerification.phoneVerifiedDesc')
: t('accountVerification.phoneVerifyDesc')}
</p>
</div> </div>
{!phoneVerified && !showPhoneInput && ( {!phoneVerified && !showPhoneInput && (
<button <button
@ -267,29 +214,21 @@ export default function AccountVerificationPage() {
disabled={sendingPhoneOTP} disabled={sendingPhoneOTP}
className="flex items-center gap-2 px-4 py-2 bg-amber-500 text-white rounded-xl hover:bg-amber-600 transition-colors text-sm disabled:opacity-50" className="flex items-center gap-2 px-4 py-2 bg-amber-500 text-white rounded-xl hover:bg-amber-600 transition-colors text-sm disabled:opacity-50"
> >
{sendingPhoneOTP ? ( {sendingPhoneOTP ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />}
<Loader2 className="w-4 h-4 animate-spin" /> {sendingPhoneOTP ? t("accountVerification.sending") : t("accountVerification.sendOtp")}
) : (
<Send className="w-4 h-4" />
)}
{sendingPhoneOTP ? t('accountVerification.sending') : t('accountVerification.sendOtp')}
</button> </button>
)} )}
</div> </div>
{showPhoneInput && ( {showPhoneInput && (
<motion.div <motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} className="space-y-3">
initial={{ opacity: 0, height: 0 }} <p className="text-xs text-gray-500">{t("accountVerification.enter6DigitCodePhone")}</p>
animate={{ opacity: 1, height: 'auto' }}
className="space-y-3"
>
<p className="text-xs text-gray-500">{t('accountVerification.enter6DigitCodePhone')}</p>
<div className="flex gap-2"> <div className="flex gap-2">
<input <input
type="text" type="text"
value={phoneCode} value={phoneCode}
onChange={(e) => setPhoneCode(e.target.value)} onChange={(e) => setPhoneCode(e.target.value)}
placeholder={t('accountVerification.otpPlaceholder')} placeholder={t("accountVerification.otpPlaceholder")}
maxLength={6} maxLength={6}
className="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none text-center text-lg tracking-widest" className="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none text-center text-lg tracking-widest"
/> />
@ -298,20 +237,12 @@ export default function AccountVerificationPage() {
disabled={verifyingPhone} disabled={verifyingPhone}
className="px-6 py-3 bg-green-500 text-white rounded-xl hover:bg-green-600 transition-colors text-sm font-medium disabled:opacity-50 flex items-center gap-2" className="px-6 py-3 bg-green-500 text-white rounded-xl hover:bg-green-600 transition-colors text-sm font-medium disabled:opacity-50 flex items-center gap-2"
> >
{verifyingPhone ? ( {verifyingPhone ? <Loader2 className="w-4 h-4 animate-spin" /> : <Key className="w-4 h-4" />}
<Loader2 className="w-4 h-4 animate-spin" /> {t("accountVerification.verify")}
) : (
<Key className="w-4 h-4" />
)}
{t('accountVerification.verify')}
</button> </button>
</div> </div>
<button <button onClick={handleSendPhoneOTP} disabled={sendingPhoneOTP} className="text-xs text-amber-600 hover:text-amber-700">
onClick={handleSendPhoneOTP} {t("accountVerification.resendCode")}
disabled={sendingPhoneOTP}
className="text-xs text-amber-600 hover:text-amber-700"
>
{t('accountVerification.resendCode')}
</button> </button>
</motion.div> </motion.div>
)} )}
@ -319,7 +250,7 @@ export default function AccountVerificationPage() {
{phoneVerified && ( {phoneVerified && (
<div className="flex items-center gap-2 text-green-600 bg-green-50 px-4 py-3 rounded-xl"> <div className="flex items-center gap-2 text-green-600 bg-green-50 px-4 py-3 rounded-xl">
<CheckCircle className="w-5 h-5" /> <CheckCircle className="w-5 h-5" />
<span className="text-sm font-medium">{t('accountVerification.phoneVerifiedSuccess')}</span> <span className="text-sm font-medium">{t("accountVerification.phoneVerifiedSuccess")}</span>
</div> </div>
)} )}
</div> </div>
@ -328,20 +259,20 @@ export default function AccountVerificationPage() {
<motion.div variants={itemVariants} className="bg-gradient-to-br from-amber-500 to-amber-600 rounded-2xl p-6 text-white"> <motion.div variants={itemVariants} className="bg-gradient-to-br from-amber-500 to-amber-600 rounded-2xl p-6 text-white">
<div className="flex items-center gap-3 mb-3"> <div className="flex items-center gap-3 mb-3">
<Shield className="w-6 h-6" /> <Shield className="w-6 h-6" />
<h3 className="text-lg font-semibold">{t('accountVerification.whyVerify')}</h3> <h3 className="text-lg font-semibold">{t("accountVerification.whyVerify")}</h3>
</div> </div>
<ul className="space-y-2 text-sm text-amber-50"> <ul className="space-y-2 text-sm " style={{ color: "#0f172a", fontWeight: "400" }}>
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 flex-shrink-0" /> <CheckCircle className="w-4 h-4 flex-shrink-0" />
{t('accountVerification.reason1')} {t("accountVerification.reason1")}
</li> </li>
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 flex-shrink-0" /> <CheckCircle className="w-4 h-4 flex-shrink-0" />
{t('accountVerification.reason2')} {t("accountVerification.reason2")}
</li> </li>
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 flex-shrink-0" /> <CheckCircle className="w-4 h-4 flex-shrink-0" />
{t('accountVerification.reason3')} {t("accountVerification.reason3")}
</li> </li>
</ul> </ul>
</motion.div> </motion.div>

View File

@ -6,7 +6,7 @@ import Link from 'next/link';
export default function Error({ error, reset }) { export default function Error({ error, reset }) {
return ( return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center p-4"> <div className="min-h-screen bg-gray-900 flex items-center justify-center p-4">
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="text-center max-w-md"> <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="text-center max-w-md">
<div className="w-20 h-20 bg-red-500/20 rounded-full flex items-center justify-center mx-auto mb-6"> <div className="w-20 h-20 bg-red-500/20 rounded-full flex items-center justify-center mx-auto mb-6">
<AlertTriangle className="w-10 h-10 text-red-500" /> <AlertTriangle className="w-10 h-10 text-red-500" />

View File

@ -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>
);
}

View File

@ -4,6 +4,7 @@ import { motion } from 'framer-motion';
import Link from 'next/link'; import Link from 'next/link';
import { Home, Building, Briefcase, ArrowLeft } from 'lucide-react'; import { Home, Building, Briefcase, ArrowLeft } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import InteractiveBackground from '@/app/components/Animation/Background';
export default function ChooseRolePage() { export default function ChooseRolePage() {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
@ -37,7 +38,8 @@ 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">
<InteractiveBackground/>
<div className="absolute inset-0 overflow-hidden"> <div className="absolute inset-0 overflow-hidden">
{[...Array(20)].map((_, i) => ( {[...Array(20)].map((_, i) => (
<motion.div <motion.div
@ -88,7 +90,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')}

View File

@ -1,24 +1,25 @@
'use client'; "use client";
import { useState } from 'react'; import { useState } from "react";
import { useRouter } from 'next/navigation'; import { useRouter } from "next/navigation";
import { motion } from 'framer-motion'; import { motion } from "framer-motion";
import { 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";
import InteractiveBackground from "../components/Animation/Background";
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 +31,7 @@ export default function BlockedPage() {
event.preventDefault(); event.preventDefault();
if (!form.subject.trim() || !form.body.trim()) { if (!form.subject.trim() || !form.body.trim()) {
toast.error(t('blocked.fillAllFields')); toast.error(t("blocked.fillAllFields"));
return; return;
} }
@ -39,39 +40,29 @@ export default function BlockedPage() {
try { try {
await sendGeneralReport(form.subject.trim(), form.body.trim()); await sendGeneralReport(form.subject.trim(), form.body.trim());
setIsSent(true); setIsSent(true);
setForm({ subject: '', body: '' }); setForm({ subject: "", body: "" });
toast.success(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-50 flex items-center justify-center p-4" dir="rtl">
<Toaster position="top-center" reverseOrder={false} /> <Toaster position="top-center" reverseOrder={false} />
<InteractiveBackground />
<motion.div <motion.div initial={{ opacity: 0, y: 24 }} animate={{ opacity: 1, y: 0 }} className="w-full max-w-5xl relative z-10">
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
className="w-full max-w-5xl"
>
<div className="text-center mb-10"> <div className="text-center mb-10">
<motion.div <motion.div initial={{ scale: 0.9 }} animate={{ scale: 1 }} className="w-24 h-24 bg-red-100 rounded-3xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-red-100">
initial={{ scale: 0.9 }}
animate={{ scale: 1 }}
className="w-24 h-24 bg-red-100 rounded-3xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-red-100"
>
<ShieldAlert className="w-12 h-12 text-red-600" /> <ShieldAlert className="w-12 h-12 text-red-600" />
</motion.div> </motion.div>
<h1 className="text-4xl font-bold text-gray-900 mb-3">{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 relative z-10 ">
<motion.div <motion.div
initial={{ opacity: 0, x: -24 }} initial={{ opacity: 0, x: -24 }}
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, x: 0 }}
@ -82,57 +73,50 @@ export default function BlockedPage() {
<div className="w-14 h-14 bg-red-50 rounded-2xl flex items-center justify-center mb-6"> <div className="w-14 h-14 bg-red-50 rounded-2xl flex items-center justify-center mb-6">
<LogOut className="w-7 h-7 text-red-600" /> <LogOut className="w-7 h-7 text-red-600" />
</div> </div>
<h2 className="text-2xl font-bold text-gray-900 mb-3">{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
type="button" type="button"
onClick={handleLogout} onClick={handleLogout}
className="mt-8 w-full bg-red-600 hover:bg-red-700 text-white rounded-2xl py-4 font-bold transition-colors flex items-center justify-center gap-3" className="mt-8 w-full cursor-pointer bg-red-600 hover:bg-red-700 text-white rounded-2xl py-4 font-bold transition-colors flex items-center justify-center gap-3"
> >
<LogOut className="w-5 h-5" /> <LogOut className="w-5 h-5" />
{t('blocked.logoutButton')} {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 +129,17 @@ export default function BlockedPage() {
{isSubmitting ? ( {isSubmitting ? (
<> <>
<Loader2 className="w-5 h-5 animate-spin" /> <Loader2 className="w-5 h-5 animate-spin" />
{t('blocked.sending')} {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>

View File

@ -144,13 +144,7 @@ export default function BookedPropertiesPage() {
} }
}, []); }, []);
if (loading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center" dir="rtl">
<Loader2 className="w-12 h-12 text-amber-500 animate-spin" />
</div>
);
}
return ( return (
<div className="min-h-screen bg-gray-50 py-8" dir="rtl"> <div className="min-h-screen bg-gray-50 py-8" dir="rtl">

View File

@ -1,18 +1,19 @@
'use client'; "use client";
import { useState } from 'react'; import { useState } from "react";
import { motion } from 'framer-motion'; import { motion } from "framer-motion";
import { useRouter } from 'next/navigation'; import { useRouter } from "next/navigation";
import toast, { Toaster } from 'react-hot-toast'; import toast, { Toaster } from "react-hot-toast";
import { Lock, Eye, EyeOff, ArrowLeft, Shield } from 'lucide-react'; import { Lock, Eye, EyeOff, ArrowLeft, Shield } from "lucide-react";
import { useTranslation } from 'react-i18next'; import { useTranslation } from "react-i18next";
import { changePassword } from '../utils/api'; import { changePassword } from "../utils/api";
import AuthService from '../services/AuthService'; import AuthService from "../services/AuthService";
import InteractiveBackground from "../components/Animation/Background";
export default function ChangePasswordPage() { export default function ChangePasswordPage() {
const router = useRouter(); const router = useRouter();
const { t } = useTranslation(); const { t } = useTranslation();
const [form, setForm] = useState({ oldPassword: '', newPassword: '', confirmPassword: '' }); const [form, setForm] = useState({ oldPassword: "", newPassword: "", confirmPassword: "" });
const [show, setShow] = useState({ old: false, new: false, confirm: false }); const [show, setShow] = useState({ old: false, new: false, confirm: false });
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@ -24,162 +25,124 @@ export default function ChangePasswordPage() {
e.preventDefault(); e.preventDefault();
if (!AuthService.isAuthenticated()) { if (!AuthService.isAuthenticated()) {
toast.error(t('changePassword.loginRequired')); toast.error(t("changePassword.loginRequired"));
router.push('/login'); router.push("/login");
return; return;
} }
if (form.newPassword !== form.confirmPassword) { if (form.newPassword !== form.confirmPassword) {
toast.error(t('changePassword.passwordsDoNotMatch')); toast.error(t("changePassword.passwordsDoNotMatch"));
return; return;
} }
if (form.newPassword.length < 6) { if (form.newPassword.length < 6) {
toast.error(t('validation.passwordMinLength')); toast.error(t("validation.passwordMinLength"));
return; return;
} }
setIsLoading(true); setIsLoading(true);
try { try {
await changePassword(form.oldPassword, form.newPassword); await changePassword(form.oldPassword, form.newPassword);
toast.success(t('changePassword.changeSuccess')); toast.success(t("changePassword.changeSuccess"));
setTimeout(() => router.push('/profile'), 1200); setTimeout(() => router.push("/profile"), 1200);
} catch (err) { } catch (err) {
toast.error(err?.message || t('changePassword.changeFailed')); toast.error(err?.message || t("changePassword.changeFailed"));
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}; };
const inputClass = "w-full pr-12 pl-4 py-3 bg-gray-50 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400 transition-all"; const inputClass =
"w-full pr-12 pl-4 py-3 bg-gray-50 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400 transition-all";
return ( return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4"> <div className="min-h-screen relative z-10 flex items-center justify-center p-4" style={{ backgroundColor: "#0f172a" }}>
<Toaster position="top-center" reverseOrder={false} /> <Toaster position="top-center" reverseOrder={false} />
<InteractiveBackground />
<motion.div <motion.div initial={{ opacity: 0, y: 30 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }} className="w-full max-w-md relative z-10 ">
initial={{ opacity: 0, y: 30 }} <motion.div initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} className="mb-6 relative z-10">
animate={{ opacity: 1, y: 0 }} <button onClick={() => router.push("/profile")} className="flex items-center relative z-10 gap-2 text-gray-600 hover:text-amber-600 transition-colors">
transition={{ duration: 0.5 }}
className="w-full max-w-md"
>
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
className="mb-6"
>
<button
onClick={() => router.push('/profile')}
className="flex items-center gap-2 text-gray-600 hover:text-amber-600 transition-colors"
>
<ArrowLeft className="w-5 h-5" /> <ArrowLeft className="w-5 h-5" />
<span>{t('changePassword.backToProfile')}</span> <span>{t("changePassword.backToProfile")}</span>
</button> </button>
</motion.div> </motion.div>
<div className="bg-white rounded-3xl shadow-xl overflow-hidden"> <div className=" rounded-3xl shadow-xl overflow-hidden" style={{ backgroundColor: "#1e293b" }}>
<div className="bg-gradient-to-l from-amber-500 to-amber-600 p-8 text-center"> <div className="bg-gradient-to-l from-amber-500 to-amber-600 p-8 text-center">
<motion.div <motion.div
initial={{ scale: 0 }} initial={{ scale: 0 }}
animate={{ scale: 1 }} animate={{ scale: 1 }}
transition={{ type: 'spring', stiffness: 200 }} transition={{ type: "spring", stiffness: 200 }}
className="w-16 h-16 bg-white/20 rounded-full flex items-center justify-center mx-auto mb-4" className="w-16 h-16 bg-white/20 rounded-full flex items-center justify-center mx-auto mb-4"
> >
<Shield className="w-8 h-8 text-white" /> <Shield className="w-8 h-8 text-white" />
</motion.div> </motion.div>
<motion.h1 <motion.h1 initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} className="text-3xl font-bold text-white mb-2">
initial={{ y: 20, opacity: 0 }} {t("changePassword.title")}
animate={{ y: 0, opacity: 1 }}
className="text-3xl font-bold text-white mb-2"
>
{t('changePassword.title')}
</motion.h1> </motion.h1>
<motion.p <motion.p initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} transition={{ delay: 0.1 }} className="text-amber-100">
initial={{ y: 20, opacity: 0 }} {t("changePassword.subtitle")}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.1 }}
className="text-amber-100"
>
{t('changePassword.subtitle')}
</motion.p> </motion.p>
</div> </div>
<form onSubmit={handleSubmit} className="p-8 space-y-6"> <form onSubmit={handleSubmit} className="p-8 space-y-6 " style={{ backgroundColor: "#0f172a" }}>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">{t("changePassword.currentPasswordLabel")}</label>
{t('changePassword.currentPasswordLabel')}
</label>
<div className="relative"> <div className="relative">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"> <div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className="w-5 h-5 text-gray-400" /> <Lock className="w-5 h-5 text-gray-400" />
</div> </div>
<input <input
type={show.old ? 'text' : 'password'} type={show.old ? "text" : "password"}
value={form.oldPassword} value={form.oldPassword}
onChange={handleChange('oldPassword')} onChange={handleChange("oldPassword")}
className={inputClass} className={inputClass}
placeholder={t('changePassword.currentPasswordPlaceholder')} placeholder={t("changePassword.currentPasswordPlaceholder")}
required required
/> />
<button <button type="button" onClick={() => toggleShow("old")} className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600">
type="button"
onClick={() => toggleShow('old')}
className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600"
>
{show.old ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />} {show.old ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button> </button>
</div> </div>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">{t("changePassword.newPasswordLabel")}</label>
{t('changePassword.newPasswordLabel')}
</label>
<div className="relative"> <div className="relative">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"> <div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className="w-5 h-5 text-gray-400" /> <Lock className="w-5 h-5 text-gray-400" />
</div> </div>
<input <input
type={show.new ? 'text' : 'password'} type={show.new ? "text" : "password"}
value={form.newPassword} value={form.newPassword}
onChange={handleChange('newPassword')} onChange={handleChange("newPassword")}
className={inputClass} className={inputClass}
placeholder={t('changePassword.newPasswordPlaceholder')} placeholder={t("changePassword.newPasswordPlaceholder")}
required required
minLength={6} minLength={6}
/> />
<button <button type="button" onClick={() => toggleShow("new")} className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600">
type="button"
onClick={() => toggleShow('new')}
className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600"
>
{show.new ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />} {show.new ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button> </button>
</div> </div>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">{t("changePassword.confirmNewPasswordLabel")}</label>
{t('changePassword.confirmNewPasswordLabel')}
</label>
<div className="relative"> <div className="relative">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"> <div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className="w-5 h-5 text-gray-400" /> <Lock className="w-5 h-5 text-gray-400" />
</div> </div>
<input <input
type={show.confirm ? 'text' : 'password'} type={show.confirm ? "text" : "password"}
value={form.confirmPassword} value={form.confirmPassword}
onChange={handleChange('confirmPassword')} onChange={handleChange("confirmPassword")}
className={inputClass} className={inputClass}
placeholder={t('changePassword.confirmPasswordPlaceholder')} placeholder={t("changePassword.confirmPasswordPlaceholder")}
required required
minLength={6} minLength={6}
/> />
<button <button type="button" onClick={() => toggleShow("confirm")} className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600">
type="button"
onClick={() => toggleShow('confirm')}
className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600"
>
{show.confirm ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />} {show.confirm ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button> </button>
</div> </div>
@ -195,10 +158,10 @@ export default function ChangePasswordPage() {
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" /> <div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
<span>{t('changePassword.saving')}</span> <span>{t("changePassword.saving")}</span>
</div> </div>
) : ( ) : (
t('changePassword.changePassword') t("changePassword.changePassword")
)} )}
</motion.button> </motion.button>
</form> </form>

View 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>
);
}

View File

@ -0,0 +1,70 @@
import { motion } from "framer-motion";
export const particles = Array.from({ length: 20 }, (_, i) => ({
id: i,
x: Math.random() * 100,
y: Math.random() * 100,
size: Math.random() * 3 + 1,
duration: Math.random() * 15 + 10,
delay: Math.random() * 5,
}));
export const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.1, delayChildren: 0.2 },
},
};
export const itemVariants = {
hidden: { y: 20, opacity: 0 },
visible: {
y: 0,
opacity: 1,
transition: { type: "spring", stiffness: 100 },
},
};
export default function HeaderAnimation() {
return (
<>
<div className="absolute inset-0 overflow-hidden">
{particles.map((p) => (
<motion.div
key={p.id}
className="absolute rounded-full bg-amber-500/20"
style={{
left: `${p.x}%`,
top: `${p.y}%`,
width: p.size,
height: p.size,
}}
animate={{
y: [0, -20, 0],
x: [0, 10, -10, 0],
opacity: [0.2, 0.4, 0.2],
}}
transition={{
duration: p.duration,
repeat: Infinity,
delay: p.delay,
ease: "linear",
}}
/>
))}
</div>
{/* Glow orbs */}
<motion.div
className="absolute top-20 left-20 w-64 h-64 bg-amber-500/10 rounded-full blur-3xl"
animate={{ scale: [1, 1.2, 1], x: [0, 30, 0], y: [0, -20, 0] }}
transition={{ duration: 12, repeat: Infinity }}
/>
<motion.div
className="absolute bottom-20 right-20 w-80 h-80 bg-blue-500/10 rounded-full blur-3xl"
animate={{ scale: [1, 1.3, 1], x: [0, -30, 0], y: [0, 20, 0] }}
transition={{ duration: 15, repeat: Infinity }}
/>
</>
);
}

View File

@ -0,0 +1,26 @@
export const fadeInUp = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.5 },
};
export const staggerContainer = {
animate: { transition: { staggerChildren: 0.1 } },
};
import React from "react";
export const BackgroundElements = () => {
const circles = [
{ style: { top: "20%", right: "20%", width: "256px", height: "256px" }, className: "bg-amber-500/5" },
{ style: { bottom: "10%", left: "20%", width: "320px", height: "320px" }, className: "bg-blue-500/5" },
{ style: { bottom: "40%", left: "10%", width: "320px", height: "320px" }, className: "bg-blue-500/5" },
{ style: { bottom: "60%", left: "60%", width: "320px", height: "320px" }, className: "bg-blue-500/5" }
];
return (
<>
{circles.map((circle, index) => (
<div key={index} style={circle.style} className={`absolute rounded-full pointer-events-none ${circle.className}`} />
))}
</>
);
};

View File

@ -2,10 +2,9 @@
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { Home, Building, Calendar, CreditCard, Briefcase, BookOpen } from "lucide-react"; import { Home, Building2, Building, CalendarCheck, CalendarDays, ClipboardList, CreditCard, BookOpenCheck } from "lucide-react";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
export default function BottomNav({ isOwner, isOwnerOrAgent }) { export default function BottomNav({ isOwner, isOwnerOrAgent }) {
const { t } = useTranslation(); const { t } = useTranslation();
const pathname = usePathname(); const pathname = usePathname();
@ -18,11 +17,20 @@ export default function BottomNav({ isOwner, isOwnerOrAgent }) {
const items = [ const items = [
{ href: "/", label: t("home"), icon: Home }, { href: "/", label: t("home"), icon: Home },
{ href: "/properties", label: t("ourProperties"), icon: Building }, { href: "/properties", label: t("ourProperties"), icon: Building2 },
...(isOwnerOrAgent ? [{ href: "/owner/properties", label: t("myProperties"), icon: Briefcase }] : []),
{ href: bookingsHref, label: t("reservations"), icon: Calendar }, ...(isOwnerOrAgent
? [
{ href: "/owner/properties", label: t("myProperties"), icon: Building },
{ href: "/owner/bookings", label: t("Myseizedproperties"), icon: CalendarCheck },
{ href: "/reservations", label: t("Mybookings"), icon: CalendarDays },
]
: [{ href: "/booked-properties", label: t("Mybookings"), icon: CalendarDays }]),
{ href: bookingsHref, label: t("Orders"), icon: ClipboardList },
{ href: "/payments", label: t("payments"), icon: CreditCard }, { href: "/payments", label: t("payments"), icon: CreditCard },
...(isOwnerOrAgent ? [{ href: "/owner/account-book", label: t("accountBook"), icon: BookOpen }] : []),
...(isOwnerOrAgent ? [{ href: "/owner/account-book", label: t("accountBook"), icon: BookOpenCheck }] : []),
]; ];
const isActive = (href) => { const isActive = (href) => {
@ -31,29 +39,67 @@ export default function BottomNav({ isOwner, isOwnerOrAgent }) {
}; };
return ( return (
<div className="fixed bottom-0 left-0 right-0 z-50 flex justify-center pb-4 pt-2 pointer-events-none"> <div className="fixed bottom-0 left-0 right-0 z-50 flex justify-center px-5 pb-4 pt-4 pointer-events-none">
<nav className="bg-gray-50 rounded-3xl px-4 py-2 shadow-[0_4px_24px_rgba(251,146,60,0.25)] pointer-events-auto"> <nav
<div className="flex items-center justify-center gap-1"> className="
w-fit max-w-full
rounded-3xl
bg-gray-50
px-5 py-3 sm:px-4
shadow-[0_4px_24px_rgba(251,146,60,0.25)]
pointer-events-auto
"
>
<div
className="flex items-center sm:justify-center gap-0.5 sm:gap-1 overflow-x-auto max-w-full
[&::-webkit-scrollbar]:h-1.5
[&::-webkit-scrollbar-track]:bg-gray-100
[&::-webkit-scrollbar-thumb]:bg-gray-700
[&::-webkit-scrollbar-thumb]:rounded-full hover:[&::-webkit-scrollbar-thumb]:bg-gray-700"
>
{items.map((it) => { {items.map((it) => {
const Icon = it.icon; const Icon = it.icon;
const active = isActive(it.href); const active = isActive(it.href);
return ( return (
<Link <Link
key={it.href} key={it.href}
href={it.href} href={it.href}
className={`relative flex flex-col items-center justify-center px-3 py-2 rounded-xl transition-all duration-200 ease-out hover:shadow-md hover:bg-white/80 ${active ? "bg-white shadow-sm text-amber-600" : "text-gray-700 hover:text-amber-600"}`} style={{ padding: "3px", width: "115px" }}
className={`
flex
flex-col
items-center
justify-center
rounded-xl
transition-all
duration-200
ease-out
${active ? "bg-white shadow-sm text-amber-600" : "text-gray-700 hover:bg-white/80 hover:text-amber-600 hover:shadow-md"}
`}
> >
<div className="relative"> <div className="relative">
<Icon className="w-6 h-6" /> <Icon className="h-5 w-5 sm:h-6 sm:w-6" />
{it.badge > 0 && ( {it.badge > 0 && (
<div className="absolute -top-2 -right-2 bg-red-500 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center"> <div
className="
absolute -right-2 -top-2
flex h-6 w-7 sm:h-5 sm:w-5
items-center justify-center
rounded-full
bg-red-500
text-[9px] sm:text-xs
font-bold
text-white
"
>
{it.badge > 9 ? "9+" : it.badge} {it.badge > 9 ? "9+" : it.badge}
</div> </div>
)} )}
</div> </div>
<div className="mt-1 text-xs text-center" aria-hidden>
{it.label} <div className="mt-0.5 text-[10px] sm:mt-1 text-center sm:text-xs">{it.label}</div>
</div>
</Link> </Link>
); );
})} })}

View File

@ -0,0 +1,17 @@
import { motion } from "framer-motion";
import { useTranslation } from "react-i18next";
import { fadeInUp } from "./Animation/OwnerPageAnimation";
import Link from "next/link";
export default function HavAccount() {
const { t } = useTranslation();
return (
<>
<motion.p variants={fadeInUp} className="text-center text-gray-400 mt-4">
{t("register.haveAccount")}
<Link href="/login" className="text-blue-400 hover:text-blue-300 font-medium transition-colors">
{t("register.loginLink")}
</Link>
</motion.p>
</>
);
}

View File

@ -0,0 +1,17 @@
import { motion } from 'framer-motion';
import { itemVariants } from '../Animation/HeaderLoginAnimation';
import Link from 'next/link';
import { useTranslation } from 'react-i18next';
export default function CreateNewAccount() {
const {t}=useTranslation()
return (
<>
<motion.p variants={itemVariants} className="text-center text-gray-400 mt-6">
{t("noAccount")}{" "}
<Link href="/auth/choose-role" className="text-amber-400 hover:text-amber-300 font-medium transition-colors">
{t("createNewAccount")}
</Link>
</motion.p>
</>
);
}

View File

@ -0,0 +1,45 @@
import { motion } from "framer-motion";
import { itemVariants } from "../Animation/HeaderLoginAnimation";
import { ArrowLeft, KeyRound,Home } from "lucide-react";
import { useTranslation } from "react-i18next";
import Link from "next/link";
//Back Link
export function BackLink() {
const { t } = useTranslation();
return (
<>
<motion.div variants={itemVariants} className="absolute -top-16 left-0">
<Link href="/" className="group flex items-center gap-2 text-gray-400 hover:text-white transition-colors">
<motion.div whileHover={{ x: -5 }} transition={{ type: "spring", stiffness: 400 }}>
<ArrowLeft className="w-4 h-4" />
</motion.div>
<span>{t("backToHome")}</span>
</Link>
</motion.div>
</>
);
}
export function HeaderForm({ step }) {
const { t } = useTranslation();
return (
<>
<div className="bg-gradient-to-r from-amber-700 to-amber-400 p-8 text-center relative overflow-hidden">
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ delay: 0.2, type: "spring" }} className="absolute -top-10 -right-10 w-40 h-40 bg-white/10 rounded-full" />
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ delay: 0.3, type: "spring" }} className="absolute -bottom-10 -left-10 w-40 h-40 bg-white/10 rounded-full" />
<motion.div initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} transition={{ delay: 0.2 }} className="relative z-10">
<motion.div
animate={{ rotate: [0, 10, -10, 0] }}
transition={{ duration: 2, repeat: Infinity }}
className="w-20 h-20 mx-auto mb-4 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-sm"
>
{step === "otp" ? <KeyRound className="w-10 h-10 text-white" /> : <Home className="w-10 h-10 text-white" />}
</motion.div>
<h1 className="text-3xl font-bold text-white mb-2">SweetHome</h1>
<p className="text-amber-100">{step === "otp" ? t("enterOTP") : t("welcomeBack")}</p>
</motion.div>
</div>
</>
);
}

View File

@ -0,0 +1,127 @@
"use client";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { motion, AnimatePresence } from "framer-motion";
import toast, { Toaster } from "react-hot-toast";
import { useRouter } from "next/navigation";
import AuthService from "../../services/AuthService";
import InteractiveBackground from "../Animation/Background"
import { BackLink, HeaderForm } from "./HeaderLogin";
import HeaderAnimation, { containerVariants, itemVariants } from "../Animation/HeaderLoginAnimation";
import TabsLogin from "./Tabs";
import LoginForm from "./LoginForm";
import CreateNewAccount from "./CreateNewAccount";
import PolicyAndPrivecy from "./PolicyAndPrivecy";
import OtpComponent from "./OtpComponent";
export default function Login() {
const router = useRouter();
const { t } = useTranslation();
const [step, setStep] = useState("login");
const [loginMethod, setLoginMethod] = useState("email");
const [isLoading, setIsLoading] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [formData, setFormData] = useState({
credential: "", //email or phoneNumber
password: "",
rememberMe: false,
});
const [errors, setErrors] = useState({});
const validateForm = () => {
const newErrors = {};
if (!formData.credential) {
newErrors.credential = loginMethod === "email" ? t("emailRequired") : t("phoneRequired");
}
if (!formData.password) {
newErrors.password = t("passwordRequired");
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleLogin = async (e) => {
e.preventDefault();
if (!validateForm()) return;
setIsLoading(true);
setErrors({});
try {
const response = await AuthService.login({
loginMethod,
credential: formData.credential,
password: formData.password,
});
switch (response.type) {
case "SUCCESS":
toast.success(t("loginSuccess"));
router.push("/");
break;
case "OTP_REQUIRED":
toast(t("otpRequired"));
setStep("otp");
break;
default:
toast.error("Login failed the email or password is wrong");
}
} catch (err) {
console.error("[Login] Error:", err);
toast.error(err.message || t("connectionError"), {
style: { background: "#fee2e2", color: "#991b1b" },
});
} finally {
setIsLoading(false);
}
};
// Auto-detect login method from input
const handleCredentialChange = (value) => {
setFormData({ ...formData, credential: value });
if (errors.credential) setErrors({ ...errors, credential: null });
};
const handelTabs = (type) => {
setLoginMethod(type);
setFormData({ ...formData, credential: "" });
};
return (
<div className="min-h-screen flex items-center justify-center p-4 relative overflow-hidden">
<Toaster position="top-center" reverseOrder={false} />
<InteractiveBackground />
{/* Particles */}
<HeaderAnimation />
<motion.div variants={containerVariants} initial="hidden" animate="visible" className="relative w-full max-w-md z-10">
{/* Back link */}
<BackLink />
<motion.div variants={itemVariants} className="bg-white/10 backdrop-blur-2xl rounded-3xl shadow-2xl border border-white/10 overflow-hidden">
{/* Header */}
<HeaderForm step={step} />
<div className="p-8">
<AnimatePresence mode="wait">
{step === "login" ? (
<motion.form key="login" initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: 20 }} onSubmit={handleLogin} className="space-y-6">
{/* Login method tabs */}
<TabsLogin loginMethod={loginMethod} handelTabs={handelTabs} />
{/* Form Login */}
<LoginForm
loginMethod={loginMethod}
errors={errors}
formData={formData}
isLoading={isLoading}
isSuccess={isSuccess}
setFormData={setFormData}
handleCredentialChange={handleCredentialChange}
/>
</motion.form>
) : (
// /* OTP Verification Step */
<OtpComponent isLoading={isLoading} setIsLoading={setIsLoading} isSuccess={isSuccess} setIsSuccess={setIsSuccess} loginMethod={loginMethod} formData={formData} setStep={setStep} />
)}
</AnimatePresence>
<CreateNewAccount />
</div>
</motion.div>
<PolicyAndPrivecy />
</motion.div>
</div>
);
}

View File

@ -0,0 +1,141 @@
import { EyeOff, Mail, Phone, Eye, LogIn, Lock, Loader2, CheckCircle } from "lucide-react";
import { useTranslation } from "react-i18next";
import { motion } from "framer-motion";
import Link from "next/link";
import { useState } from "react";
export default function LoginForm({ loginMethod, errors, formData, isLoading, isSuccess, setFormData, handleCredentialChange }) {
const { t } = useTranslation();
const [showPassword, setShowPassword] = useState(false);
return (
<>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">{loginMethod === "email" ? t("emailLabel") : t("phoneLabel")}</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
{loginMethod === "email" ? (
<Mail className={`w-5 h-5 transition-colors ${errors.credential ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
) : (
<Phone className={`w-5 h-5 transition-colors ${errors.credential ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
)}
</div>
{loginMethod === "email" ? (
<>
<input
type="email"
// type={loginMethod === 'email' ? 'email' : 'tel'}
value={formData.credential}
onChange={(e) => handleCredentialChange(e.target.value)}
className={`w-full pr-12 pl-4 py-4 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${
errors.credential ? "border-red-500" : "border-gray-700"
}`}
placeholder={t("emailPlaceholder")}
dir="ltr"
/>
</>
) : (
<>
<input
type="text"
value={formData.credential}
onChange={(e) => handleCredentialChange(e.target.value)}
className={`w-full pr-12 pl-4 py-4 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${
errors.credential ? "border-red-500" : "border-gray-700"
}`}
placeholder={t("phonePlaceholder")}
dir="ltr"
/>
</>
)}
</div>
{errors.credential && (
<motion.p initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} className="text-red-500 text-sm mt-1">
{errors.credential}
</motion.p>
)}
</div>
{/* Password */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">{t("password")}</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className={`w-5 h-5 transition-colors ${errors.password ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
</div>
<input
type={showPassword ? "text" : "password"}
value={formData.password}
onChange={(e) => {
setFormData({
...formData,
password: e.target.value,
});
if (errors.password) setErrors({ ...errors, password: null });
}}
className={`w-full pr-12 pl-12 py-4 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${
errors.password ? "border-red-500" : "border-gray-700"
}`}
placeholder={t("passwordPlaceholder")}
/>
<button type="button" onClick={() => setShowPassword(!showPassword)} className="absolute inset-y-0 left-0 pl-3 flex items-center">
{showPassword ? <EyeOff className="w-5 h-5 text-gray-400 hover:text-gray-300 transition-colors" /> : <Eye className="w-5 h-5 text-gray-400 hover:text-gray-300 transition-colors" />}
</button>
</div>
{errors.password && (
<motion.p initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} className="text-red-500 text-sm mt-1">
{errors.password}
</motion.p>
)}
</div>
{/* Remember + Forgot */}
<div className="flex items-center justify-between">
<label className="flex items-center gap-2 cursor-pointer group">
<input
type="checkbox"
checked={formData.rememberMe}
onChange={(e) =>
setFormData({
...formData,
rememberMe: e.target.checked,
})
}
className="w-4 h-4 rounded border-gray-600 bg-white/5 text-amber-500 focus:ring-amber-500 focus:ring-offset-0"
/>
<span className="text-sm text-gray-400 group-hover:text-white transition-colors">{t("rememberMe")}</span>
</label>
<Link href="/forgot-password" className="text-sm text-amber-400 hover:text-amber-300 transition-colors">
{t("forgotPassword")}
</Link>
</div>
{/* Submit */}
<motion.button
type="submit"
disabled={isLoading || isSuccess}
className="relative w-full bg-gradient-to-r from-amber-500 to-amber-600 text-white py-4 rounded-xl font-bold text-lg overflow-hidden group disabled:opacity-50 disabled:cursor-not-allowed"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<span className="relative z-10 flex items-center justify-center gap-2">
{isLoading ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
{t("loggingIn")}{" "}
</>
) : isSuccess ? (
<>
<CheckCircle className="w-5 h-5" /> {t("success")}{" "}
</>
) : (
<>
{" "}
<LogIn className="w-5 h-5" />
{t("login")}
</>
)}
</span>
</motion.button>
</>
);
}

View File

@ -0,0 +1,143 @@
"use client";
import { CheckCircle, KeyRound, Loader2, Shield } from "lucide-react";
import { useTranslation } from "react-i18next";
import { motion } from "framer-motion";
import { sendEmailOTP, sendPhoneOTP, verifyEmail, verifyPhone, getOwnerByUserId, getCustomerByUserId } from "@/app/utils/api";
import AuthService from "@/app/services/AuthService";
import { useState } from "react";
import { useRouter } from "next/navigation";
import toast from "react-hot-toast";
export default function OtpComponent({ isLoading, setIsLoading, isSuccess, setIsSuccess, loginMethod, formData, setStep }) {
const { t } = useTranslation();
const router = useRouter();
const [otpCode, setOtpCode] = useState("");
const [otpError, setOtpError] = useState("");
const handleVerifyOTP = async (e) => {
e.preventDefault();
if (!otpCode || otpCode.length < 4) {
setOtpError(t("otpRequired"));
return;
}
setIsLoading(true);
setOtpError("");
try {
const verifyFn = loginMethod === "email" ? verifyEmail : verifyPhone;
const result = await verifyFn(otpCode);
if (result?.ok) {
try {
await AuthService.cacheCurrentUser(getOwnerByUserId, getCustomerByUserId);
} catch (err) {
console.warn("Failed to cache user", err);
}
}
setIsSuccess(true);
toast.success(t("verifySuccess"));
setTimeout(() => {
router.push("/");
}, 1500);
} catch (err) {
console.error("[OTP] Error:", err);
setOtpError(err.message || t("verifyError"));
} finally {
setIsLoading(false);
}
};
const resendOTP = async () => {
try {
if (loginMethod === "email") {
await sendEmailOTP();
} else {
await sendPhoneOTP();
}
toast.success(t("resendSuccess"), {
style: { background: "#dcfce7", color: "#166534" },
});
} catch (err) {
console.error("[OTP] Resend failed:", err);
toast.error(t("resendFailed"));
}
};
return (
<motion.form key="otp" initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} onSubmit={handleVerifyOTP} className="space-y-6">
<div className="text-center mb-4">
<Shield className="w-12 h-12 text-amber-500 mx-auto mb-3" />
<p className="text-amber-300 text-sm">
{t("sendOTPTo")}{" "}
<span className="text-amber-50 font-medium" dir="ltr">
{formData?.credential}
</span>
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">{t("otpLabel")}</label>
<input
type="text"
value={otpCode}
onChange={(e) => {
setOtpCode(e.target.value);
if (otpError) setOtpError("");
}}
className={`w-full px-4 py-4 bg-gray-800 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white text-center text-2xl tracking-[0.5em] placeholder-gray-500 transition-all ${
otpError ? "border-red-500" : "border-gray-700"
}`}
placeholder="______"
maxLength={6}
dir="ltr"
/>
{otpError && (
<motion.p initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} className="text-red-500 text-sm mt-1 text-center">
{otpError}
</motion.p>
)}
</div>
<motion.button
type="submit"
disabled={isLoading || isSuccess}
className="relative w-full bg-gradient-to-r from-amber-500 to-amber-600 text-white py-4 rounded-xl font-bold text-lg disabled:opacity-50 disabled:cursor-not-allowed"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<span className="flex items-center justify-center gap-2">
{isLoading ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
{t("verifying")}
</>
) : isSuccess ? (
<>
<CheckCircle className="w-5 h-5" />
{t("success")}
</>
) : (
<>
<KeyRound className="w-5 h-5" />
{t("verify")}
</>
)}
</span>
</motion.button>
<div className="flex items-center justify-between text-sm">
<button
type="button"
onClick={() => {
setStep("login");
setOtpCode("");
setOtpError("");
}}
className="text-gray-400 hover:text-white transition-colors"
>
{t("back")}
</button>
<button type="button" onClick={resendOTP} className="text-amber-400 hover:text-amber-300 transition-colors">
{t("resendCode")}
</button>
</div>
</motion.form>
);
}

View File

@ -0,0 +1,21 @@
import { motion } from "framer-motion";
import { itemVariants } from "../Animation/HeaderLoginAnimation";
import { useTranslation } from "react-i18next";
import Link from "next/link";
export default function PolicyAndPrivecy() {
const { t } = useTranslation();
return (
<>
<motion.p variants={itemVariants} className="text-center text-gray-500 text-xs mt-4">
{t("agreeTerms")}{" "}
<Link href="/terms" className="text-amber-400 hover:text-amber-300 transition-colors">
{t("termsOfUse")}
</Link>{" "}
{t("and")}{" "}
<Link href="/privacy" className="text-amber-400 hover:text-amber-300 transition-colors">
{t("privacyPolicy")}
</Link>
</motion.p>
</>
);
}

View File

@ -0,0 +1,36 @@
import { Mail, Phone } from "lucide-react";
import { useTranslation } from "react-i18next";
export default function TabsLogin({ loginMethod, handelTabs }) {
const { t } = useTranslation();
return (
<>
<div className="flex gap-2 bg-white/5 p-1 rounded-xl">
<button
type="button"
onClick={() => {
handelTabs("email");
}}
className={`flex-1 py-2.5 rounded-lg text-sm font-medium transition-all flex items-center justify-center gap-2 ${
loginMethod === "email" ? "bg-amber-500 text-white shadow-lg" : "text-gray-400 hover:text-white"
}`}
>
<Mail className="w-4 h-4" />
{t("emailMethod")}
</button>
<button
type="button"
onClick={() => {
handelTabs("phone");
}}
className={`flex-1 py-2.5 rounded-lg text-sm font-medium transition-all flex items-center justify-center gap-2 ${
loginMethod === "phone" ? "bg-amber-500 text-white shadow-lg" : "text-gray-400 hover:text-white"
}`}
>
<Phone className="w-4 h-4" />
{t("phoneMethod")}
</button>
</div>
</>
);
}

View File

@ -1,11 +1,11 @@
"use client"; "use client";
import { useEffect, useState, useRef } from "react"; import { useEffect, useState, useRef } from "react";
import { initializeApp, getApps } from "firebase/app"; import { initializeApp, getApps } from "firebase/app";
import { getMessaging, getToken, onMessage } from "firebase/messaging"; import { getMessaging, getToken, onMessage } from "firebase/messaging";
import AuthService from "../services/AuthService"; import AuthService from "../services/AuthService";
import { setFCMToken } from "../utils/api"; import { setFCMToken } from "../utils/api";
//فاير بيز
const firebaseConfig = { const firebaseConfig = {
apiKey: "AIzaSyBZV7KBLRJSTApahfrO8lBesmIM3zNRSaY", apiKey: "AIzaSyBZV7KBLRJSTApahfrO8lBesmIM3zNRSaY",
authDomain: "sweet-home-b2766.firebaseapp.com", authDomain: "sweet-home-b2766.firebaseapp.com",
@ -16,6 +16,7 @@ const firebaseConfig = {
measurementId: "G-M2V95NBJLX", measurementId: "G-M2V95NBJLX",
}; };
//عم نعمل تهيئة للاعدادات
const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0]; const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
export default function NotificationHandler() { export default function NotificationHandler() {
@ -26,11 +27,10 @@ export default function NotificationHandler() {
useEffect(() => { useEffect(() => {
function checkAuth() { function checkAuth() {
if (initialized.current) return; if (initialized.current) return;
if (!AuthService.getToken()) return; if (!AuthService.getToken()) return;
initialized.current = true; initialized.current = true;
if ("Notification" in window) { if ("Notification" in window) {
//عم نشوف اذا حط سماح او رفض
if (Notification.permission === "default") { if (Notification.permission === "default") {
setShowPrompt(true); setShowPrompt(true);
} else if (Notification.permission === "granted") { } else if (Notification.permission === "granted") {

View File

@ -0,0 +1,25 @@
import { useRouter } from "next/navigation";
import { useTranslation } from "react-i18next";
export default function ButtomStepOne({ type }) {
const router = useRouter();
const { t } = useTranslation();
return (
<>
<>
<button
type="button"
onClick={() => router.push("/auth/choose-role")}
className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors"
>
{t("register.cancel")}
</button>
<button
type="submit"
className={`flex-1 bg-gradient-to-r ${type === "owner" ? " from-amber-500 to-amber-600" : " from-blue-500 to-blue-600"} text-white py-3 px-4 rounded-xl font-medium ${type === "onwer" ? "hover:from-amber-600 hover:to-amber-700" : "hover:from-blue-600 hover:to-blue-700"} transition-all`}
>
{t("register.next")}
</button>
</>
</>
);
}

View File

@ -0,0 +1,27 @@
import { Loader2 } from "lucide-react";
import { useTranslation } from "react-i18next";
export default function ButtomStepTwo({ setStep, isLoading, formData, type }) {
const { t } = useTranslation();
return (
<>
<button type="button" onClick={() => setStep(1)} className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors">
{t("register.previous")}
</button>
<button
type="submit"
disabled={isLoading || !formData.agreeTerms}
className={`flex-1 bg-gradient-to-r${type === "owner" ? " from-amber-500 to-amber-600" : " from-blue-500 to-blue-600"} text-amber-50 py-3 px-4 rounded-xl font-medium${type === "onwer" ? "hover:from-amber-600 hover:to-amber-700" : "hover:from-blue-600 hover:to-blue-700"} transition-all disabled:opacity-50 disabled:cursor-not-allowed`}
>
{isLoading ? (
<div className="flex items-center justify-center gap-2">
<Loader2 className="w-5 h-5 animate-spin" />
<span>{t("register.registering")}</span>
</div>
) : (
t("register.createAccount")
)}
</button>
</>
);
}

View File

@ -0,0 +1,6 @@
export default function FormOwner(){
return(<>
</>)
}

View File

@ -0,0 +1,44 @@
"use client";
import { useTranslation } from "react-i18next";
import { motion } from "framer-motion";
import { Building, Home } from "lucide-react";
const data = [
{
colors: "from-amber-500 to-amber-600",
titleKey: "register.owner.step1Title",
title2Key: "register.owner.step1bTitle",
descKey: "register.owner.step1Desc",
desc2Key: "register.owner.step1bDesc",
icon: <Building className="w-10 h-10 text-white" />,
},
{
colors: "from-blue-500 to-blue-600",
titleKey: "register.tenant.step1Title",
title2Key: "register.tenant.step2Title",
descKey: "register.tenant.step1Desc",
desc2Key: "register.tenant.step2Desc",
icon: <Home className="w-10 h-10 text-white" />,
},
];
export default function HeaderOfForm({ step = 1, num = 0 }) {
const { t } = useTranslation();
const currentData = data[num] || data[0];
return (
<div className={`bg-gradient-to-r ${currentData.colors} p-8 text-center relative overflow-hidden`}>
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ delay: 0.2, type: "spring" }} className="absolute -top-10 -right-10 w-40 h-40 bg-white/10 rounded-full" />
<motion.div initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} className="relative z-10">
<motion.div
animate={{ rotate: [0, 10, -10, 0] }}
transition={{ duration: 2, repeat: Infinity }}
className="w-20 h-20 mx-auto mb-4 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-sm"
>
{data[num].icon}
</motion.div>
<h1 className="text-3xl font-bold text-white mb-2">{t(step === 1 ? t(currentData.titleKey) : t(currentData.title2Key))}</h1>
<p className="text-amber-100">{t(step === 1 ? t(currentData.descKey) : t(currentData.desc2Key))}</p>
</motion.div>
</div>
);
}

View File

@ -0,0 +1,28 @@
import Link from "next/link";
import { motion } from "framer-motion";
import { ArrowLeft } from "lucide-react";
import { useTranslation } from "react-i18next";
export default function HeaderOfSteps({ step, colors, numberStep }) {
const { t } = useTranslation();
return (
<>
<div className="mb-8">
<div className="flex items-center justify-between mb-4">
<Link href="/auth/choose-role" className="flex items-center gap-2 text-gray-400 hover:text-white transition-colors group">
<motion.div whileHover={{ x: -5 }}>
<ArrowLeft className="w-4 h-4" />
</motion.div>
<span>{t("forgotPassword.backToLogin")}</span>
</Link>
<span className="text-sm text-gray-200">{t("register.stepOf2", { step: step })}</span>
</div>
<div className="flex gap-2">
{numberStep.map((s) => (
<motion.div key={s} className={`h-2 flex-1 rounded-full ${step >= s ? colors : "bg-gray-800"}`} animate={{ scaleX: step >= s ? 1 : 0.5 }} />
))}
</div>
</div>
</>
);
}

View File

@ -0,0 +1,104 @@
import { KeyRound, Loader2, Shield } from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { motion } from "framer-motion";
import toast, { Toaster } from "react-hot-toast";
import { sendEmailOTP, verifyEmail } from "@/app/utils/api";
import AuthService from "@/app/services/AuthService";
import { useRouter } from "next/navigation";
export default function OtpDialog({ formData, setShowOtpModal, type }) {
const { t } = useTranslation();
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [otpCode, setOtpCode] = useState("");
const handleVerifyOTP = async () => {
if (!otpCode || otpCode.length < 4) {
toast.error(t("register.otpRequired"));
return;
}
setIsLoading(true);
try {
const res = await verifyEmail(otpCode);
if (res.status === 200) {
AuthService.deleteToken();
toast.success(res.message || t("register.emailVerified"));
setShowOtpModal(false);
setTimeout(() => router.push("/login"), 1500);
} else {
toast.error(res.message || res.data?.message || t("accountVerification.otpInvalid"));
}
} catch (err) {
toast.error(err.message || t("register.verificationError"));
} finally {
setIsLoading(false);
}
};
const handleResendOTP = async () => {
setIsLoading(true);
try {
await sendEmailOTP();
toast.success(t("register.newOtpSent"));
} catch (err) {
toast.error(t("register.resendOtpFailed"));
} finally {
setIsLoading(false);
}
};
return (
<>
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 z-50">
<motion.div initial={{ scale: 0.9, y: 20 }} animate={{ scale: 1, y: 0 }} exit={{ scale: 0.9, y: 20 }} className="bg-gray-900 border border-white/10 rounded-2xl w-full max-w-md p-6 shadow-2xl">
<div className="text-center mb-6">
<div className="w-16 h-16 bg-amber-500/20 rounded-full flex items-center justify-center mx-auto mb-3">
<Shield className={`w-8 h-8 ${type === "owner" ? "text-amber-500" : "text-blue-500"} `} />
</div>
<h2 className="text-xl font-bold text-white">{t("register.otpTitle")}</h2>
<p className="text-gray-400 text-sm mt-1">{t("register.otpSentTo")}</p>
<p className={`${type === "owner" ? "text-amber-500" : "text-blue-500"} font-medium text-sm`}>{formData.email}</p>
</div>
<div className="mb-6">
<label className="block text-sm font-medium text-gray-300 mb-2">{t("register.otpLabel")}</label>
<div className="relative">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<KeyRound className="w-5 h-5 text-gray-400" />
</div>
<input
type="text"
value={otpCode}
maxLength={6}
onChange={(e) => setOtpCode(e.target.value)}
className="w-full pr-12 pl-4 py-3 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 text-white text-center tracking-[0.5em] text-xl"
placeholder="------"
/>
</div>
</div>
<div className="flex gap-3">
<button
onClick={handleVerifyOTP}
disabled={isLoading || !otpCode}
className={`flex-1 bg-gradient-to-r ${type === "owner" ? "from-amber-500 to-amber-600" : "from-blue-500 to-blue-600"} text-white py-3 rounded-xl font-medium ${type === "owner" ? "hover:from-amber-600 hover:to-amber-700" : "hover:from-blue-600 hover:to-blue-700"} disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2`}
>
{isLoading ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
<span>{t("register.verifying")}</span>
</>
) : (
t("register.verify")
)}
</button>
</div>
<button
onClick={handleResendOTP}
disabled={isLoading}
className={`w-full text-center ${type === "owner" ? "text-amber-400 hover:text-amber-300" : "text-blue-400 hover:text-blue-300"} text-sm mt-3 disabled:opacity-50`}
>
{t("register.resendOtp")}
</button>
</motion.div>
</motion.div>
</>
);
}

View File

@ -0,0 +1,162 @@
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { useTranslation } from "react-i18next";
import toast, { Toaster } from "react-hot-toast";
import { addOwner, loginWithEmail } from "../../utils/api";
import AuthService from "../../services/AuthService";
import { OwnerType } from "../../enums";
import OtpDialog from "./OtpDialog";
import HeaderOfSteps from "./HeaderOfSteps";
import HeaderOfForm from "./HeaderOfForm";
import { BackgroundElements, fadeInUp, staggerContainer } from "../Animation/OwnerPageAnimation";
import { validateStep1, validateStep2 } from "../../validations/OwnerValidatoin";
import OwnerFormStepOne from "./OwnerFormStepOne";
import ButtomStepOne from "./ButtomStepOne";
import ButtomStepTwo from "./ButtomStepTwo";
import OwnerFormStepTwo from "./OwnerFormStepTwo";
import InteractiveBackground from "../Animation/Background";
export default function Owner() {
const { t } = useTranslation();
const [step, setStep] = useState(1);
const [showOtpModal, setShowOtpModal] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [formData, setFormData] = useState({
firstName: "",
lastName: "",
email: "",
phone: "",
whatsapp: "",
phone2: "",
nationalNumber: "",
password: "",
confirmPassword: "",
ownerType: OwnerType.PERSON,
agreeTerms: false,
});
const [idImages, setIdImages] = useState({ front: null, back: null, license: null });
const [idImagePreviews, setIdImagePreviews] = useState({ front: "", back: "", license: "" });
const [errors, setErrors] = useState({});
const isCompany = Number(formData.ownerType) === OwnerType.REAL_ESTATE_AGENCY;
const handleNextStep = () => {
if (validateStep1(formData, setErrors, t)) {
setStep(2);
window.scrollTo({ top: 0, behavior: "smooth" });
} else {
toast.error(t("validation.correctErrors"));
}
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateStep2(idImages, setErrors, t)) {
toast.error(t("register.completeRequiredImages"));
return;
}
if (!formData.agreeTerms) {
toast.error(t("validation.agreeToTerms"));
return;
}
setIsLoading(true);
const payload = {
firstName: formData.firstName,
lastName: formData.lastName,
email: formData.email,
phoneNumber: formData.phone || "",
whatsAppNumber: formData.whatsapp,
phone: formData.phone2,
nationalNumber: formData.nationalNumber,
password: formData.password,
ownerType: formData.ownerType,
};
try {
const licenseImage = isCompany ? idImages.license : null;
const res = await addOwner(payload, idImages.front, idImages.back,licenseImage);
if (res.status === 200 || res.ok) {
toast.success(res.message || t("register.accountCreated")||res.data[0].message , { duration: 4000 });
const loginRes = await loginWithEmail(formData.email, formData.password);
if (loginRes.status === 206) {
const otpToken = loginRes.data;
if (otpToken) AuthService.addToken(otpToken);
toast(loginRes.message || t("register.otpSentEmail"), { icon: "📧" });
setShowOtpModal(true);
} else if (loginRes.status === 200) {
const loginToken = loginRes.data;
if (loginToken) AuthService.addToken(loginToken);
toast.success(loginRes.message || t("register.loginSuccess"));
}
} else {
toast.error(res.data[0].message || res.data?.message);
}
} catch (err) {
toast.error(err.message || t("register.registrationError"));
} finally {
setIsLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center p-4 relative overflow-hidden">
<Toaster position="top-center" reverseOrder={false} />
<div className="absolute inset-0 overflow-hidden">
<BackgroundElements />
<InteractiveBackground />
</div>
<motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.5 }} className="relative z-10 w-full max-w-2xl">
<HeaderOfSteps step={step} numberStep={[1, 2]} colors={"bg-amber-500"} />
<motion.div
key={step}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
className="bg-white/5 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/10 overflow-hidden"
>
<HeaderOfForm step={step} num={0} />
<div className="p-8">
<motion.form
variants={staggerContainer}
initial="initial"
animate="animate"
onSubmit={
step === 1
? (e) => {
e.preventDefault();
handleNextStep();
}
: handleSubmit
}
className="space-y-6"
>
{step === 1 && (
<OwnerFormStepOne
setErrors={setErrors}
setFormData={setFormData}
formData={formData}
errors={errors}
isCompany={isCompany}
setIdImagePreviews={setIdImagePreviews}
setIdImages={setIdImages}
type={"owner"}
/>
)}
{step === 2 && (
<>
<OwnerFormStepTwo idImagePreviews={idImagePreviews} errors={errors} setIdImagePreviews={setIdImagePreviews} setIdImages={setIdImages} setFormData={setFormData} formData={formData} />
</>
)}
<motion.div variants={fadeInUp} className="flex gap-3 pt-4">
{step === 1 ? ( <> <ButtomStepOne type={"owner"} /></>) : ( <> <ButtomStepTwo formData={formData} isLoading={isLoading} setStep={setStep} type={"owner"} /> </>)}
</motion.div>
</motion.form>
</div>
</motion.div>
</motion.div>
<AnimatePresence>{showOtpModal && <OtpDialog formData={formData} setShowOtpModal={setShowOtpModal} type={"owner"} />}</AnimatePresence>
</div>
);
}

View File

@ -0,0 +1,271 @@
import { useTranslation } from "react-i18next";
import { fadeInUp } from "../Animation/OwnerPageAnimation";
import { motion, AnimatePresence } from "framer-motion";
import { User, Mail, Phone, Lock, Eye, EyeOff, MessageCircle, Camera, X, CheckCircle, XCircle } from "lucide-react";
import { CustomerTypeLabels, OwnerTypeLabels } from "@/app/enums";
import { useRef, useState } from "react";
import { handleImageUpload } from "@/app/HelperFunction/UploadImage";
import UploadImage from "./UploadImage";
export default function OwnerFormStepOne({ formData, errors, setFormData, setErrors, isCompany, idImagePreviews, setIdImagePreviews, setIdImages, type }) {
const { t } = useTranslation();
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const fileInputLicenseRef = useRef(null);
return (
<>
<motion.div variants={fadeInUp} className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
{t("register.firstName")} <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<User className={`w-5 h-5 ${errors.firstName ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
</div>
<input
type="text"
value={formData.firstName}
onChange={(e) => {
setFormData({ ...formData, firstName: e.target.value });
setErrors({ ...errors, firstName: null });
}}
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${errors.firstName ? "border-red-500" : "border-gray-700"}`}
placeholder={t("register.firstName")}
/>
</div>
{errors.firstName && <p className="text-red-500 text-sm mt-1">{errors.firstName}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
{t("register.lastName")} <span className="text-red-500">*</span>
</label>
<input
type="text"
value={formData.lastName}
onChange={(e) => {
setFormData({ ...formData, lastName: e.target.value });
setErrors({ ...errors, lastName: null });
}}
className={`w-full px-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${errors.lastName ? "border-red-500" : "border-gray-700"}`}
placeholder={t("register.lastName")}
/>
{errors.lastName && <p className="text-red-500 text-sm mt-1">{errors.lastName}</p>}
</div>
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
{t("register.email")} <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Mail className={`w-5 h-5 ${errors.email ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
</div>
<input
type="email"
value={formData.email}
onChange={(e) => {
setFormData({ ...formData, email: e.target.value });
setErrors({ ...errors, email: null });
}}
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${errors.email ? "border-red-500" : "border-gray-700"}`}
placeholder={t("register.emailPlaceholder")}
/>
</div>
{errors.email && <p className="text-red-500 text-sm mt-1">{errors.email}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
{t("register.phone")} <span className="text-gray-500">({t("register.optional")})</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Phone className="w-5 h-5 text-gray-400 group-focus-within:text-amber-500" />
</div>
<input
type="tel"
value={formData.phone}
onChange={(e) => {
setFormData({ ...formData, phone: e.target.value });
setErrors({ ...errors, phone: null });
}}
className="w-full pr-12 pl-4 py-3 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all"
placeholder={t("register.phoneOptionalPlaceholder")}
/>
</div>
{errors.phone && <p className="text-red-500 text-sm mt-1">{errors.phone}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
{t("register.whatsapp")} <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<MessageCircle className={`w-5 h-5 ${errors.whatsapp ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
</div>
<input
type="tel"
value={formData.whatsapp}
onChange={(e) => {
setFormData({ ...formData, whatsapp: e.target.value });
setErrors({ ...errors, whatsapp: null });
}}
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${errors.whatsapp ? "border-red-500" : "border-gray-700"}`}
placeholder={t("register.whatsappPlaceholder")}
/>
</div>
{errors.whatsapp && <p className="text-red-500 text-sm mt-1">{errors.whatsapp}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
{t("register.phoneSecondary")} <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Phone className={`w-5 h-5 ${errors.phone2 ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
</div>
<input
type="tel"
value={formData.phone2}
onChange={(e) => {
setFormData({ ...formData, phone2: e.target.value });
setErrors({ ...errors, phone2: null });
}}
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${errors.phone2 ? "border-red-500" : "border-gray-700"}`}
placeholder={t("register.phoneSecondaryPlaceholder")}
maxLength={7}
/>
</div>
{errors.phone2 && <p className="text-red-500 text-sm mt-1">{errors.phone2}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
{t("register.nationalNumber")} <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<User className={`w-5 h-5 ${errors.nationalNumber ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
</div>
<input
type="text"
value={formData.nationalNumber}
onChange={(e) => {
setFormData({ ...formData, nationalNumber: e.target.value });
setErrors({ ...errors, nationalNumber: null });
}}
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${errors.nationalNumber ? "border-red-500" : "border-gray-700"}`}
placeholder={t("register.nationalNumberPlaceholder")}
/>
</div>
{errors.nationalNumber && <p className="text-red-500 text-sm mt-1">{errors.nationalNumber}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
{type === "owner" ? (
<>
<label className="block text-sm font-medium text-gray-300 mb-2">
{t("register.ownerType")} <span className="text-red-500">*</span>
</label>
<select
value={formData.ownerType.toString()}
onChange={(e) => {
const selectedType = parseInt(e.target.value, 10);
setFormData((prev) => ({ ...prev, ownerType: selectedType }));
}}
className="w-full py-3 px-4 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent appearance-none text-gray-900 cursor-pointer"
>
{Object.entries(OwnerTypeLabels).map(([value, label]) => (
<option key={value} value={value} className="bg-gray-900 ">
{label}
</option>
))}
</select>
</>
) : (
<>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
{t("register.customerType")} <span className="text-red-500">*</span>
</label>
<select
value={formData.customerType}
onChange={(e) => setFormData({ ...formData, customerType: e.target.value })}
className="w-full py-3 px-4 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white appearance-none cursor-pointer"
>
{Object.entries(CustomerTypeLabels).map(([value, label]) => (
<option key={value} value={value} className="bg-gray-900 text-white">
{label}
</option>
))}
</select>
</motion.div>
</>
)}
</motion.div>
<AnimatePresence>
{isCompany && <UploadImage idImagePreviews={idImagePreviews} type={"license"} errors={errors.license} setIdImagePreviews={setIdImagePreviews} setIdImages={setIdImages} />}
</AnimatePresence>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
{t("register.password")} <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className={`w-5 h-5 ${errors.password ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
</div>
<input
type={showPassword ? "text" : "password"}
value={formData.password}
onChange={(e) => {
setFormData({ ...formData, password: e.target.value });
setErrors({ ...errors, password: null });
}}
className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${errors.password ? "border-red-500" : "border-gray-700"}`}
placeholder={t("register.passwordPlaceholder")}
/>
<button type="button" onClick={() => setShowPassword(!showPassword)} className="absolute inset-y-0 left-0 pl-3 flex items-center">
{showPassword ? <EyeOff className="w-5 h-5 text-gray-400" /> : <Eye className="w-5 h-5 text-gray-400" />}
</button>
</div>
{errors.password && <p className="text-red-500 text-sm mt-1">{errors.password}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
{t("register.confirmPassword")} <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className={`w-5 h-5 ${errors.confirmPassword ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`} />
</div>
<input
type={showConfirmPassword ? "text" : "password"}
value={formData.confirmPassword}
onChange={(e) => {
setFormData({ ...formData, confirmPassword: e.target.value });
setErrors({ ...errors, confirmPassword: null });
}}
className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${errors.confirmPassword ? "border-red-500" : "border-gray-700"}`}
placeholder={t("register.confirmPasswordPlaceholder")}
/>
<button type="button" onClick={() => setShowConfirmPassword(!showConfirmPassword)} className="absolute inset-y-0 left-0 pl-3 flex items-center">
{showConfirmPassword ? <EyeOff className="w-5 h-5 text-gray-400" /> : <Eye className="w-5 h-5 text-gray-400" />}
</button>
{formData.confirmPassword && (
<div className="absolute inset-y-0 left-12 flex items-center">
{formData.password === formData.confirmPassword ? <CheckCircle className="w-5 h-5 text-green-500" /> : <XCircle className="w-5 h-5 text-red-500" />}
</div>
)}
</div>
{errors.confirmPassword && <p className="text-red-500 text-sm mt-1">{errors.confirmPassword}</p>}
</motion.div>
</>
);
}

View File

@ -0,0 +1,12 @@
import Policy from "./PolicyAgree";
import UploadImage from "./UploadImage";
export default function OwnerFormStepTwo({idImagePreviews,errors,setIdImagePreviews,setIdImages,setFormData,formData}) {
return (
<>
<UploadImage idImagePreviews={idImagePreviews} type={"front"} errors={errors.front} setIdImagePreviews={setIdImagePreviews} setIdImages={setIdImages} />
<UploadImage idImagePreviews={idImagePreviews} type={"back"} errors={errors.back} setIdImagePreviews={setIdImagePreviews} setIdImages={setIdImages} />
<Policy setFormData={setFormData} formData={formData} type={"owner"} />
</>
);
}

View File

@ -0,0 +1,31 @@
import { motion } from "framer-motion";
import { fadeInUp } from "../Animation/OwnerPageAnimation";
import Link from "next/link";
import { useTranslation } from "react-i18next";
export default function Policy({ formData, setFormData, type }) {
const { t } = useTranslation();
return (
<>
<motion.div variants={fadeInUp} className="flex items-center gap-2">
<input
type="checkbox"
id="terms"
checked={formData.agreeTerms}
onChange={(e) => setFormData({ ...formData, agreeTerms: e.target.checked })}
className={`w-4 h-4 rounded border-gray-600 bg-white/5 ${type === "owner" ? `text-amber-500 focus:ring-amber-500` : ` text-blue-500 focus:ring-blue-500`} `}
required
/>
<label htmlFor="terms" className="text-sm text-gray-300">
{t("register.agreeTerms")}{" "}
<Link href="/terms" className={`${type === "owner" ? `text-amber-500 focus:ring-amber-500` : ` text-blue-500 focus:ring-blue-500`}`}>
{t("register.termsOfUse")}
</Link>{" "}
{t("register.and")}{" "}
<Link href="/privacy" className={`${type === "owner" ? `text-amber-500 focus:ring-amber-500` : ` text-blue-500 focus:ring-blue-500`}`}>
{t("register.privacyPolicy")}
</Link>
</label>
</motion.div>
</>
);
}

View File

@ -0,0 +1,85 @@
"use client";
import { useTranslation } from "react-i18next";
import { fadeInUp } from "../Animation/OwnerPageAnimation";
import { handleImageUpload } from "@/app/HelperFunction/UploadImage";
import { motion } from "framer-motion";
import Image from "next/image";
import { Camera, X } from "lucide-react";
import { useRef } from "react";
export default function UploadImage({ idImagePreviews, type, errors, setIdImagePreviews, setIdImages }) {
const { t } = useTranslation();
const fileInputRef = useRef(null);
const currentPreview = idImagePreviews?.[type];
const handleRemoveImage = (e) => {
e.stopPropagation();
setIdImages((prev) => ({ ...prev, [type]: null }));
setIdImagePreviews((prev) => ({ ...prev, [type]: "" }));
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
const getLabel = () => {
switch (type) {
case "front":
return t("register.owner.frontIdLabel");
case "back":
return t("register.owner.backIdLabel");
case "license":
return t("register.owner.licenseImageLabel");
default:
return "";
}
};
const getClickText = () => {
return type === "license" ? t("register.owner.clickUploadLicense") : t("register.owner.clickUploadImage");
};
const getAltText = () => {
switch (type) {
case "front":
return t("register.uploadFrontAlt");
case "back":
return t("register.uploadBackAlt");
case "license":
return t("register.uploadLicenseAlt");
default:
return "Preview Image";
}
};
return (
<motion.div variants={fadeInUp} key={`${type}-upload-step`}>
<label className="block text-sm font-medium text-gray-300 mb-2">
{getLabel()} {type === "license" ? <span className="text-gray-400">({t("register.optional")})</span> : <span className="text-red-500">*</span>}
</label>
{type === "license" && <div className="mb-2 text-xs text-gray-400">{t("register.owner.licenseHint")}</div>}
<div
onClick={() => fileInputRef.current?.click()}
className={`relative border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all ${
currentPreview ? "border-green-500 bg-green-500/10" : errors ? "border-red-500 bg-red-500/10" : "border-gray-700 hover:border-amber-500 hover:bg-white/5"
}`}
>
<input ref={fileInputRef} type="file" accept="image/*" onChange={(e) => handleImageUpload(type, e.target.files?.[0], t, setIdImagePreviews, setIdImages)} className="hidden" />
{currentPreview ? (
<div className="relative">
<Image src={currentPreview} alt={getAltText()} width={200} height={120} className="mx-auto rounded-lg object-cover" />
<button type="button" onClick={handleRemoveImage} className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 rounded-full flex items-center justify-center hover:bg-red-600 transition-colors">
<X className="w-4 h-4 text-white" />
</button>
</div>
) : (
<>
<Camera className="w-12 h-12 text-gray-500 mx-auto mb-3" />
<p className="text-gray-400">{getClickText()}</p>
<p className="text-xs text-gray-500 mt-2">JPEG, PNG, JPG {t("register.upTo5MB")}</p>
</>
)}
</div>
{errors && <p className="text-red-500 text-sm mt-1">{errors}</p>}
</motion.div>
);
}

View File

@ -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: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>', attribution: '&copy; <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} ل.س`;

View 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>
</>
);
}

View 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>
</>
);
}

View 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>
</>
);
}

View 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>
</>
);
}

View File

@ -1,89 +1,37 @@
'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",
}); });
}; };
@ -96,170 +44,51 @@ export default function HeroSearch({ onSearch, isAuthenticated }) {
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;
return (
<button
key={tab} key={tab}
onClick={() => handleTabClick(tab)} onClick={() => handleTabClick(tab)}
className={`px-4 py-2 rounded-lg font-medium text-sm transition-all ${ 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"}`}
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`)} {isActive && <motion.div layoutId="activeTabBackground" className="absolute inset-0 bg-amber-500 rounded-lg shadow-md" transition={{ type: "spring", stiffness: 500, damping: 35 }} />}
</motion.button> <span className="relative z-10">{t(`${tab}Tab`)}</span>
))} </button>
);
})}
</div> </div>
{/* city */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div> <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"))} />
<label className="block text-sm font-medium text-white mb-2"> {activeTab === "rent" && (
<div className="flex items-center gap-1"> <FilterSelect
<MapPin className="w-4 h-4" /> icon={<Home 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} value={filters.propertyType}
onChange={(e) => setFilters({...filters, propertyType: e.target.value})} 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" option={propertyTypes}
style={{ label={t("rentTypeLabel")}
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> {/* price */}
<label className="block text-sm font-medium text-white mb-2"> <FilterSelect
<div className="flex items-center gap-1"> icon={<DollarSign className="w-4 h-4" />}
<DollarSign className="w-4 h-4" />
{t("priceLabel")}
</div>
</label>
<select
value={filters.priceRange} value={filters.priceRange}
onChange={(e) => setFilters({ ...filters, priceRange: e.target.value })} 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" option={priceRanges}
style={{ label={t("priceLabel")}
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', {/* type */}
backgroundPosition: 'left 1rem center', <FilterSelect value={filters.identityType} onChange={(e) => setFilters({ ...filters, identityType: e.target.value })} option={identityTypes} label={t("identityTypeLabel")} />
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>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mt-4"> <div className="grid grid-cols-1 md:grid-cols-4 gap-4 mt-4">
<div> {/* owener */}
<label className="block text-sm font-medium text-white mb-2">{t("ownerSourceLabel")}</label> <FilterSelect value={filters.ownerSource} onChange={(e) => setFilters({ ...filters, ownerSource: e.target.value })} option={ownerSources} label={t("ownerSourceLabel")} />
<select {activeTab === "rent" && <FilterSelect value={filters.rentPeriod} onChange={(e) => setFilters({ ...filters, rentPeriod: e.target.value })} option={rentPeriods} label={t("rentTypeLabel")} />}
value={filters.ownerSource} <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`}>
onChange={(e) => setFilters({ ...filters, ownerSource: e.target.value })} {/* checkbox */}
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" <label className="mt-4 flex items-center gap-3 text-amber-50 text-sm">
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' && (
<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 <input
type="checkbox" type="checkbox"
checked={filters.availableToday} checked={filters.availableToday}
@ -282,40 +111,7 @@ export default function HeroSearch({ onSearch, isAuthenticated }) {
</motion.button> </motion.button>
</div> </div>
</motion.div> </motion.div>
{showLoginDialog && !isAuthenticated && <ShowLoginDialog setShowLoginDialog={setShowLoginDialog} />}
{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
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>
)}
</> </>
); );
} }

View 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>
</>
);
}

View File

@ -0,0 +1,71 @@
"use client";
import { useState, useRef } from "react";
import { AnimatePresence } from "framer-motion";
import HeroSearch from "./HeroSearch";
import ShowIfOwner from "./ShowIfOwner";
import MapSection from "./MapSection";
import useApplyFilters from "../../hooks/useApplyFilters";
import HeroSection from "./HeroSection";
import FeatureSection from "./FeatureSection";
import ArrowForScroll from "./ArrowForScroll";
import useAuth from "@/app/hooks/useAuth";
export default function HomeClient() {
const mapSectionRef = useRef(null);
const [showMap, setShowMap] = useState(false);
const [isScrolling, setIsScrolling] = useState(false);
const { filteredProperties, applyFilters, resetFilters } = useApplyFilters();
const { name, isOwner, isAuthenticated, role } = useAuth();
const handleSearch = (filters) => {
applyFilters(filters);
if (!showMap) {
setShowMap(true);
setTimeout(() => {
scrollToMap();
}, 300);
} else {
scrollToMap();
}
};
const scrollToMap = () => {
if (mapSectionRef.current) {
setIsScrolling(true);
mapSectionRef.current.scrollIntoView({
behavior: "smooth",
block: "center",
});
setTimeout(() => setIsScrolling(false), 1000);
}
};
const resetSearch = () => {
setShowMap(false);
resetFilters();
window.scrollTo({
top: 0,
behavior: "smooth",
});
};
return (
<div className="min-h-screen">
<section className="relative min-h-screen flex items-center justify-center overflow-hidden">
<div className="relative z-10 container mx-auto px-4 sm:px-6 lg:px-8">
<div className="max-w-6xl mx-auto">
<HeroSection />
{!isOwner && <HeroSearch onSearch={handleSearch} />}
{isOwner && <ShowIfOwner name={name} />}
</div>
</div>
{!showMap && !isOwner && <ArrowForScroll />}
</section>
{!isOwner && (
<>
<AnimatePresence mode="wait">
{showMap && <MapSection mapSectionRef={mapSectionRef} isScrolling={isScrolling} resetSearch={resetSearch} filteredProperties={filteredProperties} />}
</AnimatePresence>
</>
)}
<FeatureSection />
</div>
);
}

View 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>
</>
);
}

View 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>
</>
);
}

View 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>
</>
);
}

View File

@ -0,0 +1,246 @@
"use client";
import { useState, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { ChevronRight, ChevronLeft } from "lucide-react";
const MONTH_KEYS = [
"month.january",
"month.february",
"month.march",
"month.april",
"month.may",
"month.june",
"month.july",
"month.august",
"month.september",
"month.october",
"month.november",
"month.december",
];
const DAY_KEYS = [
"dayAbbr.sun",
"dayAbbr.mon",
"dayAbbr.tue",
"dayAbbr.wed",
"dayAbbr.thu",
"dayAbbr.fri",
"dayAbbr.sat",
];
export default function DateSelectionCalendar({
mode,
availableDates,
selectedStart,
selectedEnd,
onSelectStart,
onSelectEnd,
}) {
const { t } = useTranslation();
const [calendarMonth, setCalendarMonth] = useState(() => new Date().getMonth());
const [calendarYear, setCalendarYear] = useState(() => new Date().getFullYear());
const [rangeError, setRangeError] = useState(null);
const isPastDate = useCallback((dateStr) => {
const today = new Date();
today.setHours(0, 0, 0, 0);
return new Date(dateStr) < today;
}, []);
const isDateAvailable = useCallback(
(dateStr) => (availableDates ? availableDates.has(dateStr) : true),
[availableDates]
);
const isMonthFullyAvailable = useCallback(
(year, monthIdx) => {
const daysInMonth = new Date(year, monthIdx + 1, 0).getDate();
for (let day = 1; day <= daysInMonth; day++) {
const dateStr = `${year}-${String(monthIdx + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
if (isPastDate(dateStr) || !isDateAvailable(dateStr)) return false;
}
return true;
},
[isPastDate, isDateAvailable]
);
const isPastMonth = useCallback((year, monthIdx) => {
const now = new Date();
return (
year < now.getFullYear() ||
(year === now.getFullYear() && monthIdx < now.getMonth())
);
}, []);
const handleDayClick = (dateStr) => {
setRangeError(null);
if (!selectedStart || selectedEnd) {
onSelectStart(dateStr);
onSelectEnd(null);
return;
}
if (new Date(dateStr) <= new Date(selectedStart)) {
onSelectStart(dateStr);
onSelectEnd(null);
return;
}
const start = new Date(selectedStart);
const end = new Date(dateStr);
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
const ds = d.toISOString().split("T")[0];
if (!isDateAvailable(ds)) {
setRangeError(t("unavailableInRange", { defaultValue: "توجد أيام غير متاحة ضمن الفترة المحددة" }));
return;
}
}
onSelectEnd(dateStr);
};
const handleMonthClick = (year, monthIdx) => {
setRangeError(null);
const monthStr = `${year}-${String(monthIdx + 1).padStart(2, "0")}`;
const firstDay = `${monthStr}-01`;
if (!selectedStart || selectedEnd || monthStr <= (selectedStart || "").substring(0, 7)) {
onSelectStart(firstDay);
onSelectEnd(null);
} else {
const lastDay = new Date(year, monthIdx + 1, 0).getDate();
onSelectEnd(`${monthStr}-${String(lastDay).padStart(2, "0")}`);
}
};
const navigateMonth = (delta) => {
let month = calendarMonth + delta;
let year = calendarYear;
if (month < 0) {
month = 11;
year--;
}
if (month > 11) {
month = 0;
year++;
}
setCalendarMonth(month);
setCalendarYear(year);
};
const isEntryStep = !selectedStart || !!selectedEnd;
return (
<div className="mb-3">
{/* Step Indicator */}
<div className="flex items-center justify-center gap-2 mb-3">
<div
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${isEntryStep ? "bg-amber-100 text-amber-800" : "bg-gray-100 text-gray-500"}`}
>
<div className={`w-1.5 h-1.5 rounded-full ${isEntryStep ? "bg-amber-500" : "bg-gray-400"}`} />
{t("selectStartDate")}
</div>
<div className="text-gray-300 text-xs"></div>
<div className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${!isEntryStep ? "bg-amber-100 text-amber-800" : "bg-gray-100 text-gray-500"}`}>
<div className={`w-1.5 h-1.5 rounded-full ${!isEntryStep ? "bg-amber-500" : "bg-gray-400"}`} />
{t("selectEndDate")}
</div>
</div>
{mode === "daily" ? (
<div>
<div className="flex items-center justify-between mb-2">
<button onClick={() => navigateMonth(-1)} className="p-1 rounded-lg hover:bg-gray-100 transition-colors">
<ChevronRight className="w-4 h-4 text-gray-600" />
</button>
<span className="text-sm font-bold text-gray-900">
{t(MONTH_KEYS[calendarMonth])} {calendarYear}
</span>
<button onClick={() => navigateMonth(1)} className="p-1 rounded-lg hover:bg-gray-100 transition-colors">
<ChevronLeft className="w-4 h-4 text-gray-600" />
</button>
</div>
<div className="grid grid-cols-7 mb-1">
{DAY_KEYS.map((dk, i) => (
<div key={i} className="text-center text-[10px] text-gray-400 font-medium py-1">
{t(dk)}
</div>
))}
</div>
{(() => {
const firstDay = new Date(calendarYear, calendarMonth, 1).getDay();
const daysInMonth = new Date(calendarYear, calendarMonth + 1, 0).getDate();
const cells = [];
for (let i = 0; i < firstDay; i++) {
cells.push(<div key={`e-${i}`} />);
}
for (let day = 1; day <= daysInMonth; day++) {
const dateStr = `${calendarYear}-${String(calendarMonth + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
const past = isPastDate(dateStr);
const available = isDateAvailable(dateStr);
const isSelStart = dateStr === selectedStart;
const isSelEnd = dateStr === selectedEnd;
const inRange =
selectedStart &&
selectedEnd &&
new Date(dateStr) > new Date(selectedStart) &&
new Date(dateStr) < new Date(selectedEnd);
const disabled = past || !available;
cells.push(
<button
key={dateStr}
onClick={() => !disabled && handleDayClick(dateStr)}
disabled={disabled}
className={`text-center py-1.5 text-xs rounded-lg transition-all ${disabled ? "text-gray-300 cursor-not-allowed" : ""} ${isSelStart || isSelEnd ? "bg-amber-500 text-white font-bold shadow-sm" : ""} ${inRange ? "bg-amber-100 text-amber-800" : ""} ${!disabled && !isSelStart && !isSelEnd && !inRange && available ? "hover:bg-amber-50 text-gray-700" : ""} ${!disabled && !isSelStart && !isSelEnd && !inRange && !available ? "text-red-300" : ""}`}
>
{day}
</button>
);
}
return <div className="grid grid-cols-7 gap-0.5">{cells}</div>;
})()}
</div>
) : (
<div>
<div className="flex items-center justify-between mb-2">
<button onClick={() => setCalendarYear((prev) => prev - 1)} className="p-1 rounded-lg hover:bg-gray-100 transition-colors">
<ChevronRight className="w-4 h-4 text-gray-600" />
</button>
<span className="text-sm font-bold text-gray-900">{calendarYear}</span>
<button onClick={() => setCalendarYear((prev) => prev + 1)} className="p-1 rounded-lg hover:bg-gray-100 transition-colors">
<ChevronLeft className="w-4 h-4 text-gray-600" />
</button>
</div>
<div className="grid grid-cols-3 gap-2">
{MONTH_KEYS.map((mk, idx) => {
const monthStr = `${calendarYear}-${String(idx + 1).padStart(2, "0")}`;
const isSelStart = selectedStart && selectedStart.startsWith(monthStr);
const isSelEnd = selectedEnd && selectedEnd.startsWith(monthStr);
const inRange =
selectedStart &&
selectedEnd &&
monthStr > selectedStart.substring(0, 7) &&
monthStr < selectedEnd.substring(0, 7);
const disabled = isPastMonth(calendarYear, idx) || !isMonthFullyAvailable(calendarYear, idx);
return (
<button
key={idx}
onClick={() => !disabled && handleMonthClick(calendarYear, idx)}
disabled={disabled}
className={`p-2.5 rounded-xl text-center text-xs font-medium transition-all border ${disabled ? "text-gray-300 border-gray-100 bg-gray-50 cursor-not-allowed" : ""} ${isSelStart || isSelEnd ? "bg-amber-500 text-white border-amber-500 shadow-sm" : ""} ${!disabled && inRange ? "bg-amber-100 text-amber-800 border-amber-200" : ""} ${!disabled && !isSelStart && !isSelEnd && !inRange ? "bg-white text-gray-700 border-gray-200 hover:border-amber-300" : ""}`}
>
{t(mk)}
</button>
);
})}
</div>
</div>
)}
{rangeError && (
<div className="bg-red-50 text-red-600 p-2.5 rounded-xl text-xs mt-3">
{rangeError}
</div>
)}
</div>
);
}

View File

@ -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");

View File

@ -18,7 +18,21 @@ const City = Object.freeze({
QAMISHLI: 'القامشلي', QAMISHLI: 'القامشلي',
RURAL_DAMASCUS: 'ريف دمشق', RURAL_DAMASCUS: 'ريف دمشق',
}); });
export const CityEnum = Object.freeze({
DAMASCUS: 0,
ALEPPO: 1,
HOMS: 2,
LATAKIA: 3,
DARAA: 4,
TARTOUS: 5,
SUWEIDA: 6,
DEIR_EZZOR: 7,
RAQQA: 8,
IDLIB: 9,
HASAKAH: 10,
Qamishli: 11,
RURAL_DAMASCUS: 12,
});
// All cities as a flat array // All cities as a flat array
const CitiesList = Object.freeze(Object.values(City)); const CitiesList = Object.freeze(Object.values(City));
@ -51,4 +65,6 @@ const CityTranslationKeys = Object.freeze({
[City.RURAL_DAMASCUS]: 'city.ruralDamascus', [City.RURAL_DAMASCUS]: 'city.ruralDamascus',
}); });
export { City, CitiesList, extractCity, CityTranslationKeys };
export { City, CitiesList, extractCity, CityTranslationKeys, };

View File

@ -32,7 +32,7 @@ export default function FAQPage() {
}; };
return ( return (
<div className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12" dir="rtl"> <div className="min-h-screen py-12" dir="rtl">
<div className="container mx-auto px-4 max-w-4xl"> <div className="container mx-auto px-4 max-w-4xl">
<motion.div <motion.div
initial={{ opacity: 0, y: -20 }} initial={{ opacity: 0, y: -20 }}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -1,14 +1,15 @@
'use client'; "use client";
import { useEffect, useState } from 'react'; import { useEffect, useState } from "react";
import { useTranslation } from 'react-i18next'; import { useTranslation } from "react-i18next";
import { useRouter } from 'next/navigation'; import { useRouter } from "next/navigation";
import { motion } from 'framer-motion'; import { motion } from "framer-motion";
import Link from 'next/link'; import Link from "next/link";
import Image from 'next/image'; import Image from "next/image";
import { Heart, MapPin, Bed, Bath, Square, X, ImageIcon } from 'lucide-react'; import { Heart, MapPin, Bed, Bath, Square, X, ImageIcon } from "lucide-react";
import { useFavorites } from '@/app/contexts/FavoritesContext'; import { useFavorites } from "@/app/contexts/FavoritesContext";
import AuthService from '@/app/services/AuthService'; import AuthService from "@/app/services/AuthService";
import Loading from "../loading";
export default function FavoritesPage() { export default function FavoritesPage() {
const { t } = useTranslation(); const { t } = useTranslation();
@ -25,38 +26,28 @@ export default function FavoritesPage() {
}, [router]); }, [router]);
const formatCurrency = (amount) => { const formatCurrency = (amount) => {
return amount?.toLocaleString() + ' ' + t('currency-syp-suffix'); return amount?.toLocaleString() + " " + t("currency-syp-suffix");
}; };
if (favoritesLoading && favorites.length === 0) { if (favoritesLoading && favorites.length === 0) {
return ( return <Loading />;
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<p className="text-gray-600">{t('loading')}</p>
</div>
</div>
);
} }
return ( return (
<div className="min-h-screen bg-gray-50 py-8"> <div className="min-h-screen bg-gray-50 py-8">
<div className="container mx-auto px-4 max-w-6xl"> <div className="container mx-auto px-4 max-w-6xl">
<div className="mb-8"> <div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t('favorites')}</h1> <h1 className="text-3xl font-bold text-gray-900 mb-2">{t("favorites")}</h1>
<p className="text-gray-600">{t('saved-properties')}</p> <p className="text-gray-600">{t("saved-properties")}</p>
</div> </div>
{favorites.length === 0 ? ( {favorites.length === 0 ? (
<div className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300"> <div className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
<Heart className="w-16 h-16 text-gray-300 mx-auto mb-4" /> <Heart className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-700 mb-2">{t('no-favorites')}</h3> <h3 className="text-xl font-bold text-gray-700 mb-2">{t("no-favorites")}</h3>
<p className="text-gray-500 mb-6">{t('add-favorites-hint')}</p> <p className="text-gray-500 mb-6">{t("add-favorites-hint")}</p>
<Link <Link href="/properties" className="inline-flex items-center gap-2 bg-amber-500 text-white px-6 py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors">
href="/properties" {t("browse-properties")}
className="inline-flex items-center gap-2 bg-amber-500 text-white px-6 py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors"
>
{t('browse-properties')}
</Link> </Link>
</div> </div>
) : ( ) : (
@ -70,13 +61,7 @@ export default function FavoritesPage() {
> >
<div className="relative h-48 bg-gray-100"> <div className="relative h-48 bg-gray-100">
{property.images && property.images[0] ? ( {property.images && property.images[0] ? (
<Image <Image src={property.images[0]} alt={property.title} fill className="object-cover" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" />
src={property.images[0]}
alt={property.title}
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
) : ( ) : (
<div className="w-full h-full flex items-center justify-center"> <div className="w-full h-full flex items-center justify-center">
<ImageIcon className="w-12 h-12 text-gray-400" /> <ImageIcon className="w-12 h-12 text-gray-400" />
@ -95,7 +80,7 @@ export default function FavoritesPage() {
<div> <div>
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-lg text-xs font-medium"> <span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-lg text-xs font-medium">
{property.type === 'apartment' ? t('buildingType.apartment') : property.type === 'villa' ? t('buildingType.villa') : t('buildingType.house')} {property.type === "apartment" ? t("buildingType.apartment") : property.type === "villa" ? t("buildingType.villa") : t("buildingType.house")}
</span> </span>
</div> </div>
<h3 className="font-bold text-gray-900 mb-1 line-clamp-1">{property.title}</h3> <h3 className="font-bold text-gray-900 mb-1 line-clamp-1">{property.title}</h3>
@ -108,7 +93,7 @@ export default function FavoritesPage() {
</div> </div>
<div className="text-left"> <div className="text-left">
<div className="text-xl font-bold text-gray-900">{formatCurrency(property.price)}</div> <div className="text-xl font-bold text-gray-900">{formatCurrency(property.price)}</div>
<div className="text-xs text-gray-500">/{property.priceUnit === 'daily' ? t('day') : t('month')}</div> <div className="text-xs text-gray-500">/{property.priceUnit === "daily" ? t("day") : t("month")}</div>
</div> </div>
</div> </div>
@ -129,11 +114,8 @@ export default function FavoritesPage() {
</div> </div>
</div> </div>
<Link <Link href={`/property/${property.id}`} className="block w-full bg-amber-500 text-white py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors text-center">
href={`/property/${property.id}`} {t("viewDetails")}
className="block w-full bg-amber-500 text-white py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors text-center"
>
{t('viewDetails')}
</Link> </Link>
</div> </div>
</motion.div> </motion.div>

View File

@ -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>
);
}

View File

@ -8,6 +8,7 @@ import toast, { Toaster } from 'react-hot-toast';
import { Mail, Key, Lock, CheckCircle, ArrowLeft, RefreshCw } from 'lucide-react'; import { Mail, Key, Lock, CheckCircle, ArrowLeft, RefreshCw } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { requestForgetPasswordOtp, verifyForgetPasswordOtp } from '../utils/api'; import { requestForgetPasswordOtp, verifyForgetPasswordOtp } from '../utils/api';
import InteractiveBackground from '../components/Animation/Background';
export default function ForgotPasswordPage() { export default function ForgotPasswordPage() {
const router = useRouter(); const router = useRouter();
@ -66,8 +67,9 @@ export default function ForgotPasswordPage() {
const inputClass = "w-full pr-12 pl-4 py-3 bg-white/10 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400 transition-all"; const inputClass = "w-full pr-12 pl-4 py-3 bg-white/10 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400 transition-all";
return ( return (
<div className="min-h-screen bg-gradient-to-br from-amber-50 via-orange-50 to-yellow-50 flex items-center justify-center p-4 relative overflow-hidden"> <div className="min-h-screen flex items-center justify-center p-4 relative overflow-hidden">
<Toaster position="top-center" reverseOrder={false} /> <Toaster position="top-center" reverseOrder={false} />
<InteractiveBackground/>
<div className="absolute inset-0 overflow-hidden"> <div className="absolute inset-0 overflow-hidden">
<div className="absolute -top-40 -right-40 w-80 h-80 bg-amber-400 rounded-full opacity-20 blur-3xl animate-pulse"></div> <div className="absolute -top-40 -right-40 w-80 h-80 bg-amber-400 rounded-full opacity-20 blur-3xl animate-pulse"></div>
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-orange-400 rounded-full opacity-20 blur-3xl animate-pulse delay-1000"></div> <div className="absolute -bottom-40 -left-40 w-80 h-80 bg-orange-400 rounded-full opacity-20 blur-3xl animate-pulse delay-1000"></div>

View 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,
};
};

View 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
View 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,
};
}

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,37 @@
'use client'; "use client";
import { motion } from 'framer-motion'; import { motion } from "framer-motion";
import { useTranslation } from "react-i18next";
export default function Loading() { export default function Loading() {
const { t } = useTranslation();
return ( return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center"> <div className="min-h-screen flex items-center justify-center p-4" style={{ backgroundColor: "#0f172a" }}>
<motion.div 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">
<motion.div <motion.div
initial={{ opacity: 0 }} animate={{
animate={{ opacity: 1 }} scale: [1, 1.06, 1],
className="text-center" opacity: [0.8, 1, 0.8],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut",
}}
className="relative w-25 h-36 flex items-center justify-center"
> >
<div className="w-16 h-16 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" /> <svg viewBox="0 0 100 100" className="w-full h-full fill-amber-500/10 stroke-amber-500 stroke-[4] drop-shadow-[0_0_12px_rgba(245,158,11,0.2)] " xmlns="http://www.w3.org/2000/svg">
<p className="text-gray-500 text-lg">جاري التحميل...</p> <polygon points="50,5 95,27.5 95,72.5 50,95 5,72.5 5,27.5" />
</svg>
<div className="absolute inset-0 flex items-center justify-center p-2 text-center">
<span className="text-amber-400 font-bold text-[10px] tracking-wide select-none">Sweet Home</span>
</div>
</motion.div>
<p className="text-amber-400 text-sm font-bold tracking-wide animate-pulse" suppressHydrationWarning>
{t("loading")}
</p>
</motion.div> </motion.div>
</div> </div>
); );

View File

@ -6,7 +6,7 @@ import Link from 'next/link';
export default function Error({ error, reset }) { export default function Error({ error, reset }) {
return ( return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center p-4"> <div className="min-h-screen bg-gray-900 flex items-center justify-center p-4">
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="text-center max-w-md"> <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="text-center max-w-md">
<div className="w-20 h-20 bg-red-500/20 rounded-full flex items-center justify-center mx-auto mb-6"> <div className="w-20 h-20 bg-red-500/20 rounded-full flex items-center justify-center mx-auto mb-6">
<AlertTriangle className="w-10 h-10 text-red-500" /> <AlertTriangle className="w-10 h-10 text-red-500" />

View File

@ -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>
);
}

View File

@ -1,730 +1,8 @@
"use client"; import Login from "../components/LoginComponent/Login";
export const metadata = {
import { useState } from "react"; title: "صفحة تسجيل الدخول ",
import { useTranslation } from "react-i18next"; describtion: "صفحة تسجيل الدخول في Sweet Home",
import { motion, AnimatePresence } from "framer-motion"; };
import toast, { Toaster } from "react-hot-toast";
import Link from "next/link";
import { useRouter } from "next/navigation";
import {
Mail,
Lock,
Eye,
EyeOff,
ArrowLeft,
LogIn,
CheckCircle,
Loader2,
Home,
Shield,
Phone,
KeyRound,
} from "lucide-react";
import {
loginWithEmail,
loginWithPhone,
sendEmailOTP,
sendPhoneOTP,
verifyEmail,
verifyPhone,
isEmail,
isPhoneNumber,
getOwnerByUserId,
getCustomerByUserId,
} from "../utils/api";
import AuthService from "../services/AuthService";
export default function LoginPage() { export default function LoginPage() {
const router = useRouter(); return <Login />;
const { t } = useTranslation();
// Step: 'login' | 'otp'
const [step, setStep] = useState("login");
const [loginMethod, setLoginMethod] = useState("email"); // 'email' | 'phone'
const [showPassword, setShowPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [formData, setFormData] = useState({
credential: "",
password: "",
rememberMe: false,
});
const [otpCode, setOtpCode] = useState("");
const [otpError, setOtpError] = useState("");
const [errors, setErrors] = useState({});
const validateForm = () => {
const newErrors = {};
if (!formData.credential) {
newErrors.credential =
loginMethod === "email"
? t("emailRequired")
: t("phoneRequired");
// } else if (loginMethod === 'email' && !isEmail(formData.credential)) {
// newErrors.credential = 'البريد الإلكتروني غير صالح';
// } else if (loginMethod === 'phone' && !isPhoneNumber(formData.credential)) {
newErrors.credential = t("phoneInvalid");
}
if (!formData.password) {
newErrors.password = t("passwordRequired");
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleLogin = async (e) => {
e.preventDefault();
if (!validateForm()) return;
setIsLoading(true);
setErrors({});
try {
const loginFn = loginMethod === "email" ? loginWithEmail : loginWithPhone;
const result = await loginFn(formData.credential, formData.password);
if (result.status === 200) {
const token =
typeof result.data === "string"
? result.data
: result.data?.token || result.data?.accessToken;
AuthService.addToken(token);
// Fetch user profile to get full name
const authUser = AuthService.getUser();
if (authUser?.id) {
try {
const isOwner = AuthService.isOwner();
const fetchFn = isOwner ? getOwnerByUserId : getCustomerByUserId;
const profile = await fetchFn(authUser.id);
if (profile) {
AuthService.cacheUser({
name:
profile.fullName ||
profile.name ||
`${profile.firstName || ""} ${profile.lastName || ""}`.trim(),
email: profile.email || authUser.email,
phone: profile.phone || profile.phoneNumber || authUser.phone,
});
}
} catch (err) {
console.warn("[Login] Failed to fetch profile:", err);
}
}
const userRole = AuthService.isOwner() ? "owner" : "customer";
setIsSuccess(true);
toast.success(t("loginSuccess"), {
style: { background: "#dcfce7", color: "#166534" },
});
setTimeout(() => {
router.push("/");
}, 1500);
} else if (result.status === 206) {
const tempToken =
typeof result.data === "string"
? result.data
: result.data?.token || result.data?.accessToken;
if (tempToken) {
AuthService.addToken(tempToken);
}
toast(t("otpRequired"), {
icon: "🔐",
style: { background: "#fef3c7", color: "#92400e" },
});
// Send OTP
try {
if (loginMethod === "email") {
await sendEmailOTP();
} else {
await sendPhoneOTP();
}
} catch (otpErr) {
console.warn("[Login] OTP send failed, proceeding anyway:", otpErr);
}
setStep("otp");
} else if (result.status >= 500) {
console.error("[Login] Server error:", result.status, result.data);
toast.error(t("serverError"), {
style: { background: "#fee2e2", color: "#991b1b" },
});
} else if (result.status === 404) {
console.error("[Login] API endpoint not found:", result.status);
toast.error(t("apiNotFound"), {
style: { background: "#fee2e2", color: "#991b1b" },
});
} else if (result.status === 429) {
toast.error(t("tooManyRequests"), {
style: { background: "#fee2e2", color: "#991b1b" },
});
} else {
console.error("[Login] Unexpected status:", result.status, result.data);
toast.error(
result.data?.message || result.data || t("invalidCredentials"),
{
style: { background: "#fee2e2", color: "#991b1b" },
},
);
}
} catch (err) {
console.error("[Login] Error:", err);
toast.error(err.message || t("connectionError"), {
style: { background: "#fee2e2", color: "#991b1b" },
});
} finally {
setIsLoading(false);
}
};
const handleVerifyOTP = async (e) => {
e.preventDefault();
if (!otpCode || otpCode.length < 4) {
setOtpError(t("otpRequired"));
return;
}
setIsLoading(true);
setOtpError("");
try {
const verifyFn = loginMethod === "email" ? verifyEmail : verifyPhone;
const result = await verifyFn(otpCode);
if (result.ok) {
const finalToken =
typeof result.data === "string"
? result.data
: result.data?.token || result.data?.accessToken;
if (finalToken && typeof finalToken === "string") {
AuthService.addToken(finalToken);
}
setIsSuccess(true);
toast.success(t("verifySuccess"), {
style: { background: "#dcfce7", color: "#166534" },
});
setTimeout(() => {
router.push("/");
}, 1500);
} else {
console.error("[OTP] Verification failed:", result.data);
setOtpError(result.data?.message || t("wrongOTP"));
}
} catch (err) {
console.error("[OTP] Error:", err);
setOtpError(err.message || t("verifyError"));
} finally {
setIsLoading(false);
}
};
const resendOTP = async () => {
try {
if (loginMethod === "email") {
await sendEmailOTP();
} else {
await sendPhoneOTP();
}
toast.success(t("resendSuccess"), {
style: { background: "#dcfce7", color: "#166534" },
});
} catch (err) {
console.error("[OTP] Resend failed:", err);
toast.error(t("resendFailed"));
}
};
// Auto-detect login method from input
const handleCredentialChange = (value) => {
setFormData({ ...formData, credential: value });
if (errors.credential) setErrors({ ...errors, credential: null });
// Auto-switch method
// if (isEmail(value)) {
// setLoginMethod('email');
// } else if (isPhoneNumber(value)) {
// setLoginMethod('phone');
// }
};
const particles = Array.from({ length: 20 }, (_, i) => ({
id: i,
x: Math.random() * 100,
y: Math.random() * 100,
size: Math.random() * 3 + 1,
duration: Math.random() * 15 + 10,
delay: Math.random() * 5,
}));
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.1, delayChildren: 0.2 },
},
};
const itemVariants = {
hidden: { y: 20, opacity: 0 },
visible: {
y: 0,
opacity: 1,
transition: { type: "spring", stiffness: 100 },
},
};
return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center p-4 relative overflow-hidden">
<Toaster position="top-center" reverseOrder={false} />
{/* Particles */}
<div className="absolute inset-0 overflow-hidden">
{particles.map((p) => (
<motion.div
key={p.id}
className="absolute rounded-full bg-amber-500/20"
style={{
left: `${p.x}%`,
top: `${p.y}%`,
width: p.size,
height: p.size,
}}
animate={{
y: [0, -20, 0],
x: [0, 10, -10, 0],
opacity: [0.2, 0.4, 0.2],
}}
transition={{
duration: p.duration,
repeat: Infinity,
delay: p.delay,
ease: "linear",
}}
/>
))}
</div>
{/* Glow orbs */}
<motion.div
className="absolute top-20 left-20 w-64 h-64 bg-amber-500/10 rounded-full blur-3xl"
animate={{ scale: [1, 1.2, 1], x: [0, 30, 0], y: [0, -20, 0] }}
transition={{ duration: 12, repeat: Infinity }}
/>
<motion.div
className="absolute bottom-20 right-20 w-80 h-80 bg-blue-500/10 rounded-full blur-3xl"
animate={{ scale: [1, 1.3, 1], x: [0, -30, 0], y: [0, 20, 0] }}
transition={{ duration: 15, repeat: Infinity }}
/>
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="relative w-full max-w-md z-10"
>
{/* Back link */}
<motion.div variants={itemVariants} className="absolute -top-16 left-0">
<Link
href="/"
className="group flex items-center gap-2 text-gray-400 hover:text-white transition-colors"
>
<motion.div
whileHover={{ x: -5 }}
transition={{ type: "spring", stiffness: 400 }}
>
<ArrowLeft className="w-4 h-4" />
</motion.div>
<span>{t("backToHome")}</span>
</Link>
</motion.div>
<motion.div
variants={itemVariants}
className="bg-white/10 backdrop-blur-2xl rounded-3xl shadow-2xl border border-white/10 overflow-hidden"
>
{/* Header */}
<div className="bg-gradient-to-r from-amber-500 to-amber-600 p-8 text-center relative overflow-hidden">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: "spring" }}
className="absolute -top-10 -right-10 w-40 h-40 bg-white/10 rounded-full"
/>
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.3, type: "spring" }}
className="absolute -bottom-10 -left-10 w-40 h-40 bg-white/10 rounded-full"
/>
<motion.div
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.2 }}
className="relative z-10"
>
<motion.div
animate={{ rotate: [0, 10, -10, 0] }}
transition={{ duration: 2, repeat: Infinity }}
className="w-20 h-20 mx-auto mb-4 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-sm"
>
{step === "otp" ? (
<KeyRound className="w-10 h-10 text-white" />
) : (
<Home className="w-10 h-10 text-white" />
)}
</motion.div>
<h1 className="text-3xl font-bold text-white mb-2">SweetHome</h1>
<p className="text-amber-100">
{step === "otp" ? t("enterOTP") : t("welcomeBack")}
</p>
</motion.div>
</div>
<div className="p-8">
<AnimatePresence mode="wait">
{step === "login" ? (
<motion.form
key="login"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
onSubmit={handleLogin}
className="space-y-6"
>
{/* Login method tabs */}
<div className="flex gap-2 bg-white/5 p-1 rounded-xl">
<button
type="button"
onClick={() => {
setLoginMethod("email");
setFormData({ ...formData, credential: "" });
}}
className={`flex-1 py-2.5 rounded-lg text-sm font-medium transition-all flex items-center justify-center gap-2 ${
loginMethod === "email"
? "bg-amber-500 text-white shadow-lg"
: "text-gray-400 hover:text-white"
}`}
>
<Mail className="w-4 h-4" />
{t("emailMethod")}
</button>
<button
type="button"
onClick={() => {
setLoginMethod("phone");
setFormData({ ...formData, credential: "" });
}}
className={`flex-1 py-2.5 rounded-lg text-sm font-medium transition-all flex items-center justify-center gap-2 ${
loginMethod === "phone"
? "bg-amber-500 text-white shadow-lg"
: "text-gray-400 hover:text-white"
}`}
>
<Phone className="w-4 h-4" />
{t("phoneMethod")}
</button>
</div>
{/* Credential input */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
{loginMethod === "email"
? t("emailLabel")
: t("phoneLabel")}
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
{loginMethod === "email" ? (
<Mail
className={`w-5 h-5 transition-colors ${errors.credential ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`}
/>
) : (
<Phone
className={`w-5 h-5 transition-colors ${errors.credential ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`}
/>
)}
</div>
<input
type="text"
// type={loginMethod === 'email' ? 'email' : 'tel'}
value={formData.credential}
onChange={(e) => handleCredentialChange(e.target.value)}
className={`w-full pr-12 pl-4 py-4 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.credential
? "border-red-500"
: "border-gray-700"
}`}
placeholder={
loginMethod === "email"
? t("emailPlaceholder")
: t("phonePlaceholder")
}
dir="ltr"
/>
</div>
{errors.credential && (
<motion.p
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="text-red-500 text-sm mt-1"
>
{errors.credential}
</motion.p>
)}
</div>
{/* Password */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
{t("password")}
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock
className={`w-5 h-5 transition-colors ${errors.password ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`}
/>
</div>
<input
type={showPassword ? "text" : "password"}
value={formData.password}
onChange={(e) => {
setFormData({
...formData,
password: e.target.value,
});
if (errors.password)
setErrors({ ...errors, password: null });
}}
className={`w-full pr-12 pl-12 py-4 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.password ? "border-red-500" : "border-gray-700"
}`}
placeholder={t("passwordPlaceholder")}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute inset-y-0 left-0 pl-3 flex items-center"
>
{showPassword ? (
<EyeOff className="w-5 h-5 text-gray-400 hover:text-gray-300 transition-colors" />
) : (
<Eye className="w-5 h-5 text-gray-400 hover:text-gray-300 transition-colors" />
)}
</button>
</div>
{errors.password && (
<motion.p
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="text-red-500 text-sm mt-1"
>
{errors.password}
</motion.p>
)}
</div>
{/* Remember + Forgot */}
<div className="flex items-center justify-between">
<label className="flex items-center gap-2 cursor-pointer group">
<input
type="checkbox"
checked={formData.rememberMe}
onChange={(e) =>
setFormData({
...formData,
rememberMe: e.target.checked,
})
}
className="w-4 h-4 rounded border-gray-600 bg-white/5 text-amber-500 focus:ring-amber-500 focus:ring-offset-0"
/>
<span className="text-sm text-gray-400 group-hover:text-white transition-colors">
{t("rememberMe")}
</span>
</label>
<Link
href="/forgot-password"
className="text-sm text-amber-400 hover:text-amber-300 transition-colors"
>
{t("forgotPassword")}
</Link>
</div>
{/* Submit */}
<motion.button
type="submit"
disabled={isLoading || isSuccess}
className="relative w-full bg-gradient-to-r from-amber-500 to-amber-600 text-white py-4 rounded-xl font-bold text-lg overflow-hidden group disabled:opacity-50 disabled:cursor-not-allowed"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<span className="relative z-10 flex items-center justify-center gap-2">
{isLoading ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
{t("loggingIn")}
</>
) : isSuccess ? (
<>
<CheckCircle className="w-5 h-5" />
{t("success")}
</>
) : (
<>
<LogIn className="w-5 h-5" />
{t("login")}
</>
)}
</span>
</motion.button>
</motion.form>
) : (
/* OTP Verification Step */
<motion.form
key="otp"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
onSubmit={handleVerifyOTP}
className="space-y-6"
>
<div className="text-center mb-4">
<Shield className="w-12 h-12 text-amber-500 mx-auto mb-3" />
<p className="text-gray-300 text-sm">
{t("sendOTPTo")}{" "}
<span className="text-white font-medium" dir="ltr">
{formData.credential}
</span>
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
{t("otpLabel")}
</label>
<input
type="text"
value={otpCode}
onChange={(e) => {
setOtpCode(e.target.value);
if (otpError) setOtpError("");
}}
className={`w-full px-4 py-4 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white text-center text-2xl tracking-[0.5em] placeholder-gray-500 transition-all ${
otpError ? "border-red-500" : "border-gray-700"
}`}
placeholder="______"
maxLength={6}
dir="ltr"
/>
{otpError && (
<motion.p
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="text-red-500 text-sm mt-1 text-center"
>
{otpError}
</motion.p>
)}
</div>
<motion.button
type="submit"
disabled={isLoading || isSuccess}
className="relative w-full bg-gradient-to-r from-amber-500 to-amber-600 text-white py-4 rounded-xl font-bold text-lg disabled:opacity-50 disabled:cursor-not-allowed"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<span className="flex items-center justify-center gap-2">
{isLoading ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
{t("verifying")}
</>
) : isSuccess ? (
<>
<CheckCircle className="w-5 h-5" />
{t("success")}
</>
) : (
<>
<KeyRound className="w-5 h-5" />
{t("verify")}
</>
)}
</span>
</motion.button>
<div className="flex items-center justify-between text-sm">
<button
type="button"
onClick={() => {
setStep("login");
setOtpCode("");
setOtpError("");
}}
className="text-gray-400 hover:text-white transition-colors"
>
{t("back")}
</button>
<button
type="button"
onClick={resendOTP}
className="text-amber-400 hover:text-amber-300 transition-colors"
>
{t("resendCode")}
</button>
</div>
</motion.form>
)}
</AnimatePresence>
<motion.p
variants={itemVariants}
className="text-center text-gray-400 mt-6"
>
{t("noAccount")}{" "}
<Link
href="/auth/choose-role"
className="text-amber-400 hover:text-amber-300 font-medium transition-colors"
>
{t("createNewAccount")}
</Link>
</motion.p>
</div>
</motion.div>
<motion.p
variants={itemVariants}
className="text-center text-gray-500 text-xs mt-4"
>
{t("agreeTerms")}{" "}
<Link
href="/terms"
className="text-amber-400 hover:text-amber-300 transition-colors"
>
{t("termsOfUse")}
</Link>{" "}
{t("and")}{" "}
<Link
href="/privacy"
className="text-amber-400 hover:text-amber-300 transition-colors"
>
{t("privacyPolicy")}
</Link>
</motion.p>
</motion.div>
</div>
);
} }

View File

@ -7,6 +7,7 @@ import toast, { Toaster } from 'react-hot-toast';
import { getCustomerRatings } from '../utils/ratings'; import { getCustomerRatings } from '../utils/ratings';
import AuthService from '../services/AuthService'; import AuthService from '../services/AuthService';
import StarRating from '../components/ratings/StarRating'; import StarRating from '../components/ratings/StarRating';
import { useEffect, useState } from 'react';
const RATING_FIELDS = [ const RATING_FIELDS = [
{ key: 'cleanRating', label: 'ratings.cleanliness' }, { key: 'cleanRating', label: 'ratings.cleanliness' },
@ -99,13 +100,7 @@ export default function MyRatesPage() {
} }
} }
if (loading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center" dir="rtl">
<Loader2 className="w-12 h-12 text-amber-500 animate-spin" />
</div>
);
}
const overall = calcOverall(ratings); const overall = calcOverall(ratings);

View File

@ -1,12 +1,13 @@
'use client'; "use client";
import { useEffect, useState } from 'react'; import { useEffect, useState } from "react";
import { useTranslation } from 'react-i18next'; import { useTranslation } from "react-i18next";
import { useRouter } from 'next/navigation'; import { useRouter } from "next/navigation";
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from "framer-motion";
import { Bell, CheckCircle, XCircle, Calendar, MessageCircle, CheckCheck, Loader2 } from 'lucide-react'; import { Bell, CheckCircle, XCircle, Calendar, MessageCircle, CheckCheck, Loader2 } from "lucide-react";
import AuthService from '@/app/services/AuthService'; import AuthService from "@/app/services/AuthService";
import { getUserNotifications } from '@/app/utils/api'; import { getUserNotifications } from "@/app/utils/api";
import Loading from "../loading";
export default function NotificationsPage() { export default function NotificationsPage() {
const { t } = useTranslation(); const { t } = useTranslation();
@ -18,7 +19,7 @@ export default function NotificationsPage() {
useEffect(() => { useEffect(() => {
if (!AuthService.isAuthenticated()) { if (!AuthService.isAuthenticated()) {
router.push('/login'); router.push("/login");
return; return;
} }
fetchNotifications(); fetchNotifications();
@ -33,36 +34,27 @@ export default function NotificationsPage() {
setNotifications(items); setNotifications(items);
setUnreadCount(items.length); setUnreadCount(items.length);
} catch (err) { } catch (err) {
console.error('Error fetching notifications:', err); console.error("Error fetching notifications:", err);
setError(err.message || t('fetch-notifications-failed')); setError(err.message || t("fetch-notifications-failed"));
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}; };
const markAsRead = (id) => { const markAsRead = (id) => {
setNotifications(prev => setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, read: true } : n)));
prev.map(n => (n.id === id ? { ...n, read: true } : n)) setUnreadCount((prev) => Math.max(0, prev - 1));
);
setUnreadCount(prev => Math.max(0, prev - 1));
}; };
const markAllAsRead = () => { const markAllAsRead = () => {
setNotifications(prev => prev.map(n => ({ ...n, read: true }))); setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
setUnreadCount(0); setUnreadCount(0);
}; };
if (isLoading) { if (isLoading) {
return ( return (
<div className="min-h-screen flex items-center justify-center"> <div>
<motion.div <Loading />
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="text-center"
>
<Loader2 className="w-12 h-12 text-amber-500 mx-auto mb-4 animate-spin" />
<p className="text-gray-600">{t('loading')}</p>
</motion.div>
</div> </div>
); );
} }
@ -70,13 +62,9 @@ export default function NotificationsPage() {
if (error) { if (error) {
return ( return (
<div className="min-h-screen flex items-center justify-center"> <div className="min-h-screen flex items-center justify-center">
<motion.div <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="text-center">
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center"
>
<XCircle className="w-16 h-16 text-red-500 mx-auto mb-4" /> <XCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-700 mb-2">{t('loading-error')}</h3> <h3 className="text-xl font-bold text-gray-700 mb-2">{t("loading-error")}</h3>
<p className="text-gray-500 mb-4">{error}</p> <p className="text-gray-500 mb-4">{error}</p>
<motion.button <motion.button
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
@ -84,7 +72,7 @@ export default function NotificationsPage() {
onClick={fetchNotifications} onClick={fetchNotifications}
className="px-6 py-2 bg-amber-500 text-white rounded-xl font-medium hover:bg-amber-600 transition-colors" className="px-6 py-2 bg-amber-500 text-white rounded-xl font-medium hover:bg-amber-600 transition-colors"
> >
{t('retry')} {t("retry")}
</motion.button> </motion.button>
</motion.div> </motion.div>
</div> </div>
@ -94,16 +82,10 @@ export default function NotificationsPage() {
return ( return (
<div className="min-h-screen bg-gray-50 py-8"> <div className="min-h-screen bg-gray-50 py-8">
<div className="container mx-auto px-4 max-w-4xl"> <div className="container mx-auto px-4 max-w-4xl">
<motion.div <motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="flex justify-between items-center mb-8">
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="flex justify-between items-center mb-8"
>
<div> <div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t('notifications')}</h1> <h1 className="text-3xl font-bold text-gray-900 mb-2">{t("notifications")}</h1>
<p className="text-gray-600"> <p className="text-gray-600">{unreadCount > 0 ? t("unread-notifications", { count: unreadCount }) : t("all-notifications-read")}</p>
{unreadCount > 0 ? t('unread-notifications', { count: unreadCount }) : t('all-notifications-read')}
</p>
</div> </div>
{unreadCount > 0 && ( {unreadCount > 0 && (
<motion.button <motion.button
@ -113,20 +95,16 @@ export default function NotificationsPage() {
className="flex items-center gap-2 px-4 py-2 bg-white border border-gray-200 rounded-xl text-sm font-medium text-gray-700 hover:bg-gray-50 transition-all shadow-sm" className="flex items-center gap-2 px-4 py-2 bg-white border border-gray-200 rounded-xl text-sm font-medium text-gray-700 hover:bg-gray-50 transition-all shadow-sm"
> >
<CheckCheck className="w-4 h-4 text-amber-500" /> <CheckCheck className="w-4 h-4 text-amber-500" />
{t('mark-all-read')} {t("mark-all-read")}
</motion.button> </motion.button>
)} )}
</motion.div> </motion.div>
{notifications.length === 0 ? ( {notifications.length === 0 ? (
<motion.div <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300"
>
<Bell className="w-16 h-16 text-gray-300 mx-auto mb-4" /> <Bell className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-700 mb-2">{t('no-notifications')}</h3> <h3 className="text-xl font-bold text-gray-700 mb-2">{t("no-notifications")}</h3>
<p className="text-gray-500">{t('notifications-empty-hint')}</p> <p className="text-gray-500">{t("notifications-empty-hint")}</p>
</motion.div> </motion.div>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
@ -137,32 +115,20 @@ export default function NotificationsPage() {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, x: 100 }} exit={{ opacity: 0, x: 100 }}
transition={{ delay: index * 0.05, type: 'spring', stiffness: 100 }} transition={{ delay: index * 0.05, type: "spring", stiffness: 100 }}
whileHover={{ scale: 1.01 }} whileHover={{ scale: 1.01 }}
onClick={() => markAsRead(notification.id)} onClick={() => markAsRead(notification.id)}
className={`bg-white rounded-2xl shadow-sm border transition-all hover:shadow-md cursor-pointer ${ className={`bg-white rounded-2xl shadow-sm border transition-all hover:shadow-md cursor-pointer ${!notification.read ? "border-amber-200 bg-amber-50/50" : "border-gray-200"}`}
!notification.read ? 'border-amber-200 bg-amber-50/50' : 'border-gray-200'
}`}
> >
<div className="p-5 flex gap-4"> <div className="p-5 flex gap-4">
<div className={`w-12 h-12 rounded-full flex items-center justify-center shrink-0 ${ <div className={`w-12 h-12 rounded-full flex items-center justify-center shrink-0 ${!notification.read ? "bg-amber-100" : "bg-gray-100"}`}>
!notification.read ? 'bg-amber-100' : 'bg-gray-100' {!notification.read ? <Bell className="w-6 h-6 text-amber-600" /> : <CheckCircle className="w-6 h-6 text-gray-400" />}
}`}>
{!notification.read ? (
<Bell className="w-6 h-6 text-amber-600" />
) : (
<CheckCircle className="w-6 h-6 text-gray-400" />
)}
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex justify-between items-start gap-4"> <div className="flex justify-between items-start gap-4">
<div className="min-w-0"> <div className="min-w-0">
<h3 className={`text-base ${!notification.read ? 'font-bold text-gray-900' : 'font-medium text-gray-700'}`}> <h3 className={`text-base ${!notification.read ? "font-bold text-gray-900" : "font-medium text-gray-700"}`}>{notification.title}</h3>
{notification.title} {notification.message && <p className="text-gray-500 text-sm mt-1 line-clamp-2">{notification.message}</p>}
</h3>
{notification.message && (
<p className="text-gray-500 text-sm mt-1 line-clamp-2">{notification.message}</p>
)}
<div className="flex items-center gap-2 mt-2"> <div className="flex items-center gap-2 mt-2">
{notification.date && ( {notification.date && (
<span className="text-xs text-gray-400 flex items-center gap-1"> <span className="text-xs text-gray-400 flex items-center gap-1">
@ -178,9 +144,7 @@ export default function NotificationsPage() {
)} )}
</div> </div>
</div> </div>
{!notification.read && ( {!notification.read && <span className="w-2.5 h-2.5 bg-amber-500 rounded-full shrink-0 mt-2" />}
<span className="w-2.5 h-2.5 bg-amber-500 rounded-full shrink-0 mt-2" />
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -76,7 +76,7 @@ export default function OnboardingPage() {
}; };
return ( return (
<div dir={i18n.language === 'ar' ? 'rtl' : 'ltr'} className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center p-4 relative overflow-hidden"> <div dir={i18n.language === 'ar' ? 'rtl' : 'ltr'} className="min-h-screen flex items-center justify-center p-4 relative overflow-hidden">
<div className="absolute inset-0 overflow-hidden"> <div className="absolute inset-0 overflow-hidden">
{[...Array(30)].map((_, i) => ( {[...Array(30)].map((_, i) => (
<motion.div <motion.div
@ -144,7 +144,7 @@ export default function OnboardingPage() {
transition={{ type: 'spring', stiffness: 200, delay: 0.1 }} transition={{ type: 'spring', stiffness: 200, delay: 0.1 }}
className="w-24 h-24 mx-auto bg-white/20 rounded-3xl flex items-center justify-center backdrop-blur-sm" className="w-24 h-24 mx-auto bg-white/20 rounded-3xl flex items-center justify-center backdrop-blur-sm"
> >
<IconComponent className="w-12 h-12 text-white" /> <IconComponent className="w-12 h-12 text-amber-50" />
</motion.div> </motion.div>
</div> </div>
</div> </div>
@ -154,7 +154,7 @@ export default function OnboardingPage() {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.15 }} transition={{ delay: 0.15 }}
className="text-3xl font-bold text-white mb-4" className="text-3xl font-bold text-amber-50 mb-4"
> >
{step.title} {step.title}
</motion.h1> </motion.h1>

View File

@ -1,32 +1,23 @@
'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, import Loading from "@/app/loading";
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>
); );
@ -74,11 +65,7 @@ export default function OwnerAccountBookPage() {
totalRevenue: data.totalRevenue ?? null, totalRevenue: data.totalRevenue ?? null,
totalReservations: data.totalReservations ?? null, totalReservations: data.totalReservations ?? null,
activeProperties: data.activeProperties ?? null, activeProperties: data.activeProperties ?? null,
financialRevenue: data.financialRevenue ?? data.totalRevenue ?? null,
financialRevenue:
data.financialRevenue ?? data.totalRevenue ?? null,
financialCommission: data.financialCommission ?? null, financialCommission: data.financialCommission ?? null,
financialBalance: data.financialBalance ?? null, financialBalance: data.financialBalance ?? null,
directRevenue: data.directRevenue ?? null, directRevenue: data.directRevenue ?? null,
@ -89,7 +76,7 @@ export default function OwnerAccountBookPage() {
}); });
} }
} catch { } catch {
toast.error(t('accountBook.loadError')); toast.error(t("accountBook.loadError"));
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@ -99,122 +86,54 @@ export default function OwnerAccountBookPage() {
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 <Loading />;
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<Loader2 className="w-12 h-12 text-amber-500 animate-spin mx-auto mb-4" />
<p className="text-gray-600">{t('accountBook.loading')}</p>
</div>
</div>
);
} }
return ( return (
<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>

View File

@ -1,8 +1,8 @@
'use client'; "use client";
import { motion } from 'framer-motion'; import { motion } from "framer-motion";
import { AlertTriangle, RefreshCw, Home } from 'lucide-react'; import { AlertTriangle, RefreshCw, Home } from "lucide-react";
import Link from 'next/link'; import Link from "next/link";
export default function Error({ error, reset }) { export default function Error({ error, reset }) {
return ( return (

View File

@ -1,14 +0,0 @@
'use client';
import { motion } from 'framer-motion';
export default function Loading() {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center">
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-center">
<div className="w-14 h-14 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<p className="text-gray-400 text-lg">جاري التحميل...</p>
</motion.div>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -1,14 +0,0 @@
'use client';
import { motion } from 'framer-motion';
export default function Loading() {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center">
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-center">
<div className="w-14 h-14 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<p className="text-gray-400 text-lg">جاري التحميل...</p>
</motion.div>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +0,0 @@
'use client';
import { motion } from 'framer-motion';
export default function Loading() {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center">
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-center">
<div className="w-14 h-14 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<p className="text-gray-400 text-lg">جاري التحميل...</p>
</motion.div>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -664,7 +664,8 @@ import {
} from 'lucide-react'; } 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 { getRentProperty } from '../../utils/api'; import { getRentProperties } from '../../utils/api';
import Loading from '@/app/loading';
const API_BASE = const API_BASE =
process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api'; process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api';
@ -756,8 +757,14 @@ async function enrich(r) {
} }
} }
const pAddr = (p) => p?.address ?? ''; const pAddr = (p, r) => p?.address ?? r?.propertyAddress ?? '';
const pImgs = (p) => (Array.isArray(p?.images) ? p.images : []); const pImgs = (p, r) => {
if (p?.images && Array.isArray(p.images)) return p.images;
if (r?.property?.images && Array.isArray(r.property.images)) return r.property.images;
if (r?.propertyInfo?.images && Array.isArray(r.propertyInfo.images)) return r.propertyInfo.images;
if (r?.propertyImage) return [r.propertyImage];
return [];
};
const pBeds = (p) => p?.numberOfBedRooms ?? 0; const pBeds = (p) => p?.numberOfBedRooms ?? 0;
const pBaths = (p) => p?.numberOfBathRooms ?? 0; const pBaths = (p) => p?.numberOfBathRooms ?? 0;
@ -893,9 +900,9 @@ function OwnerCard({
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const p = r._prop; const p = r._prop;
const imgs = pImgs(p); const imgs = pImgs(p, r);
const img = imgs.length > 0 ? buildImageUrl(imgs[0]) : null; const img = imgs.length > 0 ? buildImageUrl(imgs[0]) : null;
const addr = pAddr(p); const addr = pAddr(p, r);
const isPending = Number(r.status) === 0; const isPending = Number(r.status) === 0;
const isActionLoading = actionLoadingId === r.id; const isActionLoading = actionLoadingId === r.id;
const isReporting = reportingId === r.id; const isReporting = reportingId === r.id;
@ -1035,6 +1042,7 @@ function DetailsModal({ r, isOpen, onClose, onReport, reportingId }) {
const p = r._prop; const p = r._prop;
const isReporting = reportingId === r.id; const isReporting = reportingId === r.id;
const imgs = pImgs(p, r);
return ( return (
<motion.div <motion.div
@ -1066,6 +1074,12 @@ function DetailsModal({ r, isOpen, onClose, onReport, reportingId }) {
</div> </div>
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
{imgs.length > 0 && (
<div className="w-full h-56 rounded-xl overflow-hidden">
<img src={buildImageUrl(imgs[0])} alt="" className="w-full h-full object-cover" />
</div>
)}
{p && ( {p && (
<div className="bg-gray-50 p-4 rounded-xl"> <div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2"> <h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2">
@ -1075,7 +1089,7 @@ function DetailsModal({ r, isOpen, onClose, onReport, reportingId }) {
<p> <p>
<span className="text-gray-500">{t('address')}</span>{' '} <span className="text-gray-500">{t('address')}</span>{' '}
{pAddr(p) || '—'} {pAddr(p, r) || '—'}
</p> </p>
{(pBeds(p) || pBaths(p)) && ( {(pBeds(p) || pBaths(p)) && (
@ -1249,7 +1263,8 @@ export default function OwnerReservationRequestsPage() {
const token = AuthService.getToken(); const token = AuthService.getToken();
const res = await fetch( const [resResult, rentProps] = await Promise.all([
fetch(
`${API_BASE}/Reservations/GetOwnerResevationRequests`, `${API_BASE}/Reservations/GetOwnerResevationRequests`,
{ {
method: 'GET', method: 'GET',
@ -1258,19 +1273,34 @@ export default function OwnerReservationRequestsPage() {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
} }
); ).then(async (res) => {
if (!res.ok) { if (!res.ok) {
const errorText = await res.text(); const errorText = await res.text();
throw new Error(errorText || `HTTP ${res.status}`); throw new Error(errorText || `HTTP ${res.status}`);
} }
const json = await res.json(); const json = await res.json();
let list = json.data || json || []; let list = json.data || json || [];
if (!Array.isArray(list)) list = []; if (!Array.isArray(list)) list = [];
return list;
}),
getRentProperties().catch(() => []),
]);
const enriched = await Promise.all(list.map(enrich)); const propsList = Array.isArray(rentProps) ? rentProps : [];
const propMap = {};
propsList.forEach(rp => {
const info = rp?.propertyInformation ?? {};
if (rp?.allowedPaymentPeriod) info.allowedPaymentPeriod = rp.allowedPaymentPeriod;
propMap[rp.propertyInformationId] = info;
if (rp?.propertyInformation?.id) propMap[rp.propertyInformation.id] = info;
});
const enriched = resResult.map(r => {
if (r.propertyId && propMap[r.propertyId]) {
r._prop = propMap[r.propertyId];
}
return r;
});
setReservations(enriched); setReservations(enriched);
setFiltered(enriched); setFiltered(enriched);
@ -1305,7 +1335,7 @@ export default function OwnerReservationRequestsPage() {
result = result.filter( result = result.filter(
(x) => (x) =>
pAddr(x._prop).toLowerCase().includes(q) || String(x.id).includes(q) pAddr(x._prop, x).toLowerCase().includes(q) || String(x.id).includes(q)
); );
} }
@ -1404,9 +1434,7 @@ export default function OwnerReservationRequestsPage() {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center"> <Loading/>
<Loader2 className="w-12 h-12 text-amber-500 animate-spin" />
</div>
); );
} }

View File

@ -1,600 +1,9 @@
'use client'; import HomeClient from "./components/home/HomeClient";
import { useState, useRef, useEffect } from 'react'; export const metadata = {
import { motion, AnimatePresence } from 'framer-motion'; title: "الصفحة الرئيسية | عقاراتك",
import { usePathname } from 'next/navigation'; description: "ابحث عن العقارات والبيوت للإيجار والبيع بأسهل الطرق.",
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 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>
);
} }

View File

@ -1,73 +1,75 @@
"use client"; "use client";
import { useEffect, useState, useCallback } from 'react'; import { useEffect, useState, useCallback } from "react";
import Link from 'next/link'; import Link from "next/link";
import { motion } from 'framer-motion'; import { motion } from "framer-motion";
import { useTranslation } from 'react-i18next'; import { useTranslation } from "react-i18next";
import { import { Home, MapPin, Phone, ShieldCheck, CreditCard, Banknote, Building, ArrowLeftRight, Loader2, Check, X, Calendar, Clock, LogIn, Lock, Info, Landmark, Wallet } from "lucide-react";
Home, import toast, { Toaster } from "react-hot-toast";
MapPin, import AuthService from "@/app/services/AuthService";
Phone, import { payDeposit, getMyTransaction, getPaymentTypes } from "@/app/utils/api";
ShieldCheck, import Loading from "../loading";
CreditCard,
Banknote,
Building,
ArrowLeftRight,
Loader2,
Check,
X,
Calendar,
Clock,
LogIn,
Lock,
Info,
Landmark,
Wallet,
} from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
import AuthService from '@/app/services/AuthService';
import { payDeposit, getMyTransaction, getPaymentTypes } from '@/app/utils/api';
const STATUS_MAP = ['pending', 'ownerConfirmed', 'depositPaid', 'depositConfirmed', 'completed', 'cancelled']; const STATUS_MAP = ["pending", "ownerConfirmed", "depositPaid", "depositConfirmed", "completed", "cancelled"];
function getStatusConfig(t) { function getStatusConfig(t) {
return { return {
pending: { label: t('bookingStatus.pending'), color: 'bg-yellow-100 text-yellow-800 border-yellow-300', icon: Clock }, pending: { label: t("bookingStatus.pending"), color: "bg-yellow-100 text-yellow-800 border-yellow-300", icon: Clock },
ownerConfirmed: { label: t('bookingStatus.ownerConfirmed'), color: 'bg-blue-100 text-blue-800 border-blue-300', icon: ShieldCheck }, ownerConfirmed: { label: t("bookingStatus.ownerConfirmed"), color: "bg-blue-100 text-blue-800 border-blue-300", icon: ShieldCheck },
depositPaid: { label: t('bookingStatus.depositPaid'), color: 'bg-orange-100 text-orange-800 border-orange-300', icon: Wallet }, depositPaid: { label: t("bookingStatus.depositPaid"), color: "bg-orange-100 text-orange-800 border-orange-300", icon: Wallet },
depositConfirmed: { label: t('bookingStatus.depositConfirmed'), color: 'bg-green-100 text-green-800 border-green-300', icon: Check }, depositConfirmed: { label: t("bookingStatus.depositConfirmed"), color: "bg-green-100 text-green-800 border-green-300", icon: Check },
completed: { label: t('bookingStatus.completed'), color: 'bg-teal-100 text-teal-800 border-teal-300', icon: Check }, completed: { label: t("bookingStatus.completed"), color: "bg-teal-100 text-teal-800 border-teal-300", icon: Check },
cancelled: { label: t('bookingStatus.cancelled'), color: 'bg-red-100 text-red-800 border-red-300', icon: X }, cancelled: { label: t("bookingStatus.cancelled"), color: "bg-red-100 text-red-800 border-red-300", icon: X },
}; };
} }
function getPaymentMethods(t) { function getPaymentMethods(t) {
return [ return [
{ id: 'cash', label: t('payments.method.cash'), desc: t('payments.method.cashDesc'), icon: Banknote, active: true }, { id: "cash", label: t("payments.method.cash"), desc: t("payments.method.cashDesc"), icon: Banknote, active: true },
{ id: 'office', label: t('payments.method.office'), desc: t('payments.method.officeDesc'), icon: Building, active: true }, { id: "office", label: t("payments.method.office"), desc: t("payments.method.officeDesc"), icon: Building, active: true },
{ id: 'transfer', label: t('payments.method.transfer'), desc: t('payments.method.transferDesc'), icon: ArrowLeftRight, active: false }, { id: "transfer", label: t("payments.method.transfer"), desc: t("payments.method.transferDesc"), icon: ArrowLeftRight, active: false },
{ id: 'electronic', label: t('payments.method.electronic'), desc: t('payments.method.electronicDesc'), icon: CreditCard, active: false }, { id: "electronic", label: t("payments.method.electronic"), desc: t("payments.method.electronicDesc"), icon: CreditCard, active: false },
]; ];
} }
function formatCurrency(v, sign = '') { function formatCurrency(v, sign = "") {
return `${sign} ${Number(v ?? 0).toLocaleString()}`; return `${sign} ${Number(v ?? 0).toLocaleString()}`;
} }
function formatDate(date) { function formatDate(date) {
if (!date) return ''; if (!date) return "";
const d = new Date(date); const d = new Date(date);
if (Number.isNaN(d.getTime())) return ''; if (Number.isNaN(d.getTime())) return "";
return d.toLocaleDateString('en-GB'); return d.toLocaleDateString("en-GB");
} }
function normalizeText(value) { function normalizeText(value) {
return String(value ?? '').trim().toLowerCase(); return String(value ?? "")
.trim()
.toLowerCase();
} }
function isHaramText(value) { function isHaramText(value) {
const text = normalizeText(value); const text = normalizeText(value);
return text.includes('haram') || text.includes('هرم') || text.includes('هرام'); return text.includes("haram") || text.includes("هرم") || text.includes("هرام");
}
function isReceiptRequiredPayment(value, method = null) {
const haystack = [value, method?.name, method?.label, method?.id, method?.description].filter(Boolean).join(" ");
const text = normalizeText(haystack);
return (
text.includes("transfer") ||
text.includes("تحويل") ||
text.includes("حوالة") ||
text.includes("bank") ||
text.includes("بنك") ||
isHaramText(value) ||
isHaramText(method?.name) ||
isHaramText(method?.label) ||
isHaramText(method?.id) ||
isHaramText(method?.description)
);
} }
export default function PaymentsPage() { export default function PaymentsPage() {
@ -100,12 +102,12 @@ export default function PaymentsPage() {
endDate: reservation.endDate, endDate: reservation.endDate,
totalPrice: reservation.totalPrice ?? transaction.amount ?? 0, totalPrice: reservation.totalPrice ?? transaction.amount ?? 0,
depositAmount: transaction.amount ?? reservation.totalPrice ?? 0, depositAmount: transaction.amount ?? reservation.totalPrice ?? 0,
currencySign: currency.sign || t('currency.syp'), currencySign: currency.sign || t("currency.syp"),
currencyName: currency.name || '', currencyName: currency.name || "",
currencyRate: currency.rate, currencyRate: currency.rate,
propertyName: propertyInfo.name || propertyInfo.address || reservation.propertyName || `${t('payments.propertyLabel')} #${reservation.id || ''}`, propertyName: propertyInfo.name || propertyInfo.address || reservation.propertyName || `${t("payments.propertyLabel")} #${reservation.id || ""}`,
propertyAddress: propertyInfo.address || reservation.propertyAddress || '', propertyAddress: propertyInfo.address || reservation.propertyAddress || "",
propertyCity: propertyInfo.city || reservation.city || '', propertyCity: propertyInfo.city || reservation.city || "",
_deposit: deposit, _deposit: deposit,
_reservation: reservation, _reservation: reservation,
}; };
@ -114,7 +116,7 @@ export default function PaymentsPage() {
setReservations(mapped); setReservations(mapped);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
toast.error(t('payments.loadingTransactions')); toast.error(t("payments.loadingTransactions"));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -132,7 +134,7 @@ export default function PaymentsPage() {
setSelectedPayment(firstActive.name ?? firstActive.id ?? null); setSelectedPayment(firstActive.name ?? firstActive.id ?? null);
} }
} catch (err) { } catch (err) {
console.error('[Payments] failed to load payment methods', err); console.error("[Payments] failed to load payment methods", err);
setPaymentMethods([]); setPaymentMethods([]);
} finally { } finally {
setLoadingPaymentMethods(false); setLoadingPaymentMethods(false);
@ -154,24 +156,19 @@ export default function PaymentsPage() {
}, [loadPaymentMethods]); }, [loadPaymentMethods]);
const resolvePaymentTypeId = (paymentKey) => { const resolvePaymentTypeId = (paymentKey) => {
const method = paymentMethods.find( const method = paymentMethods.find((m) => String(m.id) === String(paymentKey) || String(m.name) === String(paymentKey));
(m) => String(m.id) === String(paymentKey) || String(m.name) === String(paymentKey),
);
return method?.id ?? paymentKey; return method?.id ?? paymentKey;
}; };
const handlePayDeposit = async (reservation, paymentKey, paymentImageFile) => { const handlePayDeposit = async (reservation, paymentKey, paymentImageFile) => {
const paymentTypeId = resolvePaymentTypeId(paymentKey); const paymentTypeId = resolvePaymentTypeId(paymentKey);
const selectedMethod = paymentMethods.find( const selectedMethod = paymentMethods.find((m) => String(m.id) === String(paymentTypeId) || String(m.name) === String(paymentTypeId));
(m) => String(m.id) === String(paymentTypeId) || String(m.name) === String(paymentTypeId), const requiresReceiptUpload = isReceiptRequiredPayment(paymentKey, selectedMethod);
);
const isHaram = if (requiresReceiptUpload && !paymentImageFile) {
isHaramText(selectedMethod?.name) || toast.error(t("payments.receiptRequired"));
isHaramText(selectedMethod?.label) || return;
isHaramText(selectedMethod?.id) || }
isHaramText(selectedMethod?.description) ||
isHaramText(paymentKey);
setPayingId(reservation.id); setPayingId(reservation.id);
try { try {
@ -180,46 +177,33 @@ export default function PaymentsPage() {
paymentTypeId, paymentTypeId,
paymentImage: paymentImageFile, paymentImage: paymentImageFile,
}); });
toast.success(t('payments.depositPaidSuccess')); toast.success(t("payments.depositPaidSuccess"));
loadReservations(); loadReservations();
} catch (err) { } catch (err) {
toast.error(err?.message || t('payments.paymentFailed')); toast.error(err?.message || t("payments.paymentFailed"));
} finally { } finally {
setPayingId(null); setPayingId(null);
} }
}; };
const canPay = (status) => STATUS_MAP[status] === 'ownerConfirmed'; const canPay = (status) => STATUS_MAP[status] === "ownerConfirmed";
if (loading) { if (loading) {
return ( return <Loading />;
<div className="min-h-screen bg-gray-50 flex items-center justify-center" dir={i18n.dir()}>
<Loader2 className="w-12 h-12 text-amber-500 animate-spin" />
</div>
);
} }
if (isGuest) { if (isGuest) {
return ( return (
<div className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white flex items-center justify-center p-4" dir={i18n.dir()}> <div className="min-h-screen bg-linear-to-b from-amber-50/50 to-white flex items-center justify-center p-4" dir={i18n.dir()}>
<motion.div <motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} className="bg-white rounded-3xl shadow-xl border border-gray-200 p-10 max-w-md w-full text-center">
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="bg-white rounded-3xl shadow-xl border border-gray-200 p-10 max-w-md w-full text-center"
>
<div className="w-20 h-20 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-6"> <div className="w-20 h-20 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-6">
<Lock className="w-10 h-10 text-amber-600" /> <Lock className="w-10 h-10 text-amber-600" />
</div> </div>
<h2 className="text-2xl font-bold text-gray-900 mb-3">{t('payments.title')}</h2> <h2 className="text-2xl font-bold text-gray-900 mb-3">{t("payments.title")}</h2>
<p className="text-gray-600 leading-relaxed mb-8"> <p className="text-gray-600 leading-relaxed mb-8">{t("payments.loginRequiredDesc")}</p>
{t('payments.loginRequiredDesc')} <Link href="/login" className="inline-flex items-center gap-2 bg-amber-500 hover:bg-amber-600 text-white px-8 py-3 rounded-2xl text-lg font-semibold transition shadow-lg shadow-amber-200">
</p>
<Link
href="/login"
className="inline-flex items-center gap-2 bg-amber-500 hover:bg-amber-600 text-white px-8 py-3 rounded-2xl text-lg font-semibold transition shadow-lg shadow-amber-200"
>
<LogIn className="w-5 h-5" /> <LogIn className="w-5 h-5" />
{t('login')} {t("login")}
</Link> </Link>
</motion.div> </motion.div>
</div> </div>
@ -233,20 +217,16 @@ export default function PaymentsPage() {
<div className="min-h-screen bg-gray-50 py-8" dir={i18n.dir()}> <div className="min-h-screen bg-gray-50 py-8" dir={i18n.dir()}>
<Toaster position="top-center" reverseOrder={false} /> <Toaster position="top-center" reverseOrder={false} />
<div className="container mx-auto px-4 max-w-4xl"> <div className="container mx-auto px-4 max-w-4xl">
<motion.div <motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="mb-8">
initial={{ opacity: 0, y: -20 }} <h1 className="text-3xl font-bold text-gray-900 mb-2">{t("payments.title")}</h1>
animate={{ opacity: 1, y: 0 }} <p className="text-gray-600">{t("payments.description")}</p>
className="mb-8"
>
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t('payments.title')}</h1>
<p className="text-gray-600">{t('payments.description')}</p>
</motion.div> </motion.div>
{payables.length > 0 && ( {payables.length > 0 && (
<div className="space-y-6 mb-10"> <div className="space-y-6 mb-10">
<h2 className="text-xl font-bold text-gray-800 flex items-center gap-2"> <h2 className="text-xl font-bold text-gray-800 flex items-center gap-2">
<Wallet className="w-5 h-5 text-amber-500" /> <Wallet className="w-5 h-5 text-amber-500" />
{t('payments.payablesTitle')} {t("payments.payablesTitle")}
</h2> </h2>
{payables.map((r, i) => ( {payables.map((r, i) => (
<PaymentCard <PaymentCard
@ -267,21 +247,16 @@ export default function PaymentsPage() {
<div className="space-y-4"> <div className="space-y-4">
<h2 className="text-xl font-bold text-gray-800 flex items-center gap-2"> <h2 className="text-xl font-bold text-gray-800 flex items-center gap-2">
<Clock className="w-5 h-5 text-gray-500" /> <Clock className="w-5 h-5 text-gray-500" />
{t('payments.previousReservationsTitle')} {t("payments.previousReservationsTitle")}
</h2> </h2>
{others.map((r, i) => { {others.map((r, i) => {
const statusKey = STATUS_MAP[r.status] || 'pending'; const statusKey = STATUS_MAP[r.status] || "pending";
const cfg = getStatusConfig(t)[statusKey]; const cfg = getStatusConfig(t)[statusKey];
const Icon = cfg.icon; const Icon = cfg.icon;
const amount = r.depositAmount || r.totalPrice || 0; const amount = r.depositAmount || r.totalPrice || 0;
return ( return (
<motion.div <motion.div key={r.id || i} initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="bg-white rounded-2xl shadow-sm border border-gray-200 p-5">
key={r.id || i}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-2xl shadow-sm border border-gray-200 p-5"
>
<div className="flex items-center justify-between gap-3 mb-3"> <div className="flex items-center justify-between gap-3 mb-3">
<span className="text-sm font-medium text-gray-400">#{r.reservationId || r.id}</span> <span className="text-sm font-medium text-gray-400">#{r.reservationId || r.id}</span>
<span className={`inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium border ${cfg.color}`}> <span className={`inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium border ${cfg.color}`}>
@ -294,9 +269,7 @@ export default function PaymentsPage() {
<Calendar className="w-4 h-4" /> <Calendar className="w-4 h-4" />
{formatDate(r.startDate)} - {formatDate(r.endDate)} {formatDate(r.startDate)} - {formatDate(r.endDate)}
</div> </div>
<div className="text-lg font-bold text-gray-900"> <div className="text-lg font-bold text-gray-900">{formatCurrency(amount, r.currencySign)}</div>
{formatCurrency(amount, r.currencySign)}
</div>
</div> </div>
</motion.div> </motion.div>
); );
@ -305,14 +278,10 @@ export default function PaymentsPage() {
)} )}
{reservations.length === 0 && ( {reservations.length === 0 && (
<motion.div <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300"
>
<CreditCard className="w-16 h-16 text-gray-300 mx-auto mb-4" /> <CreditCard className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-700 mb-2">{t('payments.noTransactions')}</h3> <h3 className="text-xl font-bold text-gray-700 mb-2">{t("payments.noTransactions")}</h3>
<p className="text-gray-500">{t('payments.noTransactionsDesc')}</p> <p className="text-gray-500">{t("payments.noTransactionsDesc")}</p>
</motion.div> </motion.div>
)} )}
</div> </div>
@ -326,7 +295,10 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
const r = reservation; const r = reservation;
const amount = r.depositAmount || r.totalPrice || 0; const amount = r.depositAmount || r.totalPrice || 0;
const [showCashDialog, setShowCashDialog] = useState(false); const [showCashDialog, setShowCashDialog] = useState(false);
const [showReceiptModal, setShowReceiptModal] = useState(false);
const [localPaymentImage, setLocalPaymentImage] = useState(null); const [localPaymentImage, setLocalPaymentImage] = useState(null);
const [receiptPreviewUrl, setReceiptPreviewUrl] = useState("");
const [receiptFileName, setReceiptFileName] = useState("");
const methods = paymentMethods.length > 0 ? paymentMethods : getPaymentMethods(t); const methods = paymentMethods.length > 0 ? paymentMethods : getPaymentMethods(t);
@ -339,20 +311,52 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
return methodName === paymentKey || methodLabel === paymentKey || methodId === paymentKey; return methodName === paymentKey || methodLabel === paymentKey || methodId === paymentKey;
}); });
const isHaramPayment = const requiresReceiptUpload = isReceiptRequiredPayment(selectedPayment, selectedMethod);
isHaramText(selectedPayment) ||
isHaramText(selectedMethod?.name) || useEffect(() => {
isHaramText(selectedMethod?.label) || if (!localPaymentImage) {
isHaramText(selectedMethod?.id) || setReceiptPreviewUrl("");
isHaramText(selectedMethod?.description); return undefined;
}
const previewUrl = URL.createObjectURL(localPaymentImage);
setReceiptPreviewUrl(previewUrl);
return () => URL.revokeObjectURL(previewUrl);
}, [localPaymentImage]);
const handleSelectMethod = (optionId, method) => {
onSelectPayment(optionId);
if (isReceiptRequiredPayment(optionId, method)) {
setShowReceiptModal(true);
return;
}
setShowReceiptModal(false);
setLocalPaymentImage(null);
setReceiptPreviewUrl("");
setReceiptFileName("");
};
const handleReceiptSelection = (event) => {
const file = event.target.files?.[0] || null;
setLocalPaymentImage(file);
setReceiptFileName(file?.name || "");
};
const handleReceiptConfirm = () => {
if (!localPaymentImage) {
toast.error(t("payments.receiptRequired"));
return;
}
setShowReceiptModal(false);
};
return ( return (
<> <>
<motion.div <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="bg-white rounded-3xl shadow-md border border-gray-200 overflow-hidden">
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-3xl shadow-md border border-gray-200 overflow-hidden"
>
{/* Property details */} {/* Property details */}
<div className="p-6 border-b border-gray-100"> <div className="p-6 border-b border-gray-100">
<div className="flex items-start gap-4 mb-4"> <div className="flex items-start gap-4 mb-4">
@ -360,13 +364,11 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
<Home className="w-7 h-7 text-amber-600" /> <Home className="w-7 h-7 text-amber-600" />
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<h3 className="text-xl font-bold text-gray-900 mb-1"> <h3 className="text-xl font-bold text-gray-900 mb-1">{r.propertyName}</h3>
{r.propertyName}
</h3>
{(r.propertyAddress || r.propertyCity) && ( {(r.propertyAddress || r.propertyCity) && (
<p className="text-sm text-gray-500 flex items-center gap-1"> <p className="text-sm text-gray-500 flex items-center gap-1">
<MapPin className="w-3.5 h-3.5" /> <MapPin className="w-3.5 h-3.5" />
{[r.propertyCity, r.propertyAddress].filter(Boolean).join(' - ')} {[r.propertyCity, r.propertyAddress].filter(Boolean).join(" - ")}
</p> </p>
)} )}
</div> </div>
@ -378,8 +380,7 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
{formatDate(r.startDate)} - {formatDate(r.endDate)} {formatDate(r.startDate)} - {formatDate(r.endDate)}
</span> </span>
<span className="flex items-center gap-1.5"> <span className="flex items-center gap-1.5">
<Clock className="w-4 h-4 text-gray-400" /> <Clock className="w-4 h-4 text-gray-400" />#{r.reservationId || r.id}
#{r.reservationId || r.id}
</span> </span>
</div> </div>
</div> </div>
@ -387,13 +388,9 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
{/* Deposit amount */} {/* Deposit amount */}
<div className="px-6 py-5 border-b border-gray-100"> <div className="px-6 py-5 border-b border-gray-100">
<div className="text-center"> <div className="text-center">
<p className="text-sm text-gray-500 mb-1">{t('payments.depositAmount')}</p> <p className="text-sm text-gray-500 mb-1">{t("payments.depositAmount")}</p>
<p className="text-4xl font-bold text-amber-600"> <p className="text-4xl font-bold text-amber-600">{formatCurrency(amount, r.currencySign)}</p>
{formatCurrency(amount, r.currencySign)} <p className="text-xs text-gray-400 mt-2">{t("payments.depositPaidToPlatform")}</p>
</p>
<p className="text-xs text-gray-400 mt-2">
{t('payments.depositPaidToPlatform')}
</p>
</div> </div>
</div> </div>
@ -401,24 +398,18 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
<div className="px-6 py-4 bg-amber-50 border-b border-amber-100"> <div className="px-6 py-4 bg-amber-50 border-b border-amber-100">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<Info className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" /> <Info className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" />
<p className="text-sm text-amber-800 leading-relaxed"> <p className="text-sm text-amber-800 leading-relaxed">{t("payments.cashOnlyNotice")}</p>
{t('payments.cashOnlyNotice')}
</p>
</div> </div>
</div> </div>
{/* Payment method options */} {/* Payment method options */}
<div className="px-6 py-5 border-b border-gray-100"> <div className="px-6 py-5 border-b border-gray-100">
<p className="text-sm font-bold text-gray-700 mb-3">{t('payments.paymentMethodLabel')}</p> <p className="text-sm font-bold text-gray-700 mb-3">{t("payments.paymentMethodLabel")}</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{loadingPaymentMethods ? ( {loadingPaymentMethods ? (
<div className="col-span-full rounded-2xl border border-dashed border-gray-200 bg-gray-50 p-4 text-sm text-gray-600"> <div className="col-span-full rounded-2xl border border-dashed border-gray-200 bg-gray-50 p-4 text-sm text-gray-600">{t("payments.loadingPaymentMethods")}</div>
{t('payments.loadingPaymentMethods')}
</div>
) : methods.length === 0 ? ( ) : methods.length === 0 ? (
<div className="col-span-full rounded-2xl border border-dashed border-gray-200 bg-gray-50 p-4 text-sm text-gray-600"> <div className="col-span-full rounded-2xl border border-dashed border-gray-200 bg-gray-50 p-4 text-sm text-gray-600">{t("payments.noPaymentMethodsAvailable")}</div>
{t('payments.noPaymentMethodsAvailable')}
</div>
) : ( ) : (
methods.map((method, index) => { methods.map((method, index) => {
const optionId = method.name ?? method.id ?? `payment-method-${index}`; const optionId = method.name ?? method.id ?? `payment-method-${index}`;
@ -431,37 +422,23 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
key={optionId} key={optionId}
type="button" type="button"
disabled={!isActive} disabled={!isActive}
onClick={() => isActive && onSelectPayment(optionId)} onClick={() => isActive && handleSelectMethod(optionId, method)}
className={`relative flex items-start gap-3 p-4 rounded-2xl border-2 text-right transition-all ${ className={`relative flex items-start gap-3 p-4 rounded-2xl border-2 text-right transition-all ${
!isActive !isActive
? 'border-gray-100 bg-gray-50 opacity-50 cursor-not-allowed' ? "border-gray-100 bg-gray-50 opacity-50 cursor-not-allowed"
: isSelected : isSelected
? 'border-amber-500 bg-amber-50 shadow-sm' ? "border-amber-500 bg-amber-50 shadow-sm"
: 'border-gray-200 bg-white hover:border-amber-300 cursor-pointer' : "border-gray-200 bg-white hover:border-amber-300 cursor-pointer"
}`}
>
<div
className={`w-10 h-10 rounded-xl flex items-center justify-center shrink-0 ${
isSelected ? 'bg-amber-500 text-white' : 'bg-gray-100 text-gray-500'
}`} }`}
> >
<div className={`w-10 h-10 rounded-xl flex items-center justify-center shrink-0 ${isSelected ? "bg-amber-500 text-white" : "bg-gray-100 text-gray-500"}`}>
<Icon className="w-5 h-5" /> <Icon className="w-5 h-5" />
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className={`text-sm font-bold ${isSelected ? 'text-amber-900' : 'text-gray-800'}`}> <p className={`text-sm font-bold ${isSelected ? "text-amber-900" : "text-gray-800"}`}>{method.name || method.label || t("payments.paymentMethodName")}</p>
{method.name || method.label || t('payments.paymentMethodName')} {method.description && <p className="text-xs text-gray-500 mt-0.5 leading-relaxed">{method.description}</p>}
</p> <span className={`inline-block mt-1 text-[10px] font-medium px-2 py-0.5 rounded-full ${isActive ? "bg-amber-100 text-amber-700" : "bg-gray-200 text-gray-500"}`}>
{method.description && ( {isActive ? t("payments.methodActive") : t("payments.methodInactive")}
<p className="text-xs text-gray-500 mt-0.5 leading-relaxed">
{method.description}
</p>
)}
<span
className={`inline-block mt-1 text-[10px] font-medium px-2 py-0.5 rounded-full ${
isActive ? 'bg-amber-100 text-amber-700' : 'bg-gray-200 text-gray-500'
}`}
>
{isActive ? t('payments.methodActive') : t('payments.methodInactive')}
</span> </span>
</div> </div>
{isSelected && ( {isSelected && (
@ -476,18 +453,12 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
</div> </div>
</div> </div>
{/* Upload receipt only for Haram */} {requiresReceiptUpload && (
{isHaramPayment && ( <div className="px-6 py-4 bg-amber-50 border-b border-amber-100">
<div className="px-6 py-5 border-b border-gray-100"> <div className="flex items-start gap-2 text-sm text-amber-800">
<label className="block text-sm font-bold text-gray-700 mb-3"> <Info className="w-4 h-4 shrink-0 mt-0.5" />
{t('payments.uploadReceipt')} <p>{t("payments.receiptUploadRequiredHint")}</p>
</label> </div>
<input
type="file"
accept="image/*"
onChange={(e) => setLocalPaymentImage(e.target.files?.[0] || null)}
className="block w-full rounded-2xl border border-gray-300 bg-white px-4 py-3 text-sm text-gray-700 file:mr-4 file:rounded-xl file:border-0 file:bg-amber-500 file:px-4 file:py-2 file:text-white hover:file:bg-amber-600"
/>
</div> </div>
)} )}
@ -504,17 +475,62 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
{payingId === r.id ? ( {payingId === r.id ? (
<> <>
<Loader2 className="w-5 h-5 animate-spin" /> <Loader2 className="w-5 h-5 animate-spin" />
{t('payments.processingPayment')} {t("payments.processingPayment")}
</> </>
) : ( ) : (
<> <>
<Banknote className="w-5 h-5" /> <Banknote className="w-5 h-5" />
{t('payments.payButton', { amount: formatCurrency(amount, r.currencySign) })} {t("payments.payButton", { amount: formatCurrency(amount, r.currencySign) })}
</> </>
)} )}
</motion.button> </motion.button>
</div> </div>
{showReceiptModal && (
<div className="fixed inset-0 z-60 flex items-center justify-center bg-black/60 px-4 py-8">
<motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} className="w-full max-w-md rounded-3xl bg-white p-6 shadow-2xl border border-gray-200">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-xl font-bold text-gray-900">{t("payments.receiptDialogTitle")}</h3>
<p className="text-sm text-gray-600 mt-1">{t("payments.receiptDialogDescription")}</p>
</div>
<div className="w-12 h-12 bg-amber-100 rounded-2xl flex items-center justify-center">
<CreditCard className="w-6 h-6 text-amber-600" />
</div>
</div>
<label className="block text-sm font-bold text-gray-700 mb-2">{t("payments.uploadReceipt")}</label>
<input
type="file"
accept="image/*"
onChange={handleReceiptSelection}
className="block w-full rounded-2xl border border-gray-300 bg-white px-4 py-3 text-sm text-gray-700 file:mr-4 file:rounded-xl file:border-0 file:bg-amber-500 file:px-4 file:py-2 file:text-white hover:file:bg-amber-600"
/>
{receiptFileName && (
<div className="mt-3 rounded-2xl border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800">
{t("payments.receiptSelected")} {receiptFileName}
</div>
)}
{localPaymentImage && receiptPreviewUrl && (
<div className="mt-4 overflow-hidden rounded-2xl border border-gray-200 bg-gray-50 p-2">
<img src={receiptPreviewUrl} alt={t("payments.uploadReceipt")} className="h-48 w-full rounded-xl object-cover" />
</div>
)}
<div className="mt-6 flex flex-col gap-3 sm:flex-row">
<button type="button" onClick={() => setShowReceiptModal(false)} className="w-full rounded-xl border border-gray-300 px-4 py-3 text-sm font-semibold text-gray-700 hover:bg-gray-100">
{t("payments.closeButton")}
</button>
<button type="button" onClick={handleReceiptConfirm} className="w-full rounded-xl bg-amber-500 px-4 py-3 text-sm font-semibold text-white hover:bg-amber-600">
{t("payments.continueToPay")}
</button>
</div>
</motion.div>
</div>
)}
{/* Cash pay button */} {/* Cash pay button */}
<div className="px-6 py-4 border-b border-gray-100"> <div className="px-6 py-4 border-b border-gray-100">
<button <button
@ -523,30 +539,26 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
className="w-full bg-white border-2 border-amber-200 hover:border-amber-400 text-amber-700 font-bold py-3 px-6 rounded-2xl text-base transition-all flex items-center justify-center gap-2" className="w-full bg-white border-2 border-amber-200 hover:border-amber-400 text-amber-700 font-bold py-3 px-6 rounded-2xl text-base transition-all flex items-center justify-center gap-2"
> >
<Building className="w-5 h-5" /> <Building className="w-5 h-5" />
{t('payments.payCashButton')} {t("payments.payCashButton")}
</button> </button>
</div> </div>
{/* Cash payment dialog */} {/* Cash payment dialog */}
{showCashDialog && ( {showCashDialog && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 px-4 py-8"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 px-4 py-8">
<motion.div <motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} className="w-full max-w-md rounded-3xl bg-white p-8 shadow-2xl border border-gray-200 text-center">
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="w-full max-w-md rounded-3xl bg-white p-8 shadow-2xl border border-gray-200 text-center"
>
<div className="w-16 h-16 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-5"> <div className="w-16 h-16 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-5">
<Clock className="w-8 h-8 text-amber-600" /> <Clock className="w-8 h-8 text-amber-600" />
</div> </div>
<h3 className="text-xl font-bold text-gray-900 mb-3">{t('payments.cashPendingDialogTitle')}</h3> <h3 className="text-xl font-bold text-gray-900 mb-3">{t("payments.cashPendingDialogTitle")}</h3>
<p className="text-gray-600 leading-relaxed mb-6"> <p className="text-gray-600 leading-relaxed mb-6">
{t('payments.cashPendingDialogDesc1')} {t("payments.cashPendingDialogDesc1")}
<br /> <br />
{t('payments.cashPendingDialogDesc2')} {t("payments.cashPendingDialogDesc2")}
</p> </p>
<div className="bg-gray-50 rounded-2xl p-4 mb-6 text-right"> <div className="bg-gray-50 rounded-2xl p-4 mb-6 text-right">
<p className="text-sm font-bold text-gray-800 mb-1">{t('payments.platformOfficeLabel')}</p> <p className="text-sm font-bold text-gray-800 mb-1">{t("payments.platformOfficeLabel")}</p>
<p className="text-sm text-gray-600">{t('payments.platformOfficeFullAddress')}</p> <p className="text-sm text-gray-600">{t("payments.platformOfficeFullAddress")}</p>
<p className="text-sm text-gray-600 flex items-center gap-1 mt-1" dir="ltr"> <p className="text-sm text-gray-600 flex items-center gap-1 mt-1" dir="ltr">
<Phone className="w-3.5 h-3.5" /> <Phone className="w-3.5 h-3.5" />
+963567823411 +963567823411
@ -558,13 +570,10 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
onClick={() => setShowCashDialog(false)} onClick={() => setShowCashDialog(false)}
className="w-full px-5 py-3 rounded-xl border border-gray-300 text-gray-700 hover:bg-gray-100 transition-colors font-medium" className="w-full px-5 py-3 rounded-xl border border-gray-300 text-gray-700 hover:bg-gray-100 transition-colors font-medium"
> >
{t('payments.closeButton')} {t("payments.closeButton")}
</button> </button>
<Link <Link href="/reservations" className="w-full px-5 py-3 rounded-xl bg-amber-500 text-white font-semibold text-center hover:bg-amber-600 transition-colors">
href="/reservations" {t("payments.myReservationsLink")}
className="w-full px-5 py-3 rounded-xl bg-amber-500 text-white font-semibold text-center hover:bg-amber-600 transition-colors"
>
{t('payments.myReservationsLink')}
</Link> </Link>
</div> </div>
</motion.div> </motion.div>
@ -576,10 +585,8 @@ function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMeth
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<Landmark className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" /> <Landmark className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" />
<div> <div>
<p className="text-sm font-bold text-gray-800 mb-1">{t('payments.cashPaymentLocationLabel')}</p> <p className="text-sm font-bold text-gray-800 mb-1">{t("payments.cashPaymentLocationLabel")}</p>
<p className="text-sm text-gray-600 leading-relaxed"> <p className="text-sm text-gray-600 leading-relaxed">{t("payments.platformOfficeFullAddress")}</p>
{t('payments.platformOfficeFullAddress')}
</p>
<p className="text-sm text-gray-600 flex items-center gap-1 mt-1"> <p className="text-sm text-gray-600 flex items-center gap-1 mt-1">
<Phone className="w-3.5 h-3.5" /> <Phone className="w-3.5 h-3.5" />
<span dir="ltr">+963567823411</span> <span dir="ltr">+963567823411</span>

View File

@ -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>
);
}

View File

@ -5,24 +5,11 @@ import { motion } from "framer-motion";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import { User, Mail, Phone, MessageCircle, Check, X, ArrowLeft, Building, Home, Calendar, MapPin, Loader2, Pencil } from "lucide-react";
User,
Mail,
Phone,
MessageCircle,
Check,
X,
ArrowLeft,
Building,
Home,
Calendar,
MapPin,
Loader2,
Pencil,
} from "lucide-react";
import toast, { Toaster } from "react-hot-toast"; import toast, { Toaster } from "react-hot-toast";
import AuthService from "../services/AuthService"; import AuthService from "../services/AuthService";
import { getCustomerByUserId, getOwnerByUserId } from "../utils/api"; import { getCustomerByUserId, getOwnerByUserId } from "../utils/api";
import InteractiveBackground from "../components/Animation/Background";
export default function ProfilePage() { export default function ProfilePage() {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
@ -59,30 +46,23 @@ export default function ProfilePage() {
async function fetchProfile() { async function fetchProfile() {
try { try {
const fetchFn = const fetchFn = userData.role === "owner" ? getOwnerByUserId : getCustomerByUserId;
userData.role === "owner" ? getOwnerByUserId : getCustomerByUserId;
const profile = await fetchFn(userData.id); const profile = await fetchFn(userData.id);
if (profile) { if (profile) {
const profileData = { const profileData = {
name: name: profile.fullName || profile.name || `${profile.firstName || ""} ${profile.lastName || ""}`.trim() || userData.name || "",
profile.fullName ||
profile.name ||
`${profile.firstName || ""} ${profile.lastName || ""}`.trim() ||
userData.name ||
"",
email: profile.email || userData.email || "", email: profile.email || userData.email || "",
phone: phone: profile.phone || profile.phoneNumber || userData.phone || "",
profile.phone || profile.phoneNumber || userData.phone || "",
whatsapp: profile.whatsAppNumber || profile.whatsapp || "", whatsapp: profile.whatsAppNumber || profile.whatsapp || "",
bio: profile.bio || "", bio: profile.bio || "",
location: profile.address || profile.location || "", location: profile.address || profile.location || "",
joinedDate: profile.createdAt joinedDate: profile.createdAt
? new Date(profile.createdAt).toLocaleDateString(i18n.language === 'ar' ? 'ar-SA' : 'en-US', { ? new Date(profile.createdAt).toLocaleDateString(i18n.language === "ar" ? "ar-SA" : "en-US", {
month: "long", month: "long",
year: "numeric", year: "numeric",
}) })
: new Date().toLocaleDateString(i18n.language === 'ar' ? 'ar-SA' : 'en-US', { : new Date().toLocaleDateString(i18n.language === "ar" ? "ar-SA" : "en-US", {
month: "long", month: "long",
year: "numeric", year: "numeric",
}), }),
@ -109,7 +89,7 @@ export default function ProfilePage() {
whatsapp: "", whatsapp: "",
bio: "", bio: "",
location: "", location: "",
joinedDate: new Date().toLocaleDateString(i18n.language === 'ar' ? 'ar-SA' : 'en-US', { joinedDate: new Date().toLocaleDateString(i18n.language === "ar" ? "ar-SA" : "en-US", {
month: "long", month: "long",
year: "numeric", year: "numeric",
}), }),
@ -146,7 +126,7 @@ export default function ProfilePage() {
setFormData(updatedData); setFormData(updatedData);
localStorage.setItem("userProfile", JSON.stringify(updatedData)); localStorage.setItem("userProfile", JSON.stringify(updatedData));
setEditingBio(false); setEditingBio(false);
toast.success(t('bio-updated-success')); toast.success(t("bio-updated-success"));
}; };
const fadeInUp = { const fadeInUp = {
@ -160,37 +140,25 @@ export default function ProfilePage() {
<div className="min-h-screen bg-gray-50 flex items-center justify-center"> <div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center"> <div className="text-center">
<Loader2 className="w-12 h-12 text-amber-500 animate-spin mx-auto mb-4" /> <Loader2 className="w-12 h-12 text-amber-500 animate-spin mx-auto mb-4" />
<p className="text-gray-600">{t('loading-profile')}</p> <p className="text-gray-600">{t("loading-profile")}</p>
</div> </div>
</div> </div>
); );
} }
return ( return (
<div dir={i18n.language === 'ar' ? 'rtl' : 'ltr'} className="min-h-screen bg-gray-50 py-8"> <div dir={i18n.language === "ar" ? "rtl" : "ltr"} className="min-h-screen bg-gray-50 py-8">
<Toaster position="top-center" reverseOrder={false} /> <Toaster position="top-center" reverseOrder={false} />
<InteractiveBackground />
<div className="container mx-auto px-4 max-w-4xl"> <div className="relative z-10 container mx-auto px-4 max-w-2xl">
<motion.div <motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="flex justify-between items-center mb-6">
initial={{ opacity: 0, y: -20 }} <Link href="/" className="flex items-center gap-2 text-gray-600 hover:text-amber-600 transition-colors">
animate={{ opacity: 1, y: 0 }}
className="flex justify-between items-center mb-6"
>
<Link
href="/"
className="flex items-center gap-2 text-gray-600 hover:text-amber-600 transition-colors"
>
<ArrowLeft className="w-5 h-5" /> <ArrowLeft className="w-5 h-5" />
<span>{t('backToHome')}</span> <span>{t("backToHome")}</span>
</Link> </Link>
</motion.div> </motion.div>
<motion.div <motion.div variants={fadeInUp} initial="initial" animate="animate" className="bg-white rounded-3xl shadow-xl overflow-hidden">
variants={fadeInUp}
initial="initial"
animate="animate"
className="bg-white rounded-3xl shadow-xl overflow-hidden"
>
<div className="h-48 bg-gradient-to-r from-amber-500 to-amber-600 relative overflow-hidden"> <div className="h-48 bg-gradient-to-r from-amber-500 to-amber-600 relative overflow-hidden">
<motion.div <motion.div
className="absolute inset-0 bg-white/10" className="absolute inset-0 bg-white/10"
@ -209,43 +177,34 @@ export default function ProfilePage() {
<div className="flex justify-center -mt-16 mb-6"> <div className="flex justify-center -mt-16 mb-6">
<div className="relative group"> <div className="relative group">
<div className="w-32 h-32 rounded-full border-4 border-white bg-gradient-to-br from-amber-500 to-amber-600 flex items-center justify-center text-white text-4xl font-bold shadow-xl overflow-hidden"> <div className="w-32 h-32 rounded-full border-4 border-white bg-gradient-to-br from-amber-500 to-amber-600 flex items-center justify-center text-white text-4xl font-bold shadow-xl overflow-hidden">
{avatarPreview ? ( {avatarPreview ? <img src={avatarPreview} alt={formData.name} className="w-full h-full object-cover" /> : formData.name?.charAt(0).toUpperCase() || "U"}
<img
src={avatarPreview}
alt={formData.name}
className="w-full h-full object-cover"
/>
) : (
formData.name?.charAt(0).toUpperCase() || "U"
)}
</div> </div>
</div> </div>
</div> </div>
<div className="text-center mb-6"> <div className="text-center mb-6">
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<h1 className="text-3xl font-bold text-gray-900"> <h1 className="text-3xl font-bold text-gray-900">{formData.name}</h1>
{formData.name}
</h1>
</div> </div>
<div className="flex items-center justify-center gap-2 text-gray-500 mt-2"> <div className="flex items-center justify-center gap-2 text-gray-500 mt-2">
<MapPin className="w-4 h-4" /> <MapPin className="w-4 h-4" />
<span>{formData.location || t('addressNotSpecified')}</span> <span>{formData.location || t("addressNotSpecified")}</span>
</div> </div>
<div className="flex items-center justify-center gap-2 text-gray-500 mt-1"> <div className="flex items-center justify-center gap-2 text-gray-500 mt-1">
<Calendar className="w-4 h-4" /> <Calendar className="w-4 h-4" />
<span> {t('member-since')} {formData.joinedDate}</span> <span>
{" "}
{t("member-since")} {formData.joinedDate}
</span>
</div> </div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-gray-50 p-4 rounded-xl group"> <div className="bg-gray-50 p-4 rounded-xl group">
<div className="flex justify-between items-start mb-2"> <div className="flex justify-between items-start mb-2">
<label className="text-sm font-medium text-gray-600"> <label className="text-sm font-medium text-gray-600">{t("emailLabel")}</label>
{t('emailLabel')}
</label>
</div> </div>
<div className="flex items-center gap-2 text-gray-900"> <div className="flex items-center gap-2 text-gray-900">
<Mail className="w-5 h-5 text-gray-400" /> <Mail className="w-5 h-5 text-gray-400" />
@ -255,42 +214,36 @@ export default function ProfilePage() {
<div className="bg-gray-50 p-4 rounded-xl group"> <div className="bg-gray-50 p-4 rounded-xl group">
<div className="flex justify-between items-start mb-2"> <div className="flex justify-between items-start mb-2">
<label className="text-sm font-medium text-gray-600"> <label className="text-sm font-medium text-gray-600">{t("phoneLabel")}</label>
{t('phoneLabel')}
</label>
</div> </div>
<div className="flex items-center gap-2 text-gray-900"> <div className="flex items-center gap-2 text-gray-900">
<Phone className="w-5 h-5 text-gray-400" /> <Phone className="w-5 h-5 text-gray-400" />
<span>{formData.phone || t('not-specified')}</span> <span>{formData.phone || t("not-specified")}</span>
</div> </div>
</div> </div>
<div className="bg-gray-50 p-4 rounded-xl group"> <div className="bg-gray-50 p-4 rounded-xl group">
<div className="flex justify-between items-start mb-2"> <div className="flex justify-between items-start mb-2">
<label className="text-sm font-medium text-gray-600"> <label className="text-sm font-medium text-gray-600">{t("whatsapp-label")}</label>
{t('whatsapp-label')}
</label>
</div> </div>
<div className="flex items-center gap-2 text-gray-900"> <div className="flex items-center gap-2 text-gray-900">
<MessageCircle className="w-5 h-5 text-gray-400" /> <MessageCircle className="w-5 h-5 text-gray-400" />
<span>{formData.whatsapp || t('not-specified')}</span> <span>{formData.whatsapp || t("not-specified")}</span>
</div> </div>
</div> </div>
<div className="bg-gray-50 p-4 rounded-xl"> <div className="bg-gray-50 p-4 rounded-xl">
<label className="block text-sm font-medium text-gray-600 mb-2"> <label className="block text-sm font-medium text-gray-600 mb-2">{t("account-type")}</label>
{t('account-type')}
</label>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{user?.role === "owner" ? ( {user?.role === "owner" ? (
<> <>
<Building className="w-5 h-5 text-amber-500" /> <Building className="w-5 h-5 text-amber-500" />
<span className="text-gray-900">{t('property-owner')}</span> <span className="text-gray-900">{t("property-owner")}</span>
</> </>
) : ( ) : (
<> <>
<Home className="w-5 h-5 text-blue-500" /> <Home className="w-5 h-5 text-blue-500" />
<span className="text-gray-900">{t('customer')}</span> <span className="text-gray-900">{t("customer")}</span>
</> </>
)} )}
</div> </div>
@ -348,17 +301,17 @@ export default function ProfilePage() {
{user?.role === "owner" && ( {user?.role === "owner" && (
<div className="grid grid-cols-3 gap-4 mt-6"> <div className="grid grid-cols-3 gap-4 mt-6">
<div className="bg-amber-50 p-4 rounded-xl text-center"> <div className=" p-4 rounded-xl text-center" style={{backgroundColor:"#0f172a"}} >
<div className="text-2xl font-bold text-amber-600">12</div> <div className="text-2xl font-bold text-amber-600">12</div>
<div className="text-xs text-gray-600">{t('propertiesListed')}</div> <div className="text-xs text-gray-600">{t("propertiesListed")}</div>
</div> </div>
<div className="bg-blue-50 p-4 rounded-xl text-center"> <div className="bg-blue-50 p-4 rounded-xl text-center"style={{backgroundColor:"#0f172a"}} >
<div className="text-2xl font-bold text-blue-600">8</div> <div className="text-2xl font-bold text-blue-600">8</div>
<div className="text-xs text-gray-600">{t('citiesCovered')}</div> <div className="text-xs text-gray-600">{t("citiesCovered")}</div>
</div> </div>
<div className="bg-green-50 p-4 rounded-xl text-center"> <div className="bg-green-50 p-4 rounded-xl text-center" style={{backgroundColor:"#0f172a"}}>
<div className="text-2xl font-bold text-green-600">4.8</div> <div className="text-2xl font-bold text-green-600">4.8</div>
<div className="text-xs text-gray-600">{t('customerSatisfaction')}</div> <div className="text-xs text-gray-600">{t("customerSatisfaction")}</div>
</div> </div>
</div> </div>
)} )}

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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>
);
}

View File

@ -1,16 +1,21 @@
import PropertyDetail from './PropertyDetail'; import PropertyDetail from "./PropertyDetail";
// Server-side API fetch for metadata (runs at request time on server) async function fetchPropertyForMeta(id, type) {
async function fetchPropertyForMeta(id) {
try { try {
const res = await fetch(`http://45.93.137.91/api/RentProperties/GetRentProperties`, { const endpoint = type === "sale" ? "http://45.93.137.91/api/SaleProperties/GetSaleProperties" : "http://45.93.137.91/api/RentProperties/GetRentProperties";
const res = await fetch(endpoint, {
next: { revalidate: 60 }, next: { revalidate: 60 },
}); });
if (!res.ok) return null; if (!res.ok) return null;
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;
} }
@ -18,27 +23,46 @@ 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 typeLabel = { 0: 'apartment', 1: 'villa', 2: 'house' }[buildingType] || 'apartment'; const type =
const address = info.address || info.Address || ''; {
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 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}`,
@ -54,49 +78,60 @@ function mapProperty(item) {
}; };
} }
export async function generateMetadata({ params }) { export async function generateMetadata({ params, searchParams }) {
const { id } = await params; const { id } = await params;
const raw = await fetchPropertyForMeta(id); const { type } = await searchParams;
const raw = await fetchPropertyForMeta(id, type);
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 propertyImage = p.image
? (p.image.startsWith('http') ? p.image : `http://45.93.137.91${p.image}`)
: '';
const logoUrl = `http://45.93.137.91/logo.png`;
// Use property image if available, otherwise logo const priceStr = `${p.price.toLocaleString()} ل.س / ${p.priceUnit === "daily" ? "يوم" : "شهر"}`;
const propertyImage = 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 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 {
title: `${p.title} - ${priceStr}`, title: `${p.title} - ${priceStr}`,
description: p.description, description: p.description,
openGraph: { openGraph: {
title: `${p.title} - ${priceStr}`, title: `${p.title} - ${priceStr}`,
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=${type}`,
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),
}, },
}; };
} }
export default function PropertyPage({ params }) { export default async function PropertyPage({ params, searchParams }) {
return <PropertyDetail params={params} />; const { type } = await searchParams;
return <PropertyDetail params={params} type={type} />;
} }

View File

@ -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>

View File

@ -1,17 +1,17 @@
'use client'; "use client";
import { motion } from 'framer-motion'; import { motion } from "framer-motion";
import { AlertTriangle, RefreshCw, Home } from 'lucide-react'; import { AlertTriangle, RefreshCw, Home } from "lucide-react";
import Link from 'next/link'; import Link from "next/link";
export default function Error({ error, reset }) { export default function Error({ error, reset }) {
return ( return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center p-4"> <div className="min-h-screen flex items-center justify-center p-4">
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="text-center max-w-md"> <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="text-center max-w-md">
<div className="w-20 h-20 bg-red-500/20 rounded-full flex items-center justify-center mx-auto mb-6"> <div className="w-20 h-20 bg-red-500/20 rounded-full flex items-center justify-center mx-auto mb-6">
<AlertTriangle className="w-10 h-10 text-red-500" /> <AlertTriangle className="w-10 h-10 text-red-500" />
</div> </div>
<h2 className="text-2xl font-bold text-white mb-2">حدث خطأ</h2> <h2 className="text-2xl font-bold text-amber-50 mb-2">حدث خطأ</h2>
<p className="text-gray-400 mb-8">نعتذر، حدث خطأ أثناء تحميل الصفحة</p> <p className="text-gray-400 mb-8">نعتذر، حدث خطأ أثناء تحميل الصفحة</p>
<div className="flex gap-3 justify-center"> <div className="flex gap-3 justify-center">
<button onClick={reset} className="flex items-center gap-2 bg-amber-500 text-white px-6 py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors"> <button onClick={reset} className="flex items-center gap-2 bg-amber-500 text-white px-6 py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors">

View File

@ -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>
);
}

View File

@ -1,566 +1,12 @@
'use client'; import Owner from "@/app/components/OwnerComponents/Owner";
export const metadata = {
import { useState, useRef, useMemo } from 'react'; title: "انشاء حساب صاحب عقار",
import { motion, AnimatePresence } from 'framer-motion'; description: "انشاء حساب والتسجيل ك صاحب عقار",
import { useRouter } from 'next/navigation'; };
import Link from 'next/link';
import Image from 'next/image';
import { useTranslation } from 'react-i18next';
import {
User, Mail, Phone, Lock, Eye, EyeOff, MessageCircle,
Camera, X, CheckCircle, XCircle, ArrowLeft, Building,
Loader2, Shield, KeyRound, FileText
} from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
import { addOwner, loginWithEmail, sendEmailOTP, verifyEmail } from '../../utils/api';
import AuthService from '../../services/AuthService';
import { OwnerType, OwnerTypeLabels } from '../../enums';
export default function OwnerRegisterPage() { export default function OwnerRegisterPage() {
const router = useRouter();
const { t } = useTranslation();
const [step, setStep] = useState(1);
const [showOtpModal, setShowOtpModal] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: '',
phone: '',
whatsapp: '',
phone2: '',
nationalNumber: '',
password: '',
confirmPassword: '',
ownerType: OwnerType.PERSON,
agreeTerms: false
});
const [idImages, setIdImages] = useState({ front: null, back: null, license: null });
const [idImagePreviews, setIdImagePreviews] = useState({ front: '', back: '', license: '' });
const [otpCode, setOtpCode] = useState('');
const [errors, setErrors] = useState({});
const fileInputFrontRef = useRef(null);
const fileInputBackRef = useRef(null);
const fileInputLicenseRef = useRef(null);
const isCompany = Number(formData.ownerType) === OwnerType.REAL_ESTATE_AGENCY;
const handleImageUpload = (side, file) => {
if (!file) return;
if (!file.type.startsWith('image/')) {
toast.error(t('register.selectValidImage'));
return;
}
if (file.size > 5 * 1024 * 1024) {
toast.error(t('register.imageSizeLimit'));
return;
}
const reader = new FileReader();
reader.onloadend = () => {
setIdImagePreviews(prev => ({ ...prev, [side]: reader.result }));
};
reader.readAsDataURL(file);
setIdImages(prev => ({ ...prev, [side]: file }));
toast.success(t('register.imageUploadSuccess'), { style: { background: '#dcfce7', color: '#166534' } });
};
const validateEmail = (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
const validatePhone = (phone) => /^(09|05)[0-9]{8}$/.test(phone);
const validateStep1 = () => {
const newErrors = {};
if (!formData.firstName) newErrors.firstName = t('register.firstNameRequired');
if (!formData.lastName) newErrors.lastName = t('register.lastNameRequired');
if (!formData.email) newErrors.email = t('register.emailRequired');
else if (!validateEmail(formData.email)) newErrors.email = t('register.emailInvalid');
if (!formData.phone) newErrors.phone = t('register.phoneRequired');
else if (!validatePhone(formData.phone)) newErrors.phone = t('register.phoneInvalid') + ` (${t('register.phoneMustStartWith')} 09/05)`;
if (!formData.whatsapp) newErrors.whatsapp = t('register.whatsappRequired');
else if (!validatePhone(formData.whatsapp)) newErrors.whatsapp = t('register.phoneInvalid') + ` (${t('register.phoneMustStartWith')} 09/05)`;
if (!formData.password) newErrors.password = t('register.passwordRequired');
else if (formData.password.length < 6) newErrors.password = t('validation.passwordMinLength');
if (formData.password !== formData.confirmPassword) newErrors.confirmPassword = t('validation.passwordsMatch');
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const validateStep2 = () => {
const newErrors = {};
if (!idImages.front) newErrors.front = t('register.frontIdRequired');
if (!idImages.back) newErrors.back = t('register.backIdRequired');
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleNextStep = () => {
if (validateStep1()) {
setStep(2);
window.scrollTo({ top: 0, behavior: 'smooth' });
} else {
toast.error(t('validation.correctErrors'));
}
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateStep2()) {
toast.error(t('register.completeRequiredImages'));
return;
}
if (!formData.agreeTerms) {
toast.error(t('validation.agreeToTerms'));
return;
}
setIsLoading(true);
const payload = {
firstName: formData.firstName,
lastName: formData.lastName,
email: formData.email,
phoneNumber: formData.phone || '',
whatsAppNumber: formData.whatsapp,
phone: formData.phone2,
nationalNumber: formData.nationalNumber,
password: formData.password,
ownerType: formData.ownerType,
};
try {
const licenseImage = isCompany ? idImages.license : null;
const res = await addOwner(payload, idImages.front, idImages.back, licenseImage);
if (res.status === 200 || res.ok) {
const tempToken = res.data;
if (tempToken) AuthService.addToken(tempToken);
toast.success(res.message || t('register.accountCreated'), { duration: 4000 });
const loginRes = await loginWithEmail(formData.email, formData.password);
if (loginRes.status === 206) {
const otpToken = loginRes.data;
if (otpToken) AuthService.addToken(otpToken);
toast(loginRes.message || t('register.otpSentEmail'), { icon: '📧' });
setShowOtpModal(true);
} else if (loginRes.status === 200) {
const loginToken = loginRes.data;
if (loginToken) AuthService.addToken(loginToken);
toast.success(loginRes.message || t('register.loginSuccess'));
router.push('/');
}
} else {
toast.error(res.message || res.data?.message || t('register.accountCreationFailed'));
}
} catch (err) {
toast.error(err.message || t('register.registrationError'));
} finally {
setIsLoading(false);
}
};
const handleVerifyOTP = async () => {
if (!otpCode || otpCode.length < 4) {
toast.error(t('register.otpRequired'));
return;
}
setIsLoading(true);
try {
const res = await verifyEmail(otpCode);
if (res.status === 200) {
AuthService.deleteToken();
toast.success(res.message || t('register.emailVerified'));
setShowOtpModal(false);
setTimeout(() => router.push('/login'), 1500);
} else {
toast.error(res.message || res.data?.message || t('accountVerification.otpInvalid'));
}
} catch (err) {
toast.error(err.message || t('register.verificationError'));
} finally {
setIsLoading(false);
}
};
const handleResendOTP = async () => {
setIsLoading(true);
try {
await sendEmailOTP();
toast.success(t('register.newOtpSent'));
} catch (err) {
toast.error(t('register.resendOtpFailed'));
} finally {
setIsLoading(false);
}
};
const fadeInUp = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.5 }
};
const staggerContainer = {
animate: { transition: { staggerChildren: 0.1 } }
};
const backgroundElements = useMemo(() => {
const circles = [
{ style: { top: '20%', right: '20%', width: '256px', height: '256px' }, className: 'bg-amber-500/5' },
{ style: { bottom: '20%', left: '20%', width: '320px', height: '320px' }, className: 'bg-blue-500/5' },
{ style: { top: '50%', left: '50%', width: '384px', height: '384px', transform: 'translate(-50%, -50%)' }, className: 'bg-purple-500/5' },
];
const dots = Array.from({ length: 10 }).map((_, i) => ({
left: `${5 + i * 10}%`,
top: `${10 + (i * 7) % 80}%`,
size: `${80 + (i % 5) * 15}px`
}));
return ( return (
<> <>
{circles.map((circle, i) => ( <Owner />
<div key={`circle-${i}`} className={`absolute rounded-full ${circle.className}`} style={circle.style} />
))}
{dots.map((dot, i) => (
<div key={`dot-${i}`} className="absolute rounded-full bg-amber-500/10" style={{ left: dot.left, top: dot.top, width: dot.size, height: dot.size }} />
))}
</> </>
); );
}, []);
return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center p-4 relative overflow-hidden">
<Toaster position="top-center" reverseOrder={false} />
<div className="absolute inset-0 overflow-hidden">{backgroundElements}</div>
<motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.5 }}
className="relative z-10 w-full max-w-2xl">
<div className="mb-8">
<div className="flex items-center justify-between mb-4">
<Link href="/auth/choose-role" className="flex items-center gap-2 text-gray-400 hover:text-white transition-colors group">
<motion.div whileHover={{ x: -5 }}><ArrowLeft className="w-4 h-4" /></motion.div>
<span>{t('forgotPassword.backToLogin')}</span>
</Link>
<span className="text-sm text-gray-400">{t('register.stepOf2', { step: step })}</span>
</div>
<div className="flex gap-2">
{[1, 2].map((s) => (
<motion.div key={s} className={`h-2 flex-1 rounded-full ${step >= s ? 'bg-amber-500' : 'bg-gray-700'}`} animate={{ scaleX: step >= s ? 1 : 0.5 }} />
))}
</div>
</div>
<motion.div key={step} initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} transition={{ duration: 0.3 }}
className="bg-white/5 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/10 overflow-hidden">
<div className="bg-gradient-to-r from-amber-500 to-amber-600 p-8 text-center relative overflow-hidden">
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ delay: 0.2, type: "spring" }}
className="absolute -top-10 -right-10 w-40 h-40 bg-white/10 rounded-full" />
<motion.div initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} className="relative z-10">
<motion.div animate={{ rotate: [0, 10, -10, 0] }} transition={{ duration: 2, repeat: Infinity }}
className="w-20 h-20 mx-auto mb-4 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-sm">
<Building className="w-10 h-10 text-white" />
</motion.div>
<h1 className="text-3xl font-bold text-white mb-2">{step === 1 ? t('register.owner.step1Title') : t('register.owner.step1bTitle')}</h1>
<p className="text-amber-100">{step === 1 ? t('register.owner.step1Desc') : t('register.owner.step1bDesc')}</p>
</motion.div>
</div>
<div className="p-8">
<motion.form variants={staggerContainer} initial="initial" animate="animate"
onSubmit={step === 1 ? (e) => { e.preventDefault(); handleNextStep(); } : handleSubmit}
className="space-y-6">
{step === 1 && (
<>
<motion.div variants={fadeInUp} className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.firstName')} <span className="text-red-500">*</span></label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<User className={`w-5 h-5 ${errors.firstName ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
</div>
<input type="text" value={formData.firstName}
onChange={(e) => { setFormData({...formData, firstName: e.target.value}); setErrors({...errors, firstName: null}); }}
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.firstName ? 'border-red-500' : 'border-gray-700'}`}
placeholder={t('register.firstName')} />
</div>
{errors.firstName && <p className="text-red-500 text-sm mt-1">{errors.firstName}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.lastName')} <span className="text-red-500">*</span></label>
<input type="text" value={formData.lastName}
onChange={(e) => { setFormData({...formData, lastName: e.target.value}); setErrors({...errors, lastName: null}); }}
className={`w-full px-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.lastName ? 'border-red-500' : 'border-gray-700'}`}
placeholder={t('register.lastName')} />
{errors.lastName && <p className="text-red-500 text-sm mt-1">{errors.lastName}</p>}
</div>
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.email')} <span className="text-red-500">*</span></label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Mail className={`w-5 h-5 ${errors.email ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
</div>
<input type="email" value={formData.email}
onChange={(e) => { setFormData({...formData, email: e.target.value}); setErrors({...errors, email: null}); }}
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.email ? 'border-red-500' : 'border-gray-700'}`}
placeholder={t('register.emailPlaceholder')} />
</div>
{errors.email && <p className="text-red-500 text-sm mt-1">{errors.email}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.phone')} <span className="text-gray-500">({t('register.optional')})</span></label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Phone className="w-5 h-5 text-gray-400 group-focus-within:text-amber-500" />
</div>
<input type="tel" value={formData.phone}
onChange={(e) => { setFormData({...formData, phone: e.target.value}); setErrors({...errors, phone: null}); }}
className="w-full pr-12 pl-4 py-3 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all"
placeholder={t('register.phoneOptionalPlaceholder')} />
</div>
{errors.phone && <p className="text-red-500 text-sm mt-1">{errors.phone}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.whatsapp')} <span className="text-red-500">*</span></label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<MessageCircle className={`w-5 h-5 ${errors.whatsapp ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
</div>
<input type="tel" value={formData.whatsapp}
onChange={(e) => { setFormData({...formData, whatsapp: e.target.value}); setErrors({...errors, whatsapp: null}); }}
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.whatsapp ? 'border-red-500' : 'border-gray-700'}`}
placeholder={t('register.whatsappPlaceholder')} />
</div>
{errors.whatsapp && <p className="text-red-500 text-sm mt-1">{errors.whatsapp}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.phoneSecondary')} <span className="text-red-500">*</span></label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Phone className={`w-5 h-5 ${errors.phone2 ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
</div>
<input type="tel" value={formData.phone2}
onChange={(e) => { setFormData({...formData, phone2: e.target.value}); setErrors({...errors, phone2: null}); }}
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.phone2 ? 'border-red-500' : 'border-gray-700'}`}
placeholder={t('register.phoneSecondaryPlaceholder')} maxLength={7} />
</div>
{errors.phone2 && <p className="text-red-500 text-sm mt-1">{errors.phone2}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.nationalNumber')} <span className="text-red-500">*</span></label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<User className={`w-5 h-5 ${errors.nationalNumber ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
</div>
<input type="text" value={formData.nationalNumber}
onChange={(e) => { setFormData({...formData, nationalNumber: e.target.value}); setErrors({...errors, nationalNumber: null}); }}
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.nationalNumber ? 'border-red-500' : 'border-gray-700'}`}
placeholder={t('register.nationalNumberPlaceholder')} />
</div>
{errors.nationalNumber && <p className="text-red-500 text-sm mt-1">{errors.nationalNumber}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.ownerType')} <span className="text-red-500">*</span></label>
<select value={formData.ownerType.toString()}
onChange={(e) => {
const selectedType = parseInt(e.target.value, 10);
setFormData((prev) => ({ ...prev, ownerType: selectedType }));
}}
className="w-full py-3 px-4 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white appearance-none cursor-pointer">
{Object.entries(OwnerTypeLabels).map(([value, label]) => (
<option key={value} value={value} className="bg-gray-900 text-white">{label}</option>
))}
</select>
</motion.div>
<AnimatePresence>
{isCompany && (
<motion.div
key="license-upload-step1"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.owner.licenseImageLabel')} <span className="text-gray-400">({t('register.optional')})</span></label>
<div className="mb-2 text-xs text-gray-400">{t('register.owner.licenseHint')}</div>
<div onClick={() => fileInputLicenseRef.current?.click()}
className={`relative border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all ${idImagePreviews.license ? 'border-green-500 bg-green-500/10' : 'border-gray-700 hover:border-amber-500 hover:bg-white/5'}`}>
<input ref={fileInputLicenseRef} type="file" accept="image/*" onChange={(e) => handleImageUpload('license', e.target.files?.[0])} className="hidden" />
{idImagePreviews.license ? (
<div className="relative">
<Image src={idImagePreviews.license} alt={t('register.uploadLicenseAlt')} width={200} height={120} className="mx-auto rounded-lg object-cover" />
<button onClick={(e) => { e.stopPropagation(); setIdImages(prev => ({...prev, license: null})); setIdImagePreviews(prev => ({...prev, license: ''})); }}
className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 rounded-full flex items-center justify-center hover:bg-red-600">
<X className="w-4 h-4 text-white" />
</button>
</div>
) : (<><Camera className="w-12 h-12 text-gray-500 mx-auto mb-3" /><p className="text-gray-400">{t('register.owner.clickUploadLicense')}</p><p className="text-xs text-gray-500 mt-2">JPEG, PNG, JPG {t('register.upTo5MB')}</p></>)}
</div>
</motion.div>
)}
</AnimatePresence>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.password')} <span className="text-red-500">*</span></label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className={`w-5 h-5 ${errors.password ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
</div>
<input type={showPassword ? "text" : "password"} value={formData.password}
onChange={(e) => { setFormData({...formData, password: e.target.value}); setErrors({...errors, password: null}); }}
className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.password ? 'border-red-500' : 'border-gray-700'}`}
placeholder={t('register.passwordPlaceholder')} />
<button type="button" onClick={() => setShowPassword(!showPassword)} className="absolute inset-y-0 left-0 pl-3 flex items-center">
{showPassword ? <EyeOff className="w-5 h-5 text-gray-400" /> : <Eye className="w-5 h-5 text-gray-400" />}
</button>
</div>
{errors.password && <p className="text-red-500 text-sm mt-1">{errors.password}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.confirmPassword')} <span className="text-red-500">*</span></label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className={`w-5 h-5 ${errors.confirmPassword ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
</div>
<input type={showConfirmPassword ? "text" : "password"} value={formData.confirmPassword}
onChange={(e) => { setFormData({...formData, confirmPassword: e.target.value}); setErrors({...errors, confirmPassword: null}); }}
className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.confirmPassword ? 'border-red-500' : 'border-gray-700'}`}
placeholder={t('register.confirmPasswordPlaceholder')} />
<button type="button" onClick={() => setShowConfirmPassword(!showConfirmPassword)} className="absolute inset-y-0 left-0 pl-3 flex items-center">
{showConfirmPassword ? <EyeOff className="w-5 h-5 text-gray-400" /> : <Eye className="w-5 h-5 text-gray-400" />}
</button>
{formData.confirmPassword && (
<div className="absolute inset-y-0 left-12 flex items-center">
{formData.password === formData.confirmPassword ? <CheckCircle className="w-5 h-5 text-green-500" /> : <XCircle className="w-5 h-5 text-red-500" />}
</div>
)}
</div>
{errors.confirmPassword && <p className="text-red-500 text-sm mt-1">{errors.confirmPassword}</p>}
</motion.div>
</>
)}
{step === 2 && (
<>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.owner.frontIdLabel')} <span className="text-red-500">*</span></label>
<div onClick={() => fileInputFrontRef.current?.click()}
className={`relative border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all ${idImagePreviews.front ? 'border-green-500 bg-green-500/10' : errors.front ? 'border-red-500 bg-red-500/10' : 'border-gray-700 hover:border-amber-500 hover:bg-white/5'}`}>
<input ref={fileInputFrontRef} type="file" accept="image/*" onChange={(e) => handleImageUpload('front', e.target.files?.[0])} className="hidden" />
{idImagePreviews.front ? (
<div className="relative">
<Image src={idImagePreviews.front} alt={t('register.uploadFrontAlt')} width={200} height={120} className="mx-auto rounded-lg object-cover" />
<button onClick={(e) => { e.stopPropagation(); setIdImages(prev => ({...prev, front: null})); setIdImagePreviews(prev => ({...prev, front: ''})); }}
className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 rounded-full flex items-center justify-center hover:bg-red-600">
<X className="w-4 h-4 text-white" />
</button>
</div>
) : (<><Camera className="w-12 h-12 text-gray-500 mx-auto mb-3" /><p className="text-gray-400">{t('register.owner.clickUploadImage')}</p><p className="text-xs text-gray-500 mt-2">JPEG, PNG, JPG {t('register.upTo5MB')}</p></>)}
</div>
{errors.front && <p className="text-red-500 text-sm mt-1">{errors.front}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.owner.backIdLabel')} <span className="text-red-500">*</span></label>
<div onClick={() => fileInputBackRef.current?.click()}
className={`relative border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all ${idImagePreviews.back ? 'border-green-500 bg-green-500/10' : errors.back ? 'border-red-500 bg-red-500/10' : 'border-gray-700 hover:border-amber-500 hover:bg-white/5'}`}>
<input ref={fileInputBackRef} type="file" accept="image/*" onChange={(e) => handleImageUpload('back', e.target.files?.[0])} className="hidden" />
{idImagePreviews.back ? (
<div className="relative">
<Image src={idImagePreviews.back} alt={t('register.uploadBackAlt')} width={200} height={120} className="mx-auto rounded-lg object-cover" />
<button onClick={(e) => { e.stopPropagation(); setIdImages(prev => ({...prev, back: null})); setIdImagePreviews(prev => ({...prev, back: ''})); }}
className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 rounded-full flex items-center justify-center hover:bg-red-600">
<X className="w-4 h-4 text-white" />
</button>
</div>
) : (<><Camera className="w-12 h-12 text-gray-500 mx-auto mb-3" /><p className="text-gray-400">{t('register.owner.clickUploadImage')}</p><p className="text-xs text-gray-500 mt-2">JPEG, PNG, JPG {t('register.upTo5MB')}</p></>)}
</div>
{errors.back && <p className="text-red-500 text-sm mt-1">{errors.back}</p>}
</motion.div>
<motion.div variants={fadeInUp} className="flex items-center gap-2">
<input type="checkbox" id="terms" checked={formData.agreeTerms}
onChange={(e) => setFormData({...formData, agreeTerms: e.target.checked})}
className="w-4 h-4 rounded border-gray-600 bg-white/5 text-amber-500 focus:ring-amber-500" required />
<label htmlFor="terms" className="text-sm text-gray-300">
{t('register.agreeTerms')} <Link href="/terms" className="text-amber-400 hover:text-amber-300">{t('register.termsOfUse')}</Link> {t('register.and')} <Link href="/privacy" className="text-amber-400 hover:text-amber-300">{t('register.privacyPolicy')}</Link>
</label>
</motion.div>
</>
)}
<motion.div variants={fadeInUp} className="flex gap-3 pt-4">
{step === 1 ? (
<>
<button type="button" onClick={() => router.push('/auth/choose-role')}
className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors">{t('register.cancel')}</button>
<button type="submit"
className="flex-1 bg-gradient-to-r from-amber-500 to-amber-600 text-white py-3 px-4 rounded-xl font-medium hover:from-amber-600 hover:to-amber-700 transition-all">{t('register.next')}</button>
</>
) : (
<>
<button type="button" onClick={() => setStep(1)}
className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors">{t('register.previous')}</button>
<button type="submit" disabled={isLoading || !formData.agreeTerms}
className="flex-1 bg-gradient-to-r from-amber-500 to-amber-600 text-white py-3 px-4 rounded-xl font-medium hover:from-amber-600 hover:to-amber-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed">
{isLoading ? (<div className="flex items-center justify-center gap-2"><Loader2 className="w-5 h-5 animate-spin" /><span>{t('register.registering')}</span></div>) : t('register.createAccount')}
</button>
</>
)}
</motion.div>
</motion.form>
</div>
</motion.div>
</motion.div>
<AnimatePresence>
{showOtpModal && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 z-50">
<motion.div initial={{ scale: 0.9, y: 20 }} animate={{ scale: 1, y: 0 }} exit={{ scale: 0.9, y: 20 }}
className="bg-gray-900 border border-white/10 rounded-2xl w-full max-w-md p-6 shadow-2xl">
<div className="text-center mb-6">
<div className="w-16 h-16 bg-amber-500/20 rounded-full flex items-center justify-center mx-auto mb-3">
<Shield className="w-8 h-8 text-amber-500" />
</div>
<h2 className="text-xl font-bold text-white">{t('register.otpTitle')}</h2>
<p className="text-gray-400 text-sm mt-1">{t('register.otpSentTo')}</p>
<p className="text-amber-400 font-medium text-sm">{formData.email}</p>
</div>
<div className="mb-6">
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.otpLabel')}</label>
<div className="relative">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<KeyRound className="w-5 h-5 text-gray-400" />
</div>
<input type="text" value={otpCode} maxLength={6}
onChange={(e) => setOtpCode(e.target.value)}
className="w-full pr-12 pl-4 py-3 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 text-white text-center tracking-[0.5em] text-xl"
placeholder="------" />
</div>
</div>
<div className="flex gap-3">
<button onClick={handleVerifyOTP} disabled={isLoading || !otpCode}
className="flex-1 bg-gradient-to-r from-amber-500 to-amber-600 text-white py-3 rounded-xl font-medium hover:from-amber-600 hover:to-amber-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2">
{isLoading ? <><Loader2 className="w-5 h-5 animate-spin" /><span>{t('register.verifying')}</span></> : t('register.verify')}
</button>
</div>
<button onClick={handleResendOTP} disabled={isLoading}
className="w-full text-center text-amber-400 hover:text-amber-300 text-sm mt-3 disabled:opacity-50">
{t('register.resendOtp')}
</button>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
);
} }

View File

@ -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>
);
}

View File

@ -1,419 +1,133 @@
"use client";
'use client'; import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { useState, useEffect, useMemo } from 'react'; import { useTranslation } from "react-i18next";
import { motion, AnimatePresence } from 'framer-motion'; import toast, { Toaster } from "react-hot-toast";
import { useRouter } from 'next/navigation'; import { addCustomer, loginWithEmail } from "../../utils/api";
import Link from 'next/link'; import AuthService from "../../services/AuthService";
import { useTranslation } from 'react-i18next'; import { CustomerType } from "../../enums";
import { import { fadeInUp } from "../../components/Animation/OwnerPageAnimation";
User, Mail, Phone, Lock, Eye, EyeOff, import { staggerContainer } from "../../components/Animation/OwnerPageAnimation";
CheckCircle, XCircle, ArrowLeft, Home, Loader2, import InteractiveBackground from "@/app/components/Animation/Background";
Shield, KeyRound import HeaderOfSteps from "@/app/components/OwnerComponents/HeaderOfSteps";
} from 'lucide-react'; import HeaderOfForm from "@/app/components/OwnerComponents/HeaderOfForm";
import toast, { Toaster } from 'react-hot-toast'; import OwnerFormStepOne from "@/app/components/OwnerComponents/OwnerFormStepOne";
import { addCustomer, loginWithEmail, sendEmailOTP, verifyEmail } from '../../utils/api'; import Policy from "@/app/components/OwnerComponents/PolicyAgree";
import AuthService from '../../services/AuthService'; import ButtomStepOne from "@/app/components/OwnerComponents/ButtomStepOne";
import { CustomerType, CustomerTypeLabels } from '../../enums'; import ButtomStepTwo from "@/app/components/OwnerComponents/ButtomStepTwo";
import HavAccount from "@/app/components/HavAccount";
import OtpDialog from "@/app/components/OwnerComponents/OtpDialog";
import { validateStep1 } from "@/app/validations/OwnerValidatoin";
export default function TenantRegisterPage() { export default function TenantRegisterPage() {
const router = useRouter();
const { t } = useTranslation(); const { t } = useTranslation();
const [step, setStep] = useState(1); const [step, setStep] = useState(1);
const [showOtpModal, setShowOtpModal] = useState(false); const [showOtpModal, setShowOtpModal] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
firstName: '', firstName: "",
lastName: '', lastName: "",
email: '', email: "",
phone: '', phone: "",
whatsapp: '', whatsapp: "",
phone2: '', phone2: "",
nationalNumber: '', nationalNumber: "",
password: '', password: "",
confirmPassword: '', confirmPassword: "",
customerType: CustomerType.PERSONAL, customerType: CustomerType.PERSONAL,
agreeTerms: false agreeTerms: false,
}); });
const [otpCode, setOtpCode] = useState('');
const [errors, setErrors] = useState({}); const [errors, setErrors] = useState({});
const validateEmail = (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
const validatePhone = (phone) => /^(09|05)[0-9]{8}$/.test(phone);
const validateStep1 = () => {
const newErrors = {};
if (!formData.firstName) newErrors.firstName = t('register.firstNameRequired');
if (!formData.lastName) newErrors.lastName = t('register.lastNameRequired');
if (!formData.email) newErrors.email = t('register.emailRequired');
else if (!validateEmail(formData.email)) newErrors.email = t('register.emailInvalid');
if (!formData.phone) newErrors.phone = t('register.phoneRequired');
else if (!validatePhone(formData.phone)) newErrors.phone = t('register.phoneInvalid') + ` (${t('register.phoneMustStartWith')} 09/05)`;
if (!formData.password) newErrors.password = t('register.passwordRequired');
else if (formData.password.length < 6) newErrors.password = t('validation.passwordMinLength');
if (!formData.whatsapp) newErrors.whatsapp = t('register.whatsappRequired');
if (!formData.phone2 || formData.phone2.length !== 7) newErrors.phone2 = t('validation.phone7Digits');
if (!formData.nationalNumber) newErrors.nationalNumber = t('register.nationalNumberRequired');
if (formData.password !== formData.confirmPassword) newErrors.confirmPassword = t('validation.passwordsMatch');
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleNextStep = () => { const handleNextStep = () => {
if (validateStep1()) { if (validateStep1(formData, setErrors, t)) {
setStep(2); setStep(2);
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: "smooth" });
} else { } else {
toast.error(t('validation.correctErrors')); toast.error(t("validation.correctErrors"));
} }
}; };
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
if (!formData.agreeTerms) { if (!formData.agreeTerms) {
toast.error(t('validation.agreeToTerms')); toast.error(t("validation.agreeToTerms"));
return; return;
} }
setIsLoading(true); setIsLoading(true);
const payload = { const payload = {
firstName: formData.firstName, firstName: formData.firstName,
lastName: formData.lastName, lastName: formData.lastName,
email: formData.email, email: formData.email,
phoneNumber: formData.phone, phoneNumber: formData.phone,
whatsAppNumber: formData.whatsapp, whatsAppNumber: formData.whatsapp,
phone: formData.phone2, phone: formData.phone2 || "" ,
nationalNumber: formData.nationalNumber, nationalNumber: formData.nationalNumber,
password: formData.password, password: formData.password,
customerType: formData.customerType, customerType: formData.customerType,
}; };
try { try {
const res = await addCustomer(payload, null, null); const res = await addCustomer(payload, null, null);
if (res.status === 200 || res.ok) { if (res.status === 200 || res.ok) {
const tempToken = res.data; const apiMessage = res.message || res.data?.message || res.data[0]?.message;
if (tempToken) { toast.success(apiMessage || t("register.accountCreated"), { duration: 4000 });
AuthService.addToken(tempToken);
}
const apiMessage = res.message || res.data?.message;
toast.success(apiMessage || t('register.accountCreated'), { duration: 4000 });
const loginRes = await loginWithEmail(formData.email, formData.password); const loginRes = await loginWithEmail(formData.email, formData.password);
if (loginRes.status === 206) { if (loginRes.status === 206) {
const otpToken = loginRes.data; const otpToken = loginRes.data;
if (otpToken) AuthService.addToken(otpToken); if (otpToken) AuthService.addToken(otpToken);
toast(loginRes.message || t('register.otpSentEmail'), { icon: '📧' }); toast(loginRes.message || t("register.otpSentEmail"), { icon: "📧" });
setShowOtpModal(true); setShowOtpModal(true);
} else if (loginRes.status === 200) { } else if (loginRes.status === 200) {
const loginToken = loginRes.data; const loginToken = loginRes.data;
if (loginToken) AuthService.addToken(loginToken); if (loginToken) AuthService.addToken(loginToken);
toast.success(loginRes.message || t('register.loginSuccess')); toast.success(loginRes.message || t("register.loginSuccess"));
router.push('/');
} }
} else { } else {
const errMsg = res.message || res.data?.message || t('register.accountCreationFailed'); const errMsg = res.message || res.data?.message || res.data[0]?.message || t("register.accountCreationFailed");
toast.error(errMsg); toast.error(errMsg);
} }
} catch (err) { } catch (err) {
toast.error(err.message || t('register.registrationError')); toast.error(err.message || t("register.registrationError"));
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}; };
const handleVerifyOTP = async () => {
if (!otpCode || otpCode.length < 4) {
toast.error(t('register.otpRequired'));
return;
}
setIsLoading(true);
try {
const res = await verifyEmail(otpCode);
if (res.status === 200) {
AuthService.deleteToken();
toast.success(res.message || t('register.emailVerified'));
setShowOtpModal(false);
setTimeout(() => router.push('/login'), 1500);
} else {
toast.error(res.message || res.data?.message || t('accountVerification.otpInvalid'));
}
} catch (err) {
toast.error(err.message || t('register.verificationError'));
} finally {
setIsLoading(false);
}
};
const handleResendOTP = async () => {
setIsLoading(true);
try {
await sendEmailOTP();
toast.success(t('register.newOtpSent'));
} catch (err) {
toast.error(t('register.resendOtpFailed'));
} finally {
setIsLoading(false);
}
};
const fadeInUp = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.5 }
};
const staggerContainer = {
animate: { transition: { staggerChildren: 0.1 } }
};
const backgroundElements = useMemo(() => {
const circles = [
{ style: { top: '20%', right: '20%', width: '256px', height: '256px' }, className: 'bg-blue-500/10' },
{ style: { bottom: '20%', left: '20%', width: '320px', height: '320px' }, className: 'bg-blue-500/10' },
{ style: { top: '50%', left: '50%', width: '384px', height: '384px', transform: 'translate(-50%, -50%)' }, className: 'bg-blue-500/10' },
];
const dots = [
{ left: '5%', top: '10%', size: '120px' },
{ left: '15%', top: '70%', size: '80px' },
{ left: '25%', top: '30%', size: '150px' },
{ left: '35%', top: '85%', size: '100px' },
{ left: '45%', top: '15%', size: '90px' },
{ left: '55%', top: '60%', size: '130px' },
{ left: '65%', top: '40%', size: '70px' },
{ left: '75%', top: '80%', size: '110px' },
{ left: '85%', top: '20%', size: '140px' },
{ left: '95%', top: '50%', size: '85px' },
];
return ( return (
<> <div className="min-h-screen flex items-center justify-center p-4 relative overflow-hidden">
{circles.map((circle, i) => (
<div key={`circle-${i}`} className={`absolute rounded-full ${circle.className}`} style={circle.style} />
))}
{dots.map((dot, i) => (
<div key={`dot-${i}`} className="absolute rounded-full bg-blue-500/10" style={{ left: dot.left, top: dot.top, width: dot.size, height: dot.size }} />
))}
</>
);
}, []);
return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center p-4 relative overflow-hidden">
<Toaster position="top-center" reverseOrder={false} /> <Toaster position="top-center" reverseOrder={false} />
<div className="absolute inset-0 overflow-hidden">{backgroundElements}</div> <div className="absolute inset-0 overflow-hidden">
<InteractiveBackground />{" "}
</div>
<motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.5 }} className="relative z-10 w-full max-w-md"> <motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.5 }} className="relative z-10 w-full max-w-md">
<motion.div initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} className="mb-8"> <HeaderOfSteps step={step} colors={"bg-blue-500"} numberStep={[1, 2]} />
<Link href="/auth/choose-role" className="flex items-center gap-2 text-gray-400 hover:text-white transition-colors group">
<motion.div whileHover={{ x: -5 }}><ArrowLeft className="w-4 h-4" /></motion.div>
<span>{t('forgotPassword.backToLogin')}</span>
</Link>
</motion.div>
<div className="mb-6 flex gap-2">
{[1, 2].map((s) => (
<motion.div key={s} className={`h-2 flex-1 rounded-full ${step >= s ? 'bg-blue-500' : 'bg-gray-700'}`} animate={{ scaleX: step >= s ? 1 : 0.5 }} />
))}
</div>
<div className="bg-white/5 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/10 overflow-hidden"> <div className="bg-white/5 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/10 overflow-hidden">
<div className="bg-gradient-to-r from-blue-500 to-blue-600 p-8 text-center relative overflow-hidden"> <HeaderOfForm num={1} step={step} />
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ delay: 0.2, type: "spring" }} className="absolute -top-10 -right-10 w-40 h-40 bg-white/10 rounded-full" />
<motion.div initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} className="relative z-10">
<motion.div animate={{ rotate: [0, 10, -10, 0] }} transition={{ duration: 2, repeat: Infinity }} className="w-20 h-20 mx-auto mb-4 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-sm">
<Home className="w-10 h-10 text-white" />
</motion.div>
<h1 className="text-3xl font-bold text-white mb-2">{step === 1 ? t('register.tenant.step1Title') : t('register.tenant.step2Title')}</h1>
<p className="text-blue-100">{step === 1 ? t('register.tenant.step1Desc') : t('register.tenant.step2Desc')}</p>
</motion.div>
</div>
<div className="p-8"> <div className="p-8">
<motion.form variants={staggerContainer} initial="initial" animate="animate" onSubmit={step === 1 ? (e) => { e.preventDefault(); handleNextStep(); } : handleSubmit} className="space-y-6"> <motion.form
{step === 1 && ( variants={staggerContainer}
<> initial="initial"
<motion.div variants={fadeInUp} className="grid grid-cols-2 gap-3"> animate="animate"
<div> onSubmit={
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.firstName')} <span className="text-red-500">*</span></label> step === 1
<div className="relative group"> ? (e) => {
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"> e.preventDefault();
<User className={`w-5 h-5 ${errors.firstName ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'}`} /> handleNextStep();
</div> }
<input type="text" value={formData.firstName} onChange={(e) => { setFormData({...formData, firstName: e.target.value}); setErrors({...errors, firstName: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.firstName ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.firstName')} /> : handleSubmit
</div> }
{errors.firstName && <p className="text-red-500 text-sm mt-1">{errors.firstName}</p>} className="space-y-6"
</div> >
<div> {step === 1 && <OwnerFormStepOne setErrors={setErrors} setFormData={setFormData} formData={formData} errors={errors} type={"customer"} />}
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.lastName')} <span className="text-red-500">*</span></label> {step === 2 && <Policy formData={formData} setFormData={setFormData} type={"customer"} />}
<input type="text" value={formData.lastName} onChange={(e) => { setFormData({...formData, lastName: e.target.value}); setErrors({...errors, lastName: null}); }} className={`w-full px-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.lastName ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.lastName')} />
{errors.lastName && <p className="text-red-500 text-sm mt-1">{errors.lastName}</p>}
</div>
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.email')} <span className="text-red-500">*</span></label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Mail className={`w-5 h-5 ${errors.email ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'}`} />
</div>
<input type="email" value={formData.email} onChange={(e) => { setFormData({...formData, email: e.target.value}); setErrors({...errors, email: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.email ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.emailPlaceholder')} />
</div>
{errors.email && <p className="text-red-500 text-sm mt-1">{errors.email}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.phone')} <span className="text-red-500">*</span></label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Phone className={`w-5 h-5 ${errors.phone ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'}`} />
</div>
<input type="tel" value={formData.phone} onChange={(e) => { setFormData({...formData, phone: e.target.value}); setErrors({...errors, phone: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.phone ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.phonePlaceholder')} />
</div>
{errors.phone && <p className="text-red-500 text-sm mt-1">{errors.phone}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.whatsapp')} <span className="text-red-500">*</span></label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Phone className={`w-5 h-5 ${errors.whatsapp ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'}`} />
</div>
<input type="tel" value={formData.whatsapp} onChange={(e) => { setFormData({...formData, whatsapp: e.target.value}); setErrors({...errors, whatsapp: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.whatsapp ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.whatsappPlaceholder')} />
</div>
{errors.whatsapp && <p className="text-red-500 text-sm mt-1">{errors.whatsapp}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.phoneSecondary')} <span className="text-red-500">*</span></label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Phone className={`w-5 h-5 ${errors.phone2 ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'}`} />
</div>
<input type="tel" value={formData.phone2} onChange={(e) => { setFormData({...formData, phone2: e.target.value}); setErrors({...errors, phone2: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.phone2 ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.phoneSecondaryPlaceholder')} maxLength={7} />
</div>
{errors.phone2 && <p className="text-red-500 text-sm mt-1">{errors.phone2}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.nationalNumber')} <span className="text-red-500">*</span></label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<User className={`w-5 h-5 ${errors.nationalNumber ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'}`} />
</div>
<input type="text" value={formData.nationalNumber} onChange={(e) => { setFormData({...formData, nationalNumber: e.target.value}); setErrors({...errors, nationalNumber: null}); }} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.nationalNumber ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.nationalNumberPlaceholder')} />
</div>
{errors.nationalNumber && <p className="text-red-500 text-sm mt-1">{errors.nationalNumber}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.customerType')} <span className="text-red-500">*</span></label>
<select value={formData.customerType} onChange={(e) => setFormData({...formData, customerType: e.target.value})} className="w-full py-3 px-4 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white appearance-none cursor-pointer">
{Object.entries(CustomerTypeLabels).map(([value, label]) => (<option key={value} value={value} className="bg-gray-900 text-white">{label}</option>))}
</select>
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.password')} <span className="text-red-500">*</span></label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className={`w-5 h-5 ${errors.password ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'}`} />
</div>
<input type={showPassword ? "text" : "password"} value={formData.password} onChange={(e) => { setFormData({...formData, password: e.target.value}); setErrors({...errors, password: null}); }} className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.password ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.passwordPlaceholder')} />
<button type="button" onClick={() => setShowPassword(!showPassword)} className="absolute inset-y-0 left-0 pl-3 flex items-center">
{showPassword ? <EyeOff className="w-5 h-5 text-gray-400" /> : <Eye className="w-5 h-5 text-gray-400" />}
</button>
</div>
{errors.password && <p className="text-red-500 text-sm mt-1">{errors.password}</p>}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.confirmPassword')} <span className="text-red-500">*</span></label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className={`w-5 h-5 ${errors.confirmPassword ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'}`} />
</div>
<input type={showConfirmPassword ? "text" : "password"} value={formData.confirmPassword} onChange={(e) => { setFormData({...formData, confirmPassword: e.target.value}); setErrors({...errors, confirmPassword: null}); }} className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.confirmPassword ? 'border-red-500' : 'border-gray-700'}`} placeholder={t('register.confirmPasswordPlaceholder')} />
<button type="button" onClick={() => setShowConfirmPassword(!showConfirmPassword)} className="absolute inset-y-0 left-0 pl-3 flex items-center">
{showConfirmPassword ? <EyeOff className="w-5 h-5 text-gray-400" /> : <Eye className="w-5 h-5 text-gray-400" />}
</button>
{formData.confirmPassword && (<div className="absolute inset-y-0 left-12 flex items-center">{formData.password === formData.confirmPassword ? <CheckCircle className="w-5 h-5 text-green-500" /> : <XCircle className="w-5 h-5 text-red-500" />}</div>)}
</div>
{errors.confirmPassword && <p className="text-red-500 text-sm mt-1">{errors.confirmPassword}</p>}
</motion.div>
</>
)}
{step === 2 && (
<>
<motion.div variants={fadeInUp} className="flex items-center gap-2">
<input type="checkbox" id="terms" checked={formData.agreeTerms} onChange={(e) => setFormData({...formData, agreeTerms: e.target.checked})} className="w-4 h-4 rounded border-gray-600 bg-white/5 text-blue-500 focus:ring-blue-500" required />
<label htmlFor="terms" className="text-sm text-gray-300">
{t('register.agreeTerms')} <Link href="/terms" className="text-blue-400 hover:text-blue-300">{t('register.termsOfUse')}</Link> {t('register.and')} <Link href="/privacy" className="text-blue-400 hover:text-blue-300">{t('register.privacyPolicy')}</Link>
</label>
</motion.div>
</>
)}
<motion.div variants={fadeInUp} className="flex gap-3 pt-4"> <motion.div variants={fadeInUp} className="flex gap-3 pt-4">
{step === 1 ? ( {step === 1 ? <ButtomStepOne type={"custoemr"} /> : <ButtomStepTwo formData={formData} setFormData={setFormData} isLoading={isLoading} type={"type"} />}
<>
<button type="button" onClick={() => router.push('/auth/choose-role')} className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors">{t('register.cancel')}</button>
<button type="submit" className="flex-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white py-3 px-4 rounded-xl font-medium hover:from-blue-600 hover:to-blue-700 transition-all">{t('register.next')}</button>
</>
) : (
<>
<button type="button" onClick={() => setStep(1)} className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors">{t('register.previous')}</button>
<button type="submit" disabled={isLoading || !formData.agreeTerms} className="flex-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white py-3 px-4 rounded-xl font-medium hover:from-blue-600 hover:to-blue-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed">
{isLoading ? (<div className="flex items-center justify-center gap-2"><Loader2 className="w-5 h-5 animate-spin" /><span>{t('register.registering')}</span></div>) : t('register.createAccount')}
</button>
</>
)}
</motion.div> </motion.div>
{step === 1 && <HavAccount />}
{step === 1 && (
<motion.p variants={fadeInUp} className="text-center text-gray-400 mt-4">
{t('register.haveAccount')} <Link href="/login" className="text-blue-400 hover:text-blue-300 font-medium transition-colors">{t('register.loginLink')}</Link>
</motion.p>
)}
</motion.form> </motion.form>
</div> </div>
</div> </div>
</motion.div> </motion.div>
<AnimatePresence>{showOtpModal && <OtpDialog formData={formData} setShowOtpModal={setShowOtpModal} type={"customer"} />}</AnimatePresence>
<AnimatePresence>
{showOtpModal && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 z-50">
<motion.div initial={{ scale: 0.9, y: 20 }} animate={{ scale: 1, y: 0 }} exit={{ scale: 0.9, y: 20 }} className="bg-gray-900 border border-white/10 rounded-2xl w-full max-w-md p-6 shadow-2xl">
<div className="text-center mb-6">
<div className="w-16 h-16 bg-blue-500/20 rounded-full flex items-center justify-center mx-auto mb-3"><Shield className="w-8 h-8 text-blue-500" /></div>
<h2 className="text-xl font-bold text-white">{t('register.otpTitle')}</h2>
<p className="text-gray-400 text-sm mt-1">{t('register.otpSentTo')}</p>
<p className="text-blue-400 font-medium text-sm">{formData.email}</p>
</div>
<div className="mb-6">
<label className="block text-sm font-medium text-gray-300 mb-2">{t('register.otpLabel')}</label>
<div className="relative">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"><KeyRound className="w-5 h-5 text-gray-400" /></div>
<input type="text" value={otpCode} maxLength={6} onChange={(e) => setOtpCode(e.target.value)} className="w-full pr-12 pl-4 py-3 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 text-white text-center tracking-[0.5em] text-xl" placeholder="------" />
</div>
</div>
<div className="flex gap-3">
<button onClick={handleVerifyOTP} disabled={isLoading || !otpCode} className="flex-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white py-3 rounded-xl font-medium hover:from-blue-600 hover:to-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2">
{isLoading ? <><Loader2 className="w-5 h-5 animate-spin" /><span>{t('register.verifying')}</span></> : t('register.verify')}
</button>
</div>
<button onClick={handleResendOTP} disabled={isLoading} className="w-full text-center text-blue-400 hover:text-blue-300 text-sm mt-3 disabled:opacity-50">{t('register.resendOtp')}</button>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div> </div>
); );
} }

View File

@ -12,9 +12,7 @@ import {
Send, Send,
Loader2, Loader2,
CheckCircle, CheckCircle,
AlertCircle,
User, User,
MessageSquare,
Hash, Hash,
} from 'lucide-react'; } from 'lucide-react';
import { submitReport, submitReservationReport, submitSaleReport } from '../utils/api'; import { submitReport, submitReservationReport, submitSaleReport } from '../utils/api';
@ -138,14 +136,14 @@ export default function ReportsPage() {
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('reports.subjectLabel')}</label> <label className="block text-sm font-medium text-gray-700 mb-2">{t('reports.subjectLabel')}</label>
<div className="relative group"> <div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"> <div className="absolute inset-y-0 end-0 pe-3 flex items-center pointer-events-none">
<FileText className={`w-5 h-5 ${errors.subject ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} /> <FileText className={`w-5 h-5 ${errors.subject ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
</div> </div>
<input <input
type="text" type="text"
value={generalForm.subject} value={generalForm.subject}
onChange={(e) => { setGeneralForm({ ...generalForm, subject: e.target.value }); if (errors.subject) setErrors({ ...errors, subject: null }); }} onChange={(e) => { setGeneralForm({ ...generalForm, subject: e.target.value }); if (errors.subject) setErrors({ ...errors, subject: null }); }}
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all ${errors.subject ? 'border-red-500' : 'border-gray-300'}`} className={`w-full pe-12 ps-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all ${errors.subject ? 'border-red-500' : 'border-gray-300'}`}
placeholder={t('reports.subjectPlaceholder')} placeholder={t('reports.subjectPlaceholder')}
/> />
</div> </div>
@ -184,14 +182,14 @@ export default function ReportsPage() {
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('reports.reservationIdLabel')}</label> <label className="block text-sm font-medium text-gray-700 mb-2">{t('reports.reservationIdLabel')}</label>
<div className="relative group"> <div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"> <div className="absolute inset-y-0 end-0 pe-3 flex items-center pointer-events-none">
<Hash className={`w-5 h-5 ${errors.reservationId ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} /> <Hash className={`w-5 h-5 ${errors.reservationId ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
</div> </div>
<input <input
type="text" type="text"
value={reservationForm.reservationId} value={reservationForm.reservationId}
onChange={(e) => { setReservationForm({ ...reservationForm, reservationId: e.target.value }); if (errors.reservationId) setErrors({ ...errors, reservationId: null }); }} onChange={(e) => { setReservationForm({ ...reservationForm, reservationId: e.target.value }); if (errors.reservationId) setErrors({ ...errors, reservationId: null }); }}
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all ${errors.reservationId ? 'border-red-500' : 'border-gray-300'}`} className={`w-full pe-12 ps-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all ${errors.reservationId ? 'border-red-500' : 'border-gray-300'}`}
placeholder={t('reports.reservationIdPlaceholder')} placeholder={t('reports.reservationIdPlaceholder')}
/> />
</div> </div>
@ -250,14 +248,14 @@ export default function ReportsPage() {
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('reports.saleIdLabel')}</label> <label className="block text-sm font-medium text-gray-700 mb-2">{t('reports.saleIdLabel')}</label>
<div className="relative group"> <div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"> <div className="absolute inset-y-0 end-0 pe-3 flex items-center pointer-events-none">
<Hash className={`w-5 h-5 ${errors.saleId ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} /> <Hash className={`w-5 h-5 ${errors.saleId ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
</div> </div>
<input <input
type="text" type="text"
value={saleForm.saleId} value={saleForm.saleId}
onChange={(e) => { setSaleForm({ ...saleForm, saleId: e.target.value }); if (errors.saleId) setErrors({ ...errors, saleId: null }); }} onChange={(e) => { setSaleForm({ ...saleForm, saleId: e.target.value }); if (errors.saleId) setErrors({ ...errors, saleId: null }); }}
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all ${errors.saleId ? 'border-red-500' : 'border-gray-300'}`} className={`w-full pe-12 ps-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all ${errors.saleId ? 'border-red-500' : 'border-gray-300'}`}
placeholder={t('reports.saleIdPlaceholder')} placeholder={t('reports.saleIdPlaceholder')}
/> />
</div> </div>
@ -314,7 +312,7 @@ export default function ReportsPage() {
}; };
return ( return (
<div className="min-h-screen bg-gray-50 py-8"> <div className="min-h-screen py-8">
<Toaster position="top-center" reverseOrder={false} /> <Toaster position="top-center" reverseOrder={false} />
<div className="container mx-auto px-4 max-w-3xl"> <div className="container mx-auto px-4 max-w-3xl">
<motion.div <motion.div

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,5 @@
import Cookies from "js-cookie";
import { getCustomerByUserId, getOwnerByUserId, loginWithEmail, loginWithPhone } from "../utils/api";
/** /**
* AuthService * AuthService
* Manages authentication tokens and user role detection via JWT decoding. * Manages authentication tokens and user role detection via JWT decoding.
@ -20,31 +22,42 @@
* 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 isSecureContext = () =>
typeof window !== "undefined" && window.location?.protocol === "https:";
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: isSecureContext(),
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); Cookies.remove(USER_KEY);
localStorage.removeItem(USER_KEY); Cookies.remove(TOKEN_KEY);
}, },
/** /**
* Cache full user profile (from API) * Cache full user profile (from API)
* @param {object} user — { name, email, phone, ... } * @param {object} user — { name, email, phone, ... }
*/ */
cacheUser(user) { cacheUser(user) {
if (!user) return; Cookies.set(USER_KEY, JSON.stringify(user), {
localStorage.setItem(USER_KEY, JSON.stringify(user)); expires: 7,
secure: isSecureContext(),
sameSite: "lax",
path: "/",
});
}, },
/** /**
@ -52,8 +65,12 @@ const AuthService = Object.freeze({
* @returns {object|null} * @returns {object|null}
*/ */
getCachedUser() { getCachedUser() {
const user = Cookies.get(USER_KEY);
if (!user) return null;
try { try {
return JSON.parse(localStorage.getItem(USER_KEY)); return JSON.parse(user);
} catch { } catch {
return null; return null;
} }
@ -67,7 +84,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;
@ -81,15 +98,14 @@ const AuthService = Object.freeze({
getUser() { getUser() {
const payload = this.decodeToken(); const payload = this.decodeToken();
if (!payload) return null; if (!payload) return null;
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: cached?.roles,
}; };
}, },
@ -100,7 +116,6 @@ const AuthService = Object.freeze({
getUserId() { getUserId() {
const user = this.getUser(); const user = this.getUser();
if (!user?.id) return null; if (!user?.id) return null;
const parsedId = Number(user.id); const parsedId = Number(user.id);
return Number.isFinite(parsedId) ? parsedId : user.id; return Number.isFinite(parsedId) ? parsedId : user.id;
}, },
@ -112,18 +127,32 @@ 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 [];
}, },
async cacheCurrentUser(getOwnerByUserId, getCustomerByUserId) {
const authUser = this.getUser();
if (!authUser?.id) {
return;
}
const fetchFn = this.isOwner() ? getOwnerByUserId : getCustomerByUserId;
const profile = await fetchFn(authUser.id);
this.cacheUser({
name: profile.firstName + profile.lastName,
email: profile.email,
phone: profile.phoneNumber,
roles: this.getRoles(),
});
},
/** /**
* User has Owner role * User has Owner role
* @returns {boolean} * @returns {boolean}
*/ */
isOwner() { isOwner() {
return this.getRoles().includes('Owner'); return this.getRoles().includes("Owner");
}, },
/** /**
@ -131,7 +160,7 @@ const AuthService = Object.freeze({
* @returns {boolean} * @returns {boolean}
*/ */
isAgent() { isAgent() {
return this.getRoles().includes('RealEstateAgent'); return this.getRoles().includes("RealEstateAgent");
}, },
/** /**
@ -157,7 +186,37 @@ const AuthService = Object.freeze({
isAuthenticated() { isAuthenticated() {
return !!this.getToken(); return !!this.getToken();
}, },
async login({ loginMethod, credential, password }) {
const loginFn = loginMethod === "email" ? loginWithEmail : loginWithPhone;
const result = await loginFn(credential, password);
switch (result.status) {
case 200: {
const token = typeof result.data === "string" ? result.data : result.data.token;
this.addToken(token);
await this.cacheCurrentUser(getOwnerByUserId, getCustomerByUserId);
return {
type: "SUCCESS",
};
}
case 206: {
const token = typeof result.data === "string" ? result.data : result.data.token;
this.addToken(token);
if (loginMethod === "email") await sendEmailOTP();
else await sendPhoneOTP();
return {
type: "OTP_REQUIRED",
};
}
default:
return {
type: "ERROR",
result,
};
}
},
}); });
export default AuthService; export default AuthService;

View File

@ -23,7 +23,7 @@ import {
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
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 { changePassword, deleteMyAccount, submitReport } from '../utils/api'; import { deleteMyAccount } from '../utils/api';
export default function SettingsPage() { export default function SettingsPage() {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
@ -31,12 +31,6 @@ export default function SettingsPage() {
const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [deletePassword, setDeletePassword] = useState(''); const [deletePassword, setDeletePassword] = useState('');
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [showContactDialog, setShowContactDialog] = useState(false);
const [contactSubject, setContactSubject] = useState('');
const [contactBody, setContactBody] = useState('');
const [isSendingContact, setIsSendingContact] = useState(false);
const handleSignOut = () => { const handleSignOut = () => {
AuthService.deleteToken(); AuthService.deleteToken();
@ -65,23 +59,45 @@ export default function SettingsPage() {
} }
}; };
const handleSendContact = async () => { const handleContactSupport = async () => {
if (!contactSubject.trim() || !contactBody.trim()) { try {
toast.error(t('fill-subject-and-message')); let token = '';
return; if (typeof AuthService.getToken === 'function') {
token = AuthService.getToken();
} else if (typeof window !== 'undefined') {
token = localStorage.getItem('token') || localStorage.getItem('accessToken') || '';
} }
setIsSendingContact(true); const response = await fetch('http://45.93.137.91/api/Configuration/GetPlatformNumber', {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error('Failed to fetch support number');
}
const responseText = await response.text();
let numberStr = responseText;
try { try {
await submitReport(contactSubject.trim(), contactBody.trim()); const parsed = JSON.parse(responseText);
toast.success(t('send-success')); numberStr = parsed.data || parsed.number || parsed.value || parsed;
setShowContactDialog(false); } catch (e) {}
setContactSubject('');
setContactBody(''); const cleanNumber = typeof numberStr === 'string'
? numberStr.replace(/[^0-9+]/g, '')
: String(numberStr).replace(/[^0-9+]/g, '');
if (cleanNumber) {
window.location.href = `https://wa.me/${cleanNumber}`;
} else {
toast.error(t('error'));
}
} catch (err) { } catch (err) {
toast.error(err.message || t('send-error')); toast.error(t('error'));
} finally {
setIsSendingContact(false);
} }
}; };
@ -104,7 +120,7 @@ export default function SettingsPage() {
title: t('support'), title: t('support'),
items: [ items: [
{ icon: HelpCircle, label: t('faq'), href: '/faq', desc: t('faq-desc') }, { icon: HelpCircle, label: t('faq'), href: '/faq', desc: t('faq-desc') },
{ icon: MessageCircle, label: t('contactUs'), desc: t('contact-us-desc'), action: () => setShowContactDialog(true) }, { icon: MessageCircle, label: t('contactUs'), desc: t('contact-us-desc'), action: handleContactSupport },
{ icon: FileText, label: t('terms-and-conditions'), href: '/terms', desc: t('terms-desc') }, { icon: FileText, label: t('terms-and-conditions'), href: '/terms', desc: t('terms-desc') },
{ icon: Eye, label: t('privacy'), href: '/privacy', desc: t('privacy-desc') }, { icon: Eye, label: t('privacy'), href: '/privacy', desc: t('privacy-desc') },
] ]
@ -289,74 +305,6 @@ export default function SettingsPage() {
</motion.div> </motion.div>
</div> </div>
)} )}
{showContactDialog && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="bg-white rounded-2xl shadow-xl max-w-md w-full p-6"
>
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-full bg-amber-100 flex items-center justify-center">
<MessageCircle className="w-5 h-5 text-amber-600" />
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900">{t('contactUs')}</h3>
<p className="text-sm text-gray-500">{t('contact-reply-prompt')}</p>
</div>
<button
onClick={() => {
setShowContactDialog(false);
setContactSubject('');
setContactBody('');
}}
className="mr-auto p-1 hover:bg-gray-100 rounded-lg transition-colors"
>
<X className="w-5 h-5 text-gray-400" />
</button>
</div>
<input
type="text"
value={contactSubject}
onChange={(e) => setContactSubject(e.target.value)}
placeholder={t('subject')}
maxLength={300}
className="w-full px-4 py-3 border border-gray-300 rounded-xl mb-4 focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none"
/>
<textarea
value={contactBody}
onChange={(e) => setContactBody(e.target.value)}
placeholder={t('write-message-placeholder')}
rows={5}
className="w-full px-4 py-3 border border-gray-300 rounded-xl mb-4 focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none resize-none"
/>
<div className="flex gap-3">
<button
onClick={() => {
setShowContactDialog(false);
setContactSubject('');
setContactBody('');
}}
className="flex-1 px-4 py-3 rounded-xl border border-gray-200 text-gray-700 hover:bg-gray-50 transition-colors text-sm font-medium"
>
{t('cancel')}
</button>
<button
onClick={handleSendContact}
disabled={isSendingContact}
className="flex-1 px-4 py-3 rounded-xl bg-amber-600 text-white hover:bg-amber-700 transition-colors text-sm font-medium disabled:opacity-50 flex items-center justify-center gap-2"
>
{isSendingContact ? <Loader2 className="w-4 h-4 animate-spin" /> : <MessageCircle className="w-4 h-4" />}
{isSendingContact ? t('sending') : t('send')}
</button>
</div>
</motion.div>
</div>
)}
</div> </div>
); );
} }

View File

@ -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 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>

View File

@ -1,9 +1,9 @@
'use client'; "use client";
import { useEffect, useState } from 'react'; import { useEffect, useState } from "react";
import { motion } from 'framer-motion'; import { motion } from "framer-motion";
import { FileText, Shield, CheckCircle, Languages, Loader2, AlertCircle } from 'lucide-react'; import { FileText, Shield, CheckCircle, Languages, Loader2, AlertCircle } from "lucide-react";
import { getARTerms, getENTerms } from '../utils/api'; import { getARTerms, getENTerms } from "../utils/api";
const containerVariants = { const containerVariants = {
hidden: { opacity: 0 }, hidden: { opacity: 0 },
@ -21,75 +21,63 @@ const itemVariants = {
const FALLBACK_TERMS = { const FALLBACK_TERMS = {
ar: [ ar: [
{ {
title: 'مقدمة', title: "مقدمة",
description: description: "مرحباً بك في منصة SweetHome. باستخدامك للمنصة، فإنك توافق على الالتزام بشروط الاستخدام هذه. إذا كنت لا توافق على أي جزء من هذه الشروط، يرجى عدم استخدام المنصة.",
'مرحباً بك في منصة SweetHome. باستخدامك للمنصة، فإنك توافق على الالتزام بشروط الاستخدام هذه. إذا كنت لا توافق على أي جزء من هذه الشروط، يرجى عدم استخدام المنصة.',
}, },
{ {
title: 'استخدام المنصة', title: "استخدام المنصة",
description: description: "يُسمح باستخدام المنصة للأغراض المشروعة فقط. يلتزم المستخدم بعدم استخدام المنصة في أي نشاط غير قانوني.",
'يُسمح باستخدام المنصة للأغراض المشروعة فقط. يلتزم المستخدم بعدم استخدام المنصة في أي نشاط غير قانوني.',
}, },
{ {
title: 'حقوق ومسؤوليات المالك', title: "حقوق ومسؤوليات المالك",
description: description: "يتحمل المالك مسؤولية دقة المعلومات المقدمة عن العقار بما في ذلك الصور والوصف والسعر والتوفر.",
'يتحمل المالك مسؤولية دقة المعلومات المقدمة عن العقار بما في ذلك الصور والوصف والسعر والتوفر.',
}, },
{ {
title: 'حقوق ومسؤوليات المستأجر', title: "حقوق ومسؤوليات المستأجر",
description: description: "يلتزم المستأجر باستخدام العقار بطريقة مسؤولة وعدم التسبب في أي ضرر للممتلكات.",
'يلتزم المستأجر باستخدام العقار بطريقة مسؤولة وعدم التسبب في أي ضرر للممتلكات.',
}, },
{ {
title: 'الدفع والعمولات', title: "الدفع والعمولات",
description: description: "تتقاضى المنصة عمولة على كل حصة ناجحة وفقاً للنسبة المحددة في وقت الحجز.",
'تتقاضى المنصة عمولة على كل حصة ناجحة وفقاً للنسبة المحددة في وقت الحجز.',
}, },
{ {
title: 'خصوصية البيانات', title: "خصوصية البيانات",
description: description: "نحن نأخذ خصوصية بياناتك على محمل الجد. يتم جمع واستخدام البيانات الشخصية وفقاً لسياسة الخصوصية الخاصة بنا.",
'نحن نأخذ خصوصية بياناتك على محمل الجد. يتم جمع واستخدام البيانات الشخصية وفقاً لسياسة الخصوصية الخاصة بنا.',
}, },
], ],
en: [ en: [
{ {
title: 'Introduction', title: "Introduction",
description: description: "Welcome to SweetHome. By using our platform, you agree to comply with these terms. If you do not agree, please do not use the platform.",
'Welcome to SweetHome. By using our platform, you agree to comply with these terms. If you do not agree, please do not use the platform.',
}, },
{ {
title: 'Platform Usage', title: "Platform Usage",
description: description: "The platform may only be used for lawful purposes. Users must not engage in any illegal activity.",
'The platform may only be used for lawful purposes. Users must not engage in any illegal activity.',
}, },
{ {
title: 'Owner Rights & Responsibilities', title: "Owner Rights & Responsibilities",
description: description: "Owners are responsible for the accuracy of property information including images, description, price, and availability.",
'Owners are responsible for the accuracy of property information including images, description, price, and availability.',
}, },
{ {
title: 'Tenant Rights & Responsibilities', title: "Tenant Rights & Responsibilities",
description: description: "Tenants must use the property responsibly and not cause any damage to the property.",
'Tenants must use the property responsibly and not cause any damage to the property.',
}, },
{ {
title: 'Payment & Commissions', title: "Payment & Commissions",
description: description: "The platform charges a commission on each successful booking according to the rate specified at the time of booking.",
'The platform charges a commission on each successful booking according to the rate specified at the time of booking.',
}, },
{ {
title: 'Data Privacy', title: "Data Privacy",
description: description: "We take your data privacy seriously. Personal data is collected and used in accordance with our Privacy Policy.",
'We take your data privacy seriously. Personal data is collected and used in accordance with our Privacy Policy.',
}, },
], ],
}; };
export default function TermsPage() { export default function TermsPage() {
const [terms, setTerms] = useState([]); const [terms, setTerms] = useState([]);
const [language, setLanguage] = useState('ar'); const [language, setLanguage] = useState("ar");
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(''); const [error, setError] = useState("");
useEffect(() => { useEffect(() => {
const controller = new AbortController(); const controller = new AbortController();
@ -97,9 +85,9 @@ export default function TermsPage() {
const fetchTerms = async () => { const fetchTerms = async () => {
try { try {
setLoading(true); setLoading(true);
setError(''); setError("");
const fetcher = language === 'ar' ? getARTerms : getENTerms; const fetcher = language === "ar" ? getARTerms : getENTerms;
const data = await fetcher(); const data = await fetcher();
if (!data) { if (!data) {
@ -115,14 +103,14 @@ export default function TermsPage() {
} }
const mapped = raw.map((item) => ({ const mapped = raw.map((item) => ({
title: item.title || item.name || '', title: item.title || item.name || "",
description: item.description || item.content || item.body || item.text || '', description: item.description || item.content || item.body || item.text || "",
})); }));
setTerms(mapped); setTerms(mapped);
} catch { } catch {
setTerms(FALLBACK_TERMS[language]); setTerms(FALLBACK_TERMS[language]);
setError(''); setError("");
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -134,102 +122,62 @@ export default function TermsPage() {
}, [language]); }, [language]);
return ( return (
<div <div dir={language === "ar" ? "rtl" : "ltr"} className="min-h-screen bg-gradient-to-b py-12">
dir={language === 'ar' ? 'rtl' : 'ltr'}
className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12"
>
<div className="container mx-auto px-4 max-w-4xl"> <div className="container mx-auto px-4 max-w-4xl">
<motion.div <motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="text-center mb-12">
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center mb-12"
>
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-amber-100"> <div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-amber-100">
<FileText className="w-10 h-10 text-amber-600" /> <FileText className="w-10 h-10 text-amber-600" />
</div> </div>
<div className="flex items-center justify-center gap-4 mb-4"> <div className="flex items-center justify-center gap-4 mb-4">
<h1 className="text-4xl font-bold text-gray-900"> <h1 className="text-4xl font-bold text-gray-900">{language === "ar" ? "شروط الاستخدام" : "Terms of Use"}</h1>
{language === 'ar' ? 'شروط الاستخدام' : 'Terms of Use'}
</h1>
<button <button
type="button" type="button"
onClick={() => setLanguage(language === 'ar' ? 'en' : 'ar')} onClick={() => setLanguage(language === "ar" ? "en" : "ar")}
className="inline-flex items-center gap-2 rounded-full border border-amber-200 bg-white px-4 py-2 text-sm font-semibold text-gray-700 shadow-sm transition hover:shadow-md" className="inline-flex items-center gap-2 rounded-full border border-amber-200 bg-white px-4 py-2 text-sm font-semibold text-gray-700 shadow-sm transition hover:shadow-md"
> >
<Languages className="h-4 w-4" /> <Languages className="h-4 w-4" />
{language === 'ar' ? 'English' : 'العربية'} {language === "ar" ? "English" : "العربية"}
</button> </button>
</div> </div>
<p className="text-lg text-gray-600 max-w-2xl mx-auto"> <p className="text-lg text-gray-600 max-w-2xl mx-auto">
{language === 'ar' {language === "ar" ? "يرجى قراءة شروط الاستخدام التالية بعناية قبل استخدام المنصة" : "Please read the following terms of use carefully before using the platform"}
? 'يرجى قراءة شروط الاستخدام التالية بعناية قبل استخدام المنصة'
: 'Please read the following terms of use carefully before using the platform'}
</p> </p>
</motion.div> </motion.div>
{loading && ( {loading && (
<div className="flex items-center justify-center gap-3 mb-8 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-4 text-amber-800"> <div className="flex items-center justify-center gap-3 mb-8 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-4 text-amber-800">
<Loader2 className="h-5 w-5 animate-spin" /> <Loader2 className="h-5 w-5 animate-spin" />
<span> <span>{language === "ar" ? "جاري تحميل شروط الاستخدام..." : "Loading terms of use..."}</span>
{language === 'ar'
? 'جاري تحميل شروط الاستخدام...'
: 'Loading terms of use...'}
</span>
</div> </div>
)} )}
{error && ( {error && (
<motion.div <motion.div initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} className="mb-8 rounded-2xl border border-red-200 bg-red-50 p-4 flex items-center gap-3 text-red-700">
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="mb-8 rounded-2xl border border-red-200 bg-red-50 p-4 flex items-center gap-3 text-red-700"
>
<AlertCircle className="h-5 w-5 shrink-0" /> <AlertCircle className="h-5 w-5 shrink-0" />
<span>{error}</span> <span>{error}</span>
</motion.div> </motion.div>
)} )}
{!loading && terms.length === 0 && !error && ( {!loading && terms.length === 0 && !error && (
<motion.div <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-center py-16 text-gray-500">
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-center py-16 text-gray-500"
>
<FileText className="h-16 w-16 mx-auto mb-4 text-gray-300" /> <FileText className="h-16 w-16 mx-auto mb-4 text-gray-300" />
<p className="text-xl font-medium"> <p className="text-xl font-medium">{language === "ar" ? "لا توجد شروط استخدام متاحة حالياً" : "No terms of use available"}</p>
{language === 'ar'
? 'لا توجد شروط استخدام متاحة حالياً'
: 'No terms of use available'}
</p>
</motion.div> </motion.div>
)} )}
{terms.length > 0 && ( {terms.length > 0 && (
<motion.div <motion.div variants={containerVariants} initial="hidden" animate="visible" className="space-y-6">
variants={containerVariants}
initial="hidden"
animate="visible"
className="space-y-6"
>
{terms.map((term, index) => ( {terms.map((term, index) => (
<motion.div <motion.div key={index} variants={itemVariants} className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow">
key={index}
variants={itemVariants}
className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow"
>
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="w-10 h-10 bg-amber-100 rounded-xl flex items-center justify-center shrink-0 mt-1"> <div className="w-10 h-10 bg-amber-100 rounded-xl flex items-center justify-center shrink-0 mt-1">
<Shield className="w-5 h-5 text-amber-600" /> <Shield className="w-5 h-5 text-amber-600" />
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
{term.title && ( {term.title && <h2 className="text-xl font-bold text-gray-900 mb-3">{term.title}</h2>}
<h2 className="text-xl font-bold text-gray-900 mb-3">{term.title}</h2> <p className="text-gray-600 leading-relaxed whitespace-pre-wrap">{term.description}</p>
)}
<p className="text-gray-600 leading-relaxed whitespace-pre-wrap">
{term.description}
</p>
</div> </div>
</div> </div>
</motion.div> </motion.div>
@ -237,21 +185,14 @@ export default function TermsPage() {
</motion.div> </motion.div>
)} )}
<motion.div <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.6 }} className="mt-8 bg-amber-50 rounded-2xl border border-amber-200 p-6 flex items-start gap-4">
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.6 }}
className="mt-8 bg-amber-50 rounded-2xl border border-amber-200 p-6 flex items-start gap-4"
>
<CheckCircle className="w-6 h-6 text-amber-600 shrink-0 mt-0.5" /> <CheckCircle className="w-6 h-6 text-amber-600 shrink-0 mt-0.5" />
<div> <div>
<p className="font-bold text-amber-800 mb-1"> <p className="font-bold text-amber-800 mb-1">{language === "ar" ? "آخر تحديث" : "Last Updated"}</p>
{language === 'ar' ? 'آخر تحديث' : 'Last Updated'}
</p>
<p className="text-amber-700"> <p className="text-amber-700">
{language === 'ar' {language === "ar"
? 'تم آخر تحديث لشروط الاستخدام في 1 مايو 2026. يرجى مراجعة هذه الصفحة بشكل دوري للاطلاع على أي تغييرات.' ? "تم آخر تحديث لشروط الاستخدام في 1 مايو 2026. يرجى مراجعة هذه الصفحة بشكل دوري للاطلاع على أي تغييرات."
: 'Last updated on May 1, 2026. Please review this page periodically for any changes.'} : "Last updated on May 1, 2026. Please review this page periodically for any changes."}
</p> </p>
</div> </div>
</motion.div> </motion.div>

View File

@ -1,5 +1,4 @@
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";
const REPORT_API_BASE = process.env.NEXT_PUBLIC_REPORT_API_URL || "http://45.93.137.91/api"; const REPORT_API_BASE = process.env.NEXT_PUBLIC_REPORT_API_URL || "http://45.93.137.91/api";
@ -20,10 +19,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");
} }
} }
@ -39,9 +35,6 @@ function buildApiUrl(base, endpoint) {
return `${base.replace(/\/$/, "")}${endpoint.startsWith("/") ? endpoint : `/${endpoint}`}`; return `${base.replace(/\/$/, "")}${endpoint.startsWith("/") ? endpoint : `/${endpoint}`}`;
} }
/**
* Generic API fetch — attaches auth token, unwraps { data } envelope
*/
async function apiFetch(endpoint, options = {}) { async function apiFetch(endpoint, options = {}) {
const token = AuthService.getToken(); const token = AuthService.getToken();
@ -53,12 +46,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";
} }
@ -68,10 +56,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);
@ -94,18 +79,13 @@ 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( throw new Error(`تعذر الاتصال بالخادم. تحقق من اتصالك بالإنترنت أو حاول مرة أخرى لاحقاً. (${API_BASE}${endpoint})`);
`تعذر الاتصال بالخادم. تحقق من اتصالك بالإنترنت أو حاول مرة أخرى لاحقاً. (${API_BASE}${endpoint})`,
);
} }
throw error; throw error;
} }
} }
/**
* Auth fetch — returns full { status, data, ok } for status-code handling
*/
async function authFetch(endpoint, body, token = null) { async function authFetch(endpoint, body, token = null) {
const headers = {}; const headers = {};
@ -143,8 +123,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,
@ -182,8 +161,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,
@ -193,8 +171,6 @@ async function reportFetch(endpoint, body) {
}; };
} }
// ─── Rent Properties ───
export async function getRentProperties() { export async function getRentProperties() {
return apiFetch("/RentProperties/GetRentProperties"); return apiFetch("/RentProperties/GetRentProperties");
} }
@ -208,31 +184,23 @@ 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}` : ""}`,
);
} }
// ─── Sale Properties ───
export async function getSaleProperties() { export async function getSaleProperties() {
return apiFetch("/SaleProperties/GetSaleProperties"); return apiFetch("/SaleProperties/GetSaleProperties");
} }
export async function getSaleProperty(id) { export async function getSaleProperty(id) {
const items = await apiFetch("/SaleProperties/GetSaleProperties"); const items = await apiFetch(`/SaleProperties/GetSalePropertyById/${id}`);
if (!Array.isArray(items)) return items; if (!Array.isArray(items)) return items;
return items.find((p) => p.id == id) || items[0]; return items.find((p) => p.id == id) || items[0];
} }
// ─── Properties (generic) ───
export async function getProperty(id) { export async function getProperty(id) {
return apiFetch(`/Properties/Get/${id}`); return apiFetch(`/Properties/Get/${id}`);
} }
// ─── Recommendations ───
export async function getRecommendations() { export async function getRecommendations() {
return apiFetch("/Recommendations/GetRecommendations"); return apiFetch("/Recommendations/GetRecommendations");
} }
@ -241,43 +209,32 @@ export async function getTopRecommendations(count = 10) {
return apiFetch(`/Recommendations/GetTopRecommendations?count=${count}`); return apiFetch(`/Recommendations/GetTopRecommendations?count=${count}`);
} }
// ─── Reservations ─── export async function getAvailableDateRanges(propertyId, fromDate = null, toDate = null) {
export async function getAvailableDateRanges(
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() {
return apiFetch("/Reservations/GetAllReservations"); return apiFetch("/Reservations/GetAllReservations");
} }
export async function getReservation(id) { 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) {
@ -291,8 +248,6 @@ export async function bookReservation(propertyInfoId, startDate, endDate) {
}); });
} }
// ─── Terms ───
export async function getARTerms() { export async function getARTerms() {
return apiFetch("/Configuration/GetARTerms"); return apiFetch("/Configuration/GetARTerms");
} }
@ -301,8 +256,6 @@ export async function getENTerms() {
return apiFetch("/Configuration/GetENTerms"); return apiFetch("/Configuration/GetENTerms");
} }
// ─── Profile ───
export async function getCustomerByUserId(userId) { export async function getCustomerByUserId(userId) {
return apiFetch(`/Customer/GetByUserId/${userId}`); return apiFetch(`/Customer/GetByUserId/${userId}`);
} }
@ -311,32 +264,28 @@ export async function getOwnerByUserId(userId) {
return apiFetch(`/Owner/GetByUserId/${userId}`); return apiFetch(`/Owner/GetByUserId/${userId}`);
} }
// ─── Properties ───
export async function getMyRentListings() { export async function getMyRentListings() {
return apiFetch("/RentProperties/GetMyRentListings"); return apiFetch("/RentProperties/GetMyRentListings");
} }
export function buildRentPropertyPayload(data = {}) { export function buildRentPropertyPayload(data = {}) {
const propertyInformation = data?.propertyInformation || {}; const propertyInformation = data?.propertyInformation || {};
const cityValue =
data?.city ?? const rawCityValue = data?.city ?? data?.governorate ?? propertyInformation?.city ?? propertyInformation?.governorate ?? 1;
data?.governorate ??
propertyInformation?.city ?? const cityInt = parseInt(rawCityValue, 10) || 1;
propertyInformation?.governorate ??
""; const documentTypeValue = data?.documentType ?? propertyInformation?.documentType ?? 1;
const documentTypeValue =
data?.documentType ?? propertyInformation?.documentType ?? "";
return { return {
...data, ...data,
city: cityValue, city: cityInt,
governorate: cityValue, governorate: cityInt,
documentType: documentTypeValue, documentType: documentTypeValue,
propertyInformation: { propertyInformation: {
...propertyInformation, ...propertyInformation,
city: cityValue, city: cityInt,
governorate: propertyInformation?.governorate ?? cityValue, governorate: cityInt,
documentType: documentTypeValue, documentType: documentTypeValue,
}, },
}; };
@ -350,9 +299,44 @@ export async function addRentProperty(data) {
} }
export async function editRentProperty(id, data) { export async function editRentProperty(id, data) {
const pi = data?.propertyInformation || {};
const images = Array.isArray(pi.images) ? pi.images : [];
const body = {
propertyInformation: {
activityStatus: 1,
images: images.length > 0 ? images : [""],
cordsX: pi.cordsX ?? null,
cordsY: pi.cordsY ?? null,
address: pi.address ?? "",
description: pi.description ?? null,
numberOfBathRooms: pi.numberOfBathRooms ?? 0,
numberOfRooms: pi.numberOfRooms ?? 0,
numberOfBedRooms: pi.numberOfBedRooms ?? 0,
space: pi.space ?? 0,
detailsJSON: pi.detailsJSON ?? "",
buildingType: pi.buildingType ?? 0,
status: pi.status ?? 0,
propertyType: pi.propertyType ?? 0,
city: data.city ?? pi.city ?? 1,
},
deposit: data.deposit ?? 0,
acceptedCertificate: data.acceptedCertificate ?? 0,
monthlyRent: data.monthlyRent ?? 0,
dailyRent: data.dailyRent ?? 0,
rating: data.rating ?? 1,
currencyId: data.currencyId ?? 1,
rentType: data.rentType ?? 0,
isSmokeAllow: data.isSmokeAllow ?? false,
specializedFor: data.specializedFor ?? false,
isVisitorAllow: data.isVisitorAllow ?? false,
allowedPaymentPeriod: data.allowedPaymentPeriod ?? "",
type: data.type ?? 0,
};
return apiFetch(`/RentProperties/EditRentProperty/${id}`, { return apiFetch(`/RentProperties/EditRentProperty/${id}`, {
method: "PUT", method: "PUT",
body: data, body,
}); });
} }
@ -364,6 +348,7 @@ export async function editSaleProperty(id, data) {
} }
export async function addSaleProperty(data) { export async function addSaleProperty(data) {
return apiFetch("/SaleProperties/AddSaleProperty", { return apiFetch("/SaleProperties/AddSaleProperty", {
method: "POST", method: "POST",
body: data , body: data ,
@ -392,8 +377,6 @@ export async function updateSalePropertyStatus(id, status) {
}); });
} }
// ─── Currencies ───
export async function getCurrencies() { export async function getCurrencies() {
return apiFetch("/Currency/GetAll"); return apiFetch("/Currency/GetAll");
} }
@ -402,8 +385,6 @@ export async function getPaymentTypes() {
return apiFetch("/PaymentType/GetAll"); return apiFetch("/PaymentType/GetAll");
} }
// ─── Files ───
export async function uploadPicture(file) { export async function uploadPicture(file) {
const formData = new FormData(); const formData = new FormData();
formData.append("image", file); formData.append("image", file);
@ -437,11 +418,8 @@ export async function uploadPicture(file) {
} }
} }
// ─── Auth: Registration ───
async function multipartAuthFetch(endpoint, formData) { async function multipartAuthFetch(endpoint, formData) {
const token = AuthService.getToken(); const token = AuthService.getToken();
const res = await fetch(`${API_BASE}${endpoint}`, { const res = await fetch(`${API_BASE}${endpoint}`, {
method: "POST", method: "POST",
headers: { headers: {
@ -472,46 +450,28 @@ 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);
if (backImage) formData.append("RearIdCarImagePath", backImage); if (backImage) formData.append("RearIdCarImagePath", backImage);
if (licenseImage) formData.append("LicenseImagePath", licenseImage); if (licenseImage) formData.append("RearIdCarImagePath", licenseImage);
return multipartAuthFetch("/Owner/Add", formData); return multipartAuthFetch("/Owner/Add", formData);
} }
@ -534,8 +494,6 @@ export async function addCustomer(data, frontImage = null, backImage = null) {
return multipartAuthFetch("/Customer/Add", formData); return multipartAuthFetch("/Customer/Add", formData);
} }
// ─── Auth: Login ───
export async function loginWithEmail(credential, password) { export async function loginWithEmail(credential, password) {
return authFetch("/Auth/LogInWithEmail", { return authFetch("/Auth/LogInWithEmail", {
credential, credential,
@ -554,8 +512,6 @@ export async function loginWithPhone(credential, password) {
}); });
} }
// ─── Auth: OTP ───
export async function sendEmailOTP() { export async function sendEmailOTP() {
return apiFetch("/Auth/SendEmailOTP", { method: "POST" }); return apiFetch("/Auth/SendEmailOTP", { method: "POST" });
} }
@ -566,24 +522,14 @@ 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,
);
} }
// ─── Helpers ───
export function isEmail(value) { export function isEmail(value) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
} }
@ -592,8 +538,6 @@ export function isPhoneNumber(value) {
return /^\+?\d{7,15}$/.test(value.replace(/[\s\-()]/g, "")); return /^\+?\d{7,15}$/.test(value.replace(/[\s\-()]/g, ""));
} }
// ─── Favorites ───
export async function getUserFavoriteProperties() { export async function getUserFavoriteProperties() {
return apiFetch("/FavoriteProperty/GetUserFavoriteProperties"); return apiFetch("/FavoriteProperty/GetUserFavoriteProperties");
} }
@ -612,8 +556,6 @@ export async function getUserNotifications() {
return apiFetch("/Notifications/GetUserNotifications"); return apiFetch("/Notifications/GetUserNotifications");
} }
// ─── Booking/Reservation Management ───
export async function confirmDepositPayment(bookingId) { export async function confirmDepositPayment(bookingId) {
return apiFetch("/Reservations/ConfirmDepositPayment", { return apiFetch("/Reservations/ConfirmDepositPayment", {
method: "POST", method: "POST",
@ -621,14 +563,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,
@ -656,8 +593,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 };
} }
@ -669,8 +605,6 @@ export async function updateBookingStatus(bookingId, status) {
}); });
} }
// ─── Owner / Reservations ───
export async function getOwnerReservationRequests() { export async function getOwnerReservationRequests() {
return apiFetch("/Reservations/GetOwnerResevationRequests"); return apiFetch("/Reservations/GetOwnerResevationRequests");
} }
@ -692,8 +626,6 @@ export async function ownerConfirmReservation(id) {
}); });
} }
// ─── Payments ───
export async function getMyTransaction() { export async function getMyTransaction() {
return apiFetch("/Customer/GetMyTransaction"); return apiFetch("/Customer/GetMyTransaction");
} }
@ -701,15 +633,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);
@ -717,9 +643,7 @@ export async function payDeposit(data) {
formData.append("Comment", ""); formData.append("Comment", "");
} }
// صورة الوصل لطريقة الدفع Haram
if (data.paymentImage) { if (data.paymentImage) {
formData.append("TransactionType", data.paymentImage);
formData.append("paymentImage", data.paymentImage); formData.append("paymentImage", data.paymentImage);
} }
@ -729,20 +653,14 @@ export async function payDeposit(data) {
}); });
} }
// ─── Owner Contact & Stats ───
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() {
return apiFetch("/Statistics/GetOwnerStatistics"); return apiFetch("/Statistics/GetOwnerStatistics");
} }
// ─── Agent Registration ───
export async function registerRealEstateAgent(formData) { export async function registerRealEstateAgent(formData) {
const token = AuthService.getToken(); const token = AuthService.getToken();
@ -774,19 +692,12 @@ export async function registerRealEstateAgent(formData) {
}; };
} }
// ─── Change Password ───
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",
}, });
);
} }
// ─── Forget Password (OTP flow) ───
export async function requestForgetPasswordOtp(email) { export async function requestForgetPasswordOtp(email) {
return apiFetch(`/User/ForgetPassword?email=${encodeURIComponent(email)}`, { return apiFetch(`/User/ForgetPassword?email=${encodeURIComponent(email)}`, {
method: "POST", method: "POST",
@ -794,33 +705,21 @@ 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",
}, });
);
} }
// ─── Reset Password (token flow) ───
export async function resetPassword(token) { export async function resetPassword(token) {
return apiFetch(`/Auth/ResetPassword?token=${encodeURIComponent(token)}`); return apiFetch(`/Auth/ResetPassword?token=${encodeURIComponent(token)}`);
} }
// ─── Delete Account ───
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",
}, });
);
} }
// ─── Set FCM Token ───
export async function setFCMToken(token, deviceType = 2) { export async function setFCMToken(token, deviceType = 2) {
return apiFetch("/User/SetFCMToken", { return apiFetch("/User/SetFCMToken", {
method: "POST", method: "POST",
@ -828,21 +727,15 @@ export async function setFCMToken(token, deviceType = 2) {
}); });
} }
// ─── Filter Rent Properties ───
export async function filterRentProperties(params = {}) { export async function filterRentProperties(params = {}) {
const qs = new URLSearchParams(); const qs = new URLSearchParams();
Object.entries(params).forEach(([k, v]) => { Object.entries(params).forEach(([k, v]) => {
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}` : ""}`,
);
} }
// ─── Reports ───
export async function sendGeneralReport(subject, reportBody) { export async function sendGeneralReport(subject, reportBody) {
return reportFetch("/Reports/SendGeneralReport", { return reportFetch("/Reports/SendGeneralReport", {
subject, subject,
@ -885,8 +778,6 @@ export async function updateSaleReport(id, data) {
}); });
} }
// ─── Terms (Add or Update) ───
export async function addOrUpdateTerms(terms) { export async function addOrUpdateTerms(terms) {
return apiFetch("/Terms/AddOrUpdateTerms", { return apiFetch("/Terms/AddOrUpdateTerms", {
method: "POST", method: "POST",

View File

@ -38,8 +38,7 @@ export async function requestNotificationPermission() {
); );
const token = await getToken(messaging, { const token = await getToken(messaging, {
vapidKey: vapidKey:"BGZ4Fo8rRhoTdStLGlCySDZOnAX4ekCA0e3HDWXL5uEi2kOnXynYjbaDbY15002phUrFqxBpPPFHgfH2VhrmFDU",
"BGZ4Fo8rRhoTdStLGlCySDZOnAX4ekCA0e3HDWXL5uEi2kOnXynYjbaDbY15002phUrFqxBpPPFHgfH2VhrmFDU",
serviceWorkerRegistration: registration, serviceWorkerRegistration: registration,
}); });

Some files were not shown because too many files have changed in this diff Show More