Compare commits

..

78 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
47550dbfac FilterRentProperties​ API
All checks were successful
Build frontend / build (push) Successful in 2m8s
2026-07-15 04:38:32 -07:00
7176031485 Edit payment
All checks were successful
Build frontend / build (push) Successful in 52s
2026-07-13 21:51:04 +03:00
9a7e7fb5f8 Edit payment
All checks were successful
Build frontend / build (push) Successful in 53s
2026-07-13 21:04:14 +03:00
c01ada480d Edit payment with haram
All checks were successful
Build frontend / build (push) Successful in 1m31s
2026-07-13 20:20:17 +03:00
c365503382 payment with haram
Some checks failed
Build frontend / build (push) Failing after 1m39s
2026-07-11 05:21:38 -07:00
bb47b05858 Added api for add property
All checks were successful
Build frontend / build (push) Successful in 1m39s
2026-07-04 12:24:13 +03:00
3bef951d5c Added city and document type
All checks were successful
Build frontend / build (push) Successful in 54s
2026-07-04 02:53:34 +03:00
2d2e2427a2 Edit payment
All checks were successful
Build frontend / build (push) Successful in 58s
2026-07-04 02:05:13 +03:00
3b66bb53f1 Added payment types
All checks were successful
Build frontend / build (push) Successful in 1m56s
2026-07-04 01:49:04 +03:00
c7698741c7 Added translation to payments
All checks were successful
Build frontend / build (push) Successful in 1m43s
2026-07-02 10:38:55 +03:00
dab5b62fb7 Added translation to all site with edit style
All checks were successful
Build frontend / build (push) Successful in 1m40s
2026-07-01 15:43:58 +03:00
3052209df8 Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 1m5s
2026-06-30 17:17:03 +03:00
409cb41dd6 Edit style 2026-06-30 17:17:00 +03:00
3a3fa5f3fd OwnerStatistics api
All checks were successful
Build frontend / build (push) Successful in 1m41s
2026-06-30 06:55:57 -07:00
03b39317c9 Sticky top bar
All checks were successful
Build frontend / build (push) Successful in 53s
2026-06-29 01:07:50 +03:00
dc64178f94 Edit account book for owner
All checks were successful
Build frontend / build (push) Successful in 1m6s
2026-06-28 23:22:35 +03:00
253e1b7542 Edit style navbar, bottom nav
All checks were successful
Build frontend / build (push) Successful in 47s
2026-06-28 16:57:02 +03:00
d7ef0977ad Delete scroll
All checks were successful
Build frontend / build (push) Successful in 49s
2026-06-28 16:16:30 +03:00
4f58519bac Added dark and light theme
All checks were successful
Build frontend / build (push) Successful in 56s
Added navbar
2026-06-28 16:08:46 +03:00
df5801ee25 Edit properties for owner
All checks were successful
Build frontend / build (push) Successful in 57s
2026-06-28 15:16:32 +03:00
9dcbb940c6 Edit button nav
All checks were successful
Build frontend / build (push) Successful in 1m26s
Edit add properties for owner
2026-06-28 14:55:23 +03:00
12cb804b58 removing console.log
All checks were successful
Build frontend / build (push) Successful in 49s
2026-06-24 06:53:18 -07:00
5738d0369e payment api
All checks were successful
Build frontend / build (push) Successful in 56s
2026-06-24 06:36:47 -07:00
c0fdc7ef03 Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 1m30s
2026-06-24 15:04:59 +03:00
389dca56d2 Edit payments 2026-06-24 15:04:56 +03:00
6e4d629cbe re added the api url
All checks were successful
Build frontend / build (push) Successful in 48s
2026-06-24 01:32:21 +03:00
13b83e9aea removed the descption on the profile
All checks were successful
Build frontend / build (push) Successful in 49s
2026-06-24 00:15:21 +03:00
4a9380313b fixed the support and removed the send general report no need for that also fixed the client layout
All checks were successful
Build frontend / build (push) Successful in 51s
2026-06-24 00:07:09 +03:00
8d3a745cc3 fixed the api url
All checks were successful
Build frontend / build (push) Successful in 56s
2026-06-23 23:45:48 +03:00
199478f237 added the elmeent on the nav and fixed the payments transactions
All checks were successful
Build frontend / build (push) Successful in 1m27s
2026-06-23 23:32:07 +03:00
61e527fab3 removing console.log
All checks were successful
Build frontend / build (push) Successful in 54s
2026-06-17 06:46:57 -07:00
d0b626ac8c Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 1m20s
2026-06-17 06:01:32 -07:00
792bed6250 remove profile editing keep 2026-06-17 06:01:24 -07:00
bf45a48504 fixing the build
All checks were successful
Build frontend / build (push) Successful in 59s
2026-06-16 19:03:15 +03:00
eaa4206b0b fixing the build
Some checks failed
Build frontend / build (push) Failing after 50s
2026-06-16 18:58:48 +03:00
c5d9ad7b70 Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome 2026-06-16 18:58:06 +03:00
46945464ae fixing the build 2026-06-16 18:57:46 +03:00
64d422ddd8 Hide rent-type fields on buy/sell tabs and disable auto-search on tab click
Some checks failed
Build frontend / build (push) Failing after 56s
- Property type and rent period fields now only display when 'Rent' tab is active
- Adjusted grid column spans to fill layout when fields are hidden
- Tab switching no longer triggers an automatic search; search runs only on button click
2026-06-16 15:18:29 +03:00
224c29bc19 Added payments button
Some checks failed
Build frontend / build (push) Failing after 1m9s
2026-06-16 14:12:26 +03:00
f892fc4a4d Added terms with api
Some checks failed
Build frontend / build (push) Failing after 1m30s
2026-06-16 13:59:17 +03:00
01ac4f8d6c add report on reservation api
Some checks failed
Build frontend / build (push) Failing after 48s
2026-06-15 10:46:18 -07:00
f2724a5cd2 GetMyTransactions api
Some checks failed
Build frontend / build (push) Failing after 1m2s
2026-06-15 10:18:15 -07:00
bef133ad5b Fixing the map problem
Some checks failed
Build frontend / build (push) Failing after 1m1s
2026-06-14 18:58:06 +03:00
140 changed files with 15337 additions and 11530 deletions

View File

@ -6,11 +6,11 @@ on:
jobs:
build:
runs-on: ubuntu-latest
steps:
steps:
- name: Clone repository
uses: actions/checkout@v2
with:
repository: Rahaf/SweetHome
repository: amer2004/SweetHome
github-server-url: http://45.93.137.91:3000
- 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,9 +5,7 @@ import { useTranslation } from "react-i18next";
import Link from "next/link";
import Image from "next/image";
import { NavLink, MobileNavLink } from "./components/NavLinks";
import { FavoritesProvider } from '@/app/contexts/FavoritesContext';
import { NotificationsProvider } from '@/app/contexts/NotificationsContext';
import BottomNav from './components/BottomNav';
import BottomNav from "./components/BottomNav";
import {
Globe,
LogIn,
@ -18,21 +16,14 @@ import {
Building,
PlusCircle,
Heart,
MessageCircle,
Settings,
Edit,
Phone,
Mail,
MapPin,
Camera,
Bell,
Home,
ChevronDown,
Menu,
X,
TrendingUp,
CalendarDays,
Clock,
DollarSign,
Star,
FileText,
@ -44,6 +35,9 @@ import { UserRole, UserRoleLabels } from "./enums/UserRole";
import "./i18n/config";
import NotificationHandler from "./components/NotificationHandler";
import { useNotifications } from "@/app/contexts/NotificationsContext";
import Loading from "./loading";
export default function ClientLayout({ children }) {
const { t, i18n } = useTranslation();
const pathname = usePathname();
@ -55,6 +49,8 @@ export default function ClientLayout({ children }) {
const currentYear = new Date().getFullYear();
const menuRef = useRef(null);
const { unreadCount } = useNotifications();
useEffect(() => {
setIsMounted(true);
const savedLanguage = localStorage.getItem("language") || "ar";
@ -78,7 +74,7 @@ export default function ClientLayout({ children }) {
name: authUser.name || authUser.email,
email: authUser.email,
phone: authUser.phone,
role: AuthService.isOwner() ? UserRole.OWNER : UserRole.CUSTOMER,
role: AuthService.isOwner() ? UserRole.OWNER : AuthService.isAgent() ? UserRole.AGENT : UserRole.CUSTOMER,
});
} else {
setUser(null);
@ -124,17 +120,14 @@ export default function ClientLayout({ children }) {
window.location.href = "/";
};
const isAuthPage = [
"/login",
"/blocked",
"/register",
"/forgot-password",
"/auth/choose-role",
].includes(pathname);
const isAuthPage = ["/login", "/blocked", "/register", "/forgot-password", "/auth/choose-role"].includes(pathname);
const isProfilePage = pathname === "/profile";
const isHomePage = pathname === "/";
const isOwner = user?.role === UserRole.OWNER;
const isAgent = user?.role === UserRole.AGENT;
const isOwnerOrAgent = isOwner || isAgent;
const isCustomer = user?.role === UserRole.CUSTOMER;
const isAuthenticated = !!user;
@ -147,114 +140,99 @@ export default function ClientLayout({ children }) {
if (!isMounted) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
<p className="text-gray-600">جاري التحميل...</p>
</div>
</div>
<Loading/>
);
}
return (
<>
<div className="min-h-screen flex flex-col">
{!isAuthPage && !isAuthenticated && (
<nav className="fixed top-0 left-0 right-0 bg-white/95 backdrop-blur-sm border-b border-gray-200 z-50 transition-all duration-300 shadow-sm">
<nav
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${isHomePage ? "bg-transparent backdrop-blur-sm" : "bg-white/95 backdrop-blur-sm border-b border-gray-200 shadow-sm"}`}
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div
className={`flex justify-between items-center h-20 ${currentLanguage === "ar" ? "flex-row-reverse" : ""}`}
>
<div className={`flex justify-between items-center h-20 ${currentLanguage === "ar" ? "flex-row-reverse" : ""}`}>
<div className="flex items-center">
<Link href="/" className="flex items-center space-x-3 group">
<div className="relative w-[150px] h-[60px]">
<Image
src="/logo.png"
alt={t("logoAlt")}
fill
className="object-contain"
priority
/>
<Image src="/logo.png" alt={t("logoAlt")} fill className="object-contain" priority />
</div>
<span className="text-3xl font-bold text-gray-800 hidden md:block">
<span className={`text-3xl font-bold hidden md:block ${isHomePage ? "text-white" : "text-gray-800"}`}>
{t("brandNamePart1")}
<span className="text-amber-600">
{t("brandNamePart2")}
</span>
<span className={isHomePage ? "text-amber-400" : "text-amber-600"}>{t("brandNamePart2")}</span>
</span>
</Link>
</div>
<div className="hidden md:flex items-center space-x-4">
<div
className={`flex items-center space-x-1 ${currentLanguage === "ar" ? "flex-row-reverse space-x-reverse" : ""}`}
>
<div className={`flex items-center space-x-1 ${currentLanguage === "ar" ? "flex-row-reverse space-x-reverse" : ""}`}>
{/* Download App Dropdown */}
<div className="relative group">
<button className="flex items-center gap-2 px-3 py-2 text-gray-700 hover:text-amber-600 hover:bg-amber-50 rounded-lg transition-all">
<button
className={`flex items-center gap-2 px-3 py-2 rounded-lg transition-all ${isHomePage ? "text-white/80 hover:text-amber-300 hover:bg-white/10" : "text-gray-700 hover:text-amber-600 hover:bg-amber-50"}`}
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
<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="M11 2a3 3 0 0 0-3 3v6.5a.5.5 0 0 0 1 0V5a2 2 0 1 1 4 0v6.5a.5.5 0 0 0 1 0V5a3 3 0 0 0-3-3z" />
<path d="M1.5 12.5A1.5 1.5 0 0 0 3 14h10a1.5 1.5 0 0 0 0-3H3a1.5 1.5 0 0 0-1.5 1.5z" />
</svg>
<span className="text-sm font-semibold">{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>
<span className="text-sm font-semibold">تحميل التطبيق</span>
<svg className="w-4 h-4 transition-transform group-hover:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<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">
<a href="/files/SweetHome.apk" download
className="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-green-50 transition-colors">
<a href="/files/SweetHome.apk" download 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">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="#16a34a" viewBox="0 0 16 16">
<path d="M2.76 3.061a.5.5 0 0 1 .679.2l1.283 2.352A8.9 8.9 0 0 1 8 5a8.9 8.9 0 0 1 3.278.613l1.283-2.352a.5.5 0 1 1 .878.478l-1.252 2.295C14.475 7.266 16 9.477 16 12H0c0-2.523 1.525-4.734 3.813-5.966L2.56 3.74a.5.5 0 0 1 .2-.678ZM5 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2m6 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/>
<path d="M2.76 3.061a.5.5 0 0 1 .679.2l1.283 2.352A8.9 8.9 0 0 1 8 5a8.9 8.9 0 0 1 3.278.613l1.283-2.352a.5.5 0 1 1 .878.478l-1.252 2.295C14.475 7.266 16 9.477 16 12H0c0-2.523 1.525-4.734 3.813-5.966L2.56 3.74a.5.5 0 0 1 .2-.678ZM5 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2m6 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2" />
</svg>
</div>
<div>
<p className="font-semibold text-gray-900 text-sm">Android</p>
<p className="text-xs text-green-600">تحميل APK</p>
<p className="font-semibold text-gray-900 text-sm">{t("android")}</p>
<p className="text-xs text-green-600">{t("downloadAPK")}</p>
</div>
</a>
<div className="flex items-center gap-3 px-4 py-3 rounded-lg opacity-50 cursor-not-allowed">
<div className="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#6b7280" viewBox="0 0 16 16">
<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>
</div>
<div>
<p className="font-semibold text-gray-400 text-sm">iOS</p>
<p className="text-xs text-gray-400">قريباً</p>
<p className="font-semibold text-gray-400 text-sm">{t("ios")}</p>
<p className="text-xs text-gray-400">{t("comingSoon")}</p>
</div>
</div>
</div>
</div>
</div>
<NavLink href="/">الرئيسية</NavLink>
<NavLink href="/properties">عقاراتنا</NavLink>
<NavLink light={isHomePage} href="/">
{t("home")}
</NavLink>
<NavLink light={isHomePage} href="/properties">
{t("ourProperties")}
</NavLink>
{isOwner && (
<>
<NavLink href="/owner/properties">
<NavLink light={isHomePage} href="/owner/properties">
<span className="flex items-center gap-2">
<Building className="w-4 h-4" />
عقاراتي
{t("myProperties")}
</span>
</NavLink>
<NavLink href="/owner/reservations">
<NavLink light={isHomePage} href="/owner/reservations">
<span className="flex items-center gap-2">
<Calendar className="w-4 h-4" />
الحجوزات
{t("reservations")}
</span>
</NavLink>
{/* <NavLink href="/owner/calendar">
<span className="flex items-center gap-2">
<CalendarDays className="w-4 h-4" />
التقويم
</span>
</NavLink> */}
<NavLink href="/owner/profits">
<NavLink light={isHomePage} href="/owner/profits">
<span className="flex items-center gap-2">
<TrendingUp className="w-4 h-4" />
الأرباح
{t("profits")}
</span>
</NavLink>
</>
@ -262,16 +240,16 @@ export default function ClientLayout({ children }) {
{!user && !isAuthPage && (
<>
<NavLink href="/login">
<NavLink light={isHomePage} href="/login">
<span className="flex items-center gap-2">
<LogIn className="w-4 h-4" />
تسجيل الدخول
{t("login")}
</span>
</NavLink>
<NavLink href="/auth/choose-role">
<NavLink light={isHomePage} href="/auth/choose-role">
<span className="flex items-center gap-2">
<UserPlus className="w-4 h-4" />
إنشاء حساب
{t("createAccount")}
</span>
</NavLink>
</>
@ -310,49 +288,29 @@ export default function ClientLayout({ children }) {
>
<div className="bg-gradient-to-r from-amber-500 to-amber-600 p-4 text-white">
<div className="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">
{getUserInitial()}
</div>
<div className="w-12 h-12 bg-white/20 rounded-full flex items-center justify-center text-xl font-bold">{getUserInitial()}</div>
<div>
<p className="font-bold">
{user?.name || "مستخدم"}
</p>
<p className="text-xs text-amber-100">
{user?.email || ""}
</p>
<p className="text-xs text-amber-100 mt-1">
{UserRoleLabels[user?.role] || 'زائر'}
</p>
<p className="font-bold">{user?.name || t("userLabel")}</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 className="p-2">
<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)}
>
<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)}>
<UserCircle className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">الملف الشخصي</p>
<p className="text-xs text-gray-500">
عرض وتعديل معلوماتك
</p>
<p className="font-medium">{t("profile")}</p>
<p className="text-xs text-gray-500">{t("viewAndEditInfo")}</p>
</div>
</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)}
>
<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)}>
<Heart className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">المفضلة</p>
<p className="text-xs text-gray-500">
العقارات المحفوظة
</p>
<p className="font-medium">{t("favorites")}</p>
<p className="text-xs text-gray-500">{t("savedProperties")}</p>
</div>
</Link>
@ -363,38 +321,24 @@ export default function ClientLayout({ children }) {
>
<CalendarDays className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">عقاراتي المحجوزة</p>
<p className="text-xs text-gray-500">
عرض وتقييم العقارات المحجوزة
</p>
<p className="font-medium">{t("myBookedProperties")}</p>
<p className="text-xs text-gray-500">{t("viewAndRateBooked")}</p>
</div>
</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)}
>
<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)}>
<Star className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">تقييماتي</p>
<p className="text-xs text-gray-500">
التقييمات التي تلقيتها
</p>
<p className="font-medium">{t("myRatings")}</p>
<p className="text-xs text-gray-500">{t("ratingsReceived")}</p>
</div>
</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)}
>
<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)}>
<Settings className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">الإعدادات</p>
<p className="text-xs text-gray-500">
إعدادات الحساب والأمان
</p>
<p className="font-medium">{t("settings")}</p>
<p className="text-xs text-gray-500">{t("accountAndSecurity")}</p>
</div>
</Link>
@ -409,10 +353,8 @@ export default function ClientLayout({ children }) {
>
<Building className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">عقاراتي</p>
<p className="text-xs text-gray-500">
إدارة عقاراتك
</p>
<p className="font-medium">{t("myProperties")}</p>
<p className="text-xs text-gray-500">{t("manageYourProperties")}</p>
</div>
</Link>
@ -423,10 +365,8 @@ export default function ClientLayout({ children }) {
>
<PlusCircle className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">إضافة عقار</p>
<p className="text-xs text-gray-500">
أضف عقاراً جديداً
</p>
<p className="font-medium">{t("addProperty")}</p>
<p className="text-xs text-gray-500">{t("addNewProperty")}</p>
</div>
</Link>
@ -437,10 +377,8 @@ export default function ClientLayout({ children }) {
>
<Calendar className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">الحجوزات</p>
<p className="text-xs text-gray-500">
إدارة حجوزاتك
</p>
<p className="font-medium">{t("reservations")}</p>
<p className="text-xs text-gray-500">{t("manageReservations")}</p>
</div>
</Link>
@ -451,10 +389,8 @@ export default function ClientLayout({ children }) {
>
<CalendarDays className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">التقويم</p>
<p className="text-xs text-gray-500">
جدول توفر العقارات
</p>
<p className="font-medium">{t("calendar")}</p>
<p className="text-xs text-gray-500">{t("propertyAvailability")}</p>
</div>
</Link>
@ -465,10 +401,8 @@ export default function ClientLayout({ children }) {
>
<TrendingUp className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">الأرباح</p>
<p className="text-xs text-gray-500">
إحصائيات وأرباح
</p>
<p className="font-medium">{t("profits")}</p>
<p className="text-xs text-gray-500">{t("statisticsAndProfits")}</p>
</div>
</Link>
@ -479,10 +413,8 @@ export default function ClientLayout({ children }) {
>
<DollarSign className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">دفتر الحسابات</p>
<p className="text-xs text-gray-500">
سجل المعاملات المالية
</p>
<p className="font-medium">{t("accountBook")}</p>
<p className="text-xs text-gray-500">{t("financialRecords")}</p>
</div>
</Link>
</>
@ -499,10 +431,8 @@ export default function ClientLayout({ children }) {
>
<Calendar className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">حجوزاتي</p>
<p className="text-xs text-gray-500">
عرض حجوزاتك
</p>
<p className="font-medium">{t("myBookings")}</p>
<p className="text-xs text-gray-500">{t("viewBookings")}</p>
</div>
</Link>
@ -513,10 +443,8 @@ export default function ClientLayout({ children }) {
>
<CalendarDays className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">عقاراتي المحجوزة</p>
<p className="text-xs text-gray-500">
عرض وتقييم العقارات المحجوزة
</p>
<p className="font-medium">{t("myBookedProperties")}</p>
<p className="text-xs text-gray-500">{t("viewAndRateBooked")}</p>
</div>
</Link>
@ -527,24 +455,16 @@ export default function ClientLayout({ children }) {
>
<Star className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">تقييماتي</p>
<p className="text-xs text-gray-500">
التقييمات التي تلقيتها
</p>
<p className="font-medium">{t("myRatings")}</p>
<p className="text-xs text-gray-500">{t("ratingsReceived")}</p>
</div>
</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)}
>
<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)}>
<FileText className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">البلاغات</p>
<p className="text-xs text-gray-500">
تقديم ومتابعة البلاغات
</p>
<p className="font-medium">{t("reports")}</p>
<p className="text-xs text-gray-500">{t("submitAndTrackReports")}</p>
</div>
</Link>
</>
@ -552,16 +472,11 @@ export default function ClientLayout({ children }) {
<div className="border-t border-gray-100 my-2"></div>
<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"
>
<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">
<LogOut className="w-5 h-5" />
<div>
<p className="font-medium">تسجيل الخروج</p>
<p className="text-xs text-red-400">
إنهاء الجلسة الحالية
</p>
<p className="font-medium">{t("logout")}</p>
<p className="text-xs text-red-400">{t("endSession")}</p>
</div>
</button>
</div>
@ -576,9 +491,7 @@ export default function ClientLayout({ children }) {
<motion.button
whileHover={{ scale: 1.1, rotate: 360 }}
whileTap={{ scale: 0.9 }}
onClick={() =>
changeLanguage(currentLanguage === "en" ? "ar" : "en")
}
onClick={() => 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"
>
<Globe className="w-5 h-5 text-gray-700" />
@ -599,11 +512,7 @@ export default function ClientLayout({ children }) {
className="inline-flex items-center justify-center p-2 rounded-md text-gray-700 hover:text-amber-600 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-amber-500"
>
<span className="sr-only">{t("openMainMenu")}</span>
{isMobileMenuOpen ? (
<X className="w-6 h-6" />
) : (
<Menu className="w-6 h-6" />
)}
{isMobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
</button>
</div>
</div>
@ -614,70 +523,66 @@ export default function ClientLayout({ children }) {
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="md:hidden bg-white border-t border-gray-200 shadow-lg"
className={`md:hidden border-t shadow-lg ${isHomePage ? "bg-black/60 backdrop-blur-md border-white/10" : "bg-white border-gray-200"}`}
>
<div className="px-2 pt-2 pb-3 space-y-1">
<MobileNavLink href="/" onClick={closeMobileMenu}>
<MobileNavLink light={isHomePage} href="/" onClick={closeMobileMenu}>
{t("home")}
</MobileNavLink>
<MobileNavLink href="/properties" onClick={closeMobileMenu}>
<MobileNavLink light={isHomePage} href="/properties" onClick={closeMobileMenu}>
{t("ourProducts")}
</MobileNavLink>
{/* Download App - Mobile */}
<div className="border-t border-gray-200 my-2"></div>
<p className="px-3 py-1 text-xs text-gray-400 font-medium">تحميل التطبيق</p>
<a href="/files/SweetHome.apk" download onClick={closeMobileMenu}
className="flex items-center gap-2 px-3 py-2 rounded-md text-green-600 hover:bg-green-50 transition-colors">
<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>
<a
href="/files/SweetHome.apk"
download
onClick={closeMobileMenu}
className={`flex items-center gap-2 px-3 py-2 rounded-md transition-colors ${isHomePage ? "text-green-400 hover:bg-white/10" : "text-green-600 hover:bg-green-50"}`}
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
<path d="M2.76 3.061a.5.5 0 0 1 .679.2l1.283 2.352A8.9 8.9 0 0 1 8 5a8.9 8.9 0 0 1 3.278.613l1.283-2.352a.5.5 0 1 1 .878.478l-1.252 2.295C14.475 7.266 16 9.477 16 12H0c0-2.523 1.525-4.734 3.813-5.966L2.56 3.74a.5.5 0 0 1 .2-.678ZM5 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2m6 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/>
<path d="M2.76 3.061a.5.5 0 0 1 .679.2l1.283 2.352A8.9 8.9 0 0 1 8 5a8.9 8.9 0 0 1 3.278.613l1.283-2.352a.5.5 0 1 1 .878.478l-1.252 2.295C14.475 7.266 16 9.477 16 12H0c0-2.523 1.525-4.734 3.813-5.966L2.56 3.74a.5.5 0 0 1 .2-.678ZM5 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2m6 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2" />
</svg>
<span className="text-sm font-medium">Android - تحميل APK</span>
<span className="text-sm font-medium">
{t("android")} - {t("downloadAPK")}
</span>
</a>
<div className="flex items-center gap-2 px-3 py-2 rounded-md text-gray-400 cursor-not-allowed opacity-50">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M11.182.008C11.148-.03 9.923.023 8.857 1.18c-1.066 1.156-.902 2.482-.878 2.516.024.034 1.52.087 2.475-1.258.955-1.345.762-2.391.728-2.43Zm3.314 11.733c-.048-.096-2.325-1.234-2.113-3.422.212-2.189 1.675-2.789 1.698-2.854.023-.065-.597-.79-1.254-1.157a3.692 3.692 0 0 0-1.563-.434c-.108-.003-.483-.095-1.254.116-.508.139-1.653.589-1.968.607-.316.018-1.256-.522-2.267-.665-.647-.125-1.333.131-1.824.328-.49.196-1.422.754-2.074 2.237-.652 1.482-.311 3.83-.067 4.56.244.729.625 1.924 1.273 2.796.576.984 1.34 1.667 1.659 1.899.319.232 1.219.385 1.843.067.502-.308 1.408-.485 1.766-.472.357.013 1.061.154 1.782.539.571.197 1.111.115 1.652-.105.541-.221 1.324-1.059 2.238-2.758.347-.79.505-1.217.473-1.282Z"/>
<path d="M11.182.008C11.148-.03 9.923.023 8.857 1.18c-1.066 1.156-.902 2.482-.878 2.516.024.034 1.52.087 2.475-1.258.955-1.345.762-2.391.728-2.43Zm3.314 11.733c-.048-.096-2.325-1.234-2.113-3.422.212-2.189 1.675-2.789 1.698-2.854.023-.065-.597-.79-1.254-1.157a3.692 3.692 0 0 0-1.563-.434c-.108-.003-.483-.095-1.254.116-.508.139-1.653.589-1.968.607-.316.018-1.256-.522-2.267-.665-.647-.125-1.333.131-1.824.328-.49.196-1.422.754-2.074 2.237-.652 1.482-.311 3.83-.067 4.56.244.729.625 1.924 1.273 2.796.576.984 1.34 1.667 1.659 1.899.319.232 1.219.385 1.843.067.502-.308 1.408-.485 1.766-.472.357.013 1.061.154 1.782.539.571.197 1.111.115 1.652-.105.541-.221 1.324-1.059 2.238-2.758.347-.79.505-1.217.473-1.282Z" />
</svg>
<span className="text-sm">iOS - قريباً</span>
<span className="text-sm">
{t("ios")} - {t("comingSoon")}
</span>
</div>
<div className="border-t border-gray-200 my-2"></div>
<div className={`border-t my-2 ${isHomePage ? "border-white/10" : "border-gray-200"}`}></div>
{isOwner && (
<>
<MobileNavLink
href="/owner/properties"
onClick={closeMobileMenu}
>
<MobileNavLink light={isHomePage} href="/owner/properties" onClick={closeMobileMenu}>
<span className="flex items-center gap-2">
<Building className="w-4 h-4" />
عقاراتي
{t("myProperties")}
</span>
</MobileNavLink>
<MobileNavLink
href="/owner/reservations"
onClick={closeMobileMenu}
>
<MobileNavLink light={isHomePage} href="/owner/reservations" onClick={closeMobileMenu}>
<span className="flex items-center gap-2">
<Calendar className="w-4 h-4" />
الحجوزات
{t("reservations")}
</span>
</MobileNavLink>
<MobileNavLink
href="/owner/calendar"
onClick={closeMobileMenu}
>
<MobileNavLink light={isHomePage} href="/owner/calendar" onClick={closeMobileMenu}>
<span className="flex items-center gap-2">
<CalendarDays className="w-4 h-4" />
التقويم
{t("calendar")}
</span>
</MobileNavLink>
<MobileNavLink
href="/owner/profits"
onClick={closeMobileMenu}
>
<MobileNavLink light={isHomePage} href="/owner/profits" onClick={closeMobileMenu}>
<span className="flex items-center gap-2">
<TrendingUp className="w-4 h-4" />
الأرباح
{t("profits")}
</span>
</MobileNavLink>
</>
@ -685,20 +590,17 @@ export default function ClientLayout({ children }) {
{!user && (
<>
<div className="border-t border-gray-200 my-2"></div>
<MobileNavLink href="/login" onClick={closeMobileMenu}>
<div className={`border-t my-2 ${isHomePage ? "border-white/10" : "border-gray-200"}`}></div>
<MobileNavLink light={isHomePage} href="/login" onClick={closeMobileMenu}>
<span className="flex items-center gap-2">
<LogIn className="w-4 h-4" />
تسجيل الدخول
{t("login")}
</span>
</MobileNavLink>
<MobileNavLink
href="/auth/choose-role"
onClick={closeMobileMenu}
>
<MobileNavLink light={isHomePage} href="/auth/choose-role" onClick={closeMobileMenu}>
<span className="flex items-center gap-2">
<UserPlus className="w-4 h-4" />
إنشاء حساب
{t("createAccount")}
</span>
</MobileNavLink>
</>
@ -709,68 +611,105 @@ export default function ClientLayout({ children }) {
</nav>
)}
<NotificationsProvider>
<FavoritesProvider>
<main
className={`${!isAuthPage && !isProfilePage && !isAuthenticated ? "pt-20" : ""} min-h-screen bg-gradient-to-b from-gray-50 to-white ${currentLanguage === "ar" ? "text-right" : "text-left"}`}
>
{children}
</main>
{isHomePage && (
<div className="fixed inset-0 -z-10">
<motion.div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{ backgroundImage: "url(/hero.jpg)" }}
initial={{ scale: 1.1 }}
animate={{ scale: 1 }}
transition={{ duration: 1.5, ease: "easeOut" }}
/>
<div className="absolute inset-0 bg-gradient-to-r from-black/70 via-black/60 to-black/50" />
</div>
)}
{isAuthenticated && !isAuthPage && (
<BottomNav isOwner={isOwner} />
)}
</FavoritesProvider>
</NotificationsProvider>
<main
className={`${!isAuthPage && !isProfilePage && !isAuthenticated ? "pt-20" : ""} flex-1 flex flex-col ${isHomePage ? "bg-transparent" : "bg-gray-50"} ${currentLanguage === "ar" ? "text-right" : "text-left"} ${isHomePage ? "relative" : ""} ${isAuthenticated && !isAuthPage ? "pb-20" : ""}`}
>
{!isAuthPage && isAuthenticated && (
<div className={`flex items-center justify-between px-4 py-2 sticky top-0 z-20 bg-transparent backdrop-blur-3xl ${isHomePage ? "" : "border-b border-gray-200/20"}`}>
<div className="flex items-center gap-2">
<span
className={`text-sm font-semibold px-3 py-1 rounded-lg ${!isAuthenticated ? (isHomePage ? "bg-white/20 text-white" : "bg-gray-200 text-gray-600") : isOwner ? (isHomePage ? "bg-amber-500/30 text-amber-200" : "bg-amber-100 text-amber-700") : isHomePage ? "bg-blue-500/30 text-blue-200" : "bg-blue-100 text-blue-700"}`}
>
{!isAuthenticated ? t("visitor") : isOwner ? t("owner") : t("customer")}
</span>
</div>
<div className="flex items-center gap-3">
<Link
href="/favorites"
className={`relative group flex items-center justify-center w-9 h-9 rounded-full transition-colors hover:bg-white/10 ${pathname === "/favorites" ? "text-amber-400" : "nav-icon"}`}
>
<Heart className="w-5 h-5" />
<div className="absolute -bottom-8 left-1/2 -translate-x-1/2 bg-gray-800 text-white text-xs rounded-lg px-2 py-1 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none shadow-lg">
{t("favorites")}
</div>
</Link>
<Link
href="/notifications"
className={`relative group flex items-center justify-center w-9 h-9 rounded-full transition-colors hover:bg-white/10 ${pathname === "/notifications" ? "text-amber-400" : "nav-icon"}`}
>
<Bell className="w-5 h-5" />
{isMounted && unreadCount > 0 && (
<div className="absolute -top-1 -right-1 bg-red-500 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center">{unreadCount > 9 ? "9+" : unreadCount}</div>
)}
<div className="absolute -bottom-8 left-1/2 -translate-x-1/2 bg-gray-800 text-white text-xs rounded-lg px-2 py-1 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none shadow-lg">
{t("notifications")}
</div>
</Link>
<Link
href="/settings"
className={`relative group flex items-center justify-center w-9 h-9 rounded-full transition-colors hover:bg-white/10 ${pathname === "/settings" ? "text-amber-400" : "nav-icon"}`}
>
<Settings className="w-5 h-5" />
<div className="absolute -bottom-8 left-1/2 -translate-x-1/2 bg-gray-800 text-white text-xs rounded-lg px-2 py-1 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none shadow-lg">
{t("settings")}
</div>
</Link>
<button
onClick={() => changeLanguage(currentLanguage === "ar" ? "en" : "ar")}
className="relative group flex items-center justify-center w-9 h-9 rounded-full transition-colors hover:bg-white/10 nav-icon"
>
<Globe className="w-5 h-5" />
<div className="absolute -bottom-8 left-1/2 -translate-x-1/2 bg-gray-800 text-white text-xs rounded-lg px-2 py-1 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none shadow-lg">
{currentLanguage === "ar" ? t("switchToEnglish") : t("switchToArabic")}
</div>
</button>
</div>
</div>
)}
<div className="flex-1">{children}</div>
{isAuthenticated && !isAuthPage && <BottomNav isOwner={isOwner} isOwnerOrAgent={isOwnerOrAgent} />}
</main>
{pathname === "/" && (
<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={`grid grid-cols-1 md:grid-cols-4 gap-8 ${currentLanguage === "ar" ? "text-right" : "text-left"}`}
>
<div className={`grid grid-cols-1 md:grid-cols-4 gap-8 ${currentLanguage === "ar" ? "text-right" : "text-left"}`}>
<div className="space-y-4">
<div
className={`flex items-center ${currentLanguage === "ar" ? "flex-row-reverse" : "space-x-3"}`}
>
<div className={`flex items-center ${currentLanguage === "ar" ? "flex-row-reverse" : "space-x-3"}`}>
<div className="relative w-10 h-10">
<Image
src="/logo.png"
alt={t("logoAlt")}
fill
className="object-contain"
sizes="40px"
/>
<Image src="/logo.png" alt={t("logoAlt")} fill className="object-contain" sizes="40px" />
</div>
<span className="text-3xl font-bold">
{t("brandNamePart1")}
<span className="text-amber-400">
{t("brandNamePart2")}
</span>
<span className="text-amber-400">{t("brandNamePart2")}</span>
</span>
</div>
<p className="text-gray-400 text-sm">
{t("footerDescription")}
</p>
<p className="text-gray-400 text-sm">{t("footerDescription")}</p>
</div>
<div>
<h3 className="text-lg font-semibold mb-4">
{t("quickLinks")}
</h3>
<h3 className="text-lg font-semibold mb-4">{t("quickLinks")}</h3>
<ul className="space-y-2">
<li>
<Link
href="/"
className="text-gray-400 hover:text-white transition-colors block py-1"
>
<Link href="/" className="text-gray-400 hover:text-white transition-colors block py-1">
{t("home")}
</Link>
</li>
<li>
<Link
href="/properties"
className="text-gray-400 hover:text-white transition-colors block py-1"
>
<Link href="/properties" className="text-gray-400 hover:text-white transition-colors block py-1">
{t("ourProducts")}
</Link>
</li>
@ -781,7 +720,9 @@ export default function ClientLayout({ children }) {
<ul className="space-y-3 text-gray-400">
<li className="flex items-center gap-2">
<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 className="flex items-center gap-2">
<Mail className="w-5 h-5" />
@ -799,6 +740,6 @@ export default function ClientLayout({ children }) {
</footer>
)}
<NotificationHandler />
</>
</div>
);
}

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,31 +1,24 @@
'use client';
"use client";
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { motion } from 'framer-motion';
import {
Mail,
Phone,
Shield,
ChevronLeft,
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';
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { motion } from "framer-motion";
import { useTranslation } from "react-i18next";
import { Mail, Phone, Shield, ChevronLeft, 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";
import InteractiveBackground from "../components/Animation/Background";
export default function AccountVerificationPage() {
const router = useRouter();
const { t } = useTranslation();
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [emailCode, setEmailCode] = useState('');
const [phoneCode, setPhoneCode] = useState('');
const [emailCode, setEmailCode] = useState("");
const [phoneCode, setPhoneCode] = useState("");
const [sendingEmailOTP, setSendingEmailOTP] = useState(false);
const [sendingPhoneOTP, setSendingPhoneOTP] = useState(false);
const [verifyingEmail, setVerifyingEmail] = useState(false);
@ -41,7 +34,7 @@ export default function AccountVerificationPage() {
setUser(authUser);
setIsLoading(false);
} else {
router.push('/login');
router.push("/login");
}
}, [router]);
@ -49,10 +42,10 @@ export default function AccountVerificationPage() {
setSendingEmailOTP(true);
try {
await sendEmailOTP();
toast.success('تم إرسال رمز التحقق إلى بريدك الإلكتروني');
toast.success(t("accountVerification.emailOtpSent"));
setShowEmailInput(true);
} catch (err) {
toast.error(err.message || 'فشل إرسال رمز التحقق');
toast.error(err.message || t("accountVerification.otpSendFailed"));
} finally {
setSendingEmailOTP(false);
}
@ -60,7 +53,7 @@ export default function AccountVerificationPage() {
const handleVerifyEmail = async () => {
if (!emailCode.trim()) {
toast.error('الرجاء إدخال رمز التحقق');
toast.error(t("accountVerification.otpRequired"));
return;
}
setVerifyingEmail(true);
@ -68,9 +61,9 @@ export default function AccountVerificationPage() {
await verifyEmail(emailCode.trim());
setEmailVerified(true);
setShowEmailInput(false);
toast.success('تم التحقق من البريد الإلكتروني بنجاح');
toast.success(t("accountVerification.emailVerifiedSuccess"));
} catch (err) {
toast.error(err.message || 'رمز التحقق غير صحيح');
toast.error(err.message || t("accountVerification.otpInvalid"));
} finally {
setVerifyingEmail(false);
}
@ -80,10 +73,10 @@ export default function AccountVerificationPage() {
setSendingPhoneOTP(true);
try {
await sendPhoneOTP();
toast.success('تم إرسال رمز التحقق إلى هاتفك');
toast.success(t("accountVerification.phoneOtpSent"));
setShowPhoneInput(true);
} catch (err) {
toast.error(err.message || 'فشل إرسال رمز التحقق');
toast.error(err.message || t("accountVerification.otpSendFailed"));
} finally {
setSendingPhoneOTP(false);
}
@ -91,7 +84,7 @@ export default function AccountVerificationPage() {
const handleVerifyPhone = async () => {
if (!phoneCode.trim()) {
toast.error('الرجاء إدخال رمز التحقق');
toast.error(t("accountVerification.otpRequired"));
return;
}
setVerifyingPhone(true);
@ -99,80 +92,56 @@ export default function AccountVerificationPage() {
await verifyPhone(phoneCode.trim());
setPhoneVerified(true);
setShowPhoneInput(false);
toast.success('تم التحقق من رقم الهاتف بنجاح');
toast.success(t("accountVerification.phoneVerifiedSuccess"));
} catch (err) {
toast.error(err.message || 'رمز التحقق غير صحيح');
toast.error(err.message || t("accountVerification.otpInvalid"));
} finally {
setVerifyingPhone(false);
}
};
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 = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.1 }
}
transition: { staggerChildren: 0.1 },
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 }
visible: { opacity: 1, y: 0 },
};
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} />
<div className="container mx-auto px-4 max-w-2xl">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="flex items-center gap-3 mb-8"
>
<Link
href="/settings"
className="p-2 rounded-xl hover:bg-gray-200 transition-colors text-gray-600"
>
<InteractiveBackground />
<div className="relative z-10 container mx-auto px-4 max-w-2xl">
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="flex items-center gap-3 mb-8">
<Link href="/settings" className="p-2 rounded-xl hover:bg-gray-200 transition-colors text-gray-600">
<ChevronLeft className="w-5 h-5" />
</Link>
<h1 className="text-2xl font-bold text-gray-900">التحقق من الحساب</h1>
<h1 className="text-2xl font-bold text-gray-900">{t("accountVerification.title")}</h1>
</motion.div>
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="space-y-6"
>
<motion.div variants={containerVariants} initial="hidden" animate="visible" className="space-y-6">
<motion.div variants={itemVariants} className="bg-white rounded-2xl shadow-sm overflow-hidden">
<div className="px-6 py-4 border-b border-gray-100 flex items-center gap-3">
<Mail className="w-5 h-5 text-amber-600" />
<h2 className="text-lg font-semibold text-gray-800">البريد الإلكتروني</h2>
<h2 className="text-lg font-semibold text-gray-800">{t("accountVerification.emailSection")}</h2>
{emailVerified && (
<span className="flex items-center gap-1 text-xs text-green-600 bg-green-50 px-2 py-0.5 rounded-full">
<CheckCircle className="w-3 h-3" />
موثق
{t("accountVerification.verified")}
</span>
)}
</div>
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<div>
<p className="text-sm text-gray-600">{user?.email || 'بريد إلكتروني غير مسجل'}</p>
<p className="text-xs text-gray-400 mt-1">
{emailVerified
? 'بريدك الإلكتروني موثق ويمكنك استخدامه لتسجيل الدخول'
: 'يرجى توثيق بريدك الإلكتروني لحماية حسابك'}
</p>
<p className="text-sm text-gray-600">{user?.email || t("accountVerification.emailNotRegistered")}</p>
<p className="text-xs text-gray-400 mt-1">{emailVerified ? t("accountVerification.emailVerifiedDesc") : t("accountVerification.emailVerifyDesc")}</p>
</div>
{!emailVerified && !showEmailInput && (
<button
@ -180,29 +149,21 @@ export default function AccountVerificationPage() {
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"
>
{sendingEmailOTP ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Send className="w-4 h-4" />
)}
{sendingEmailOTP ? 'جاري الإرسال...' : 'إرسال رمز التحقق'}
{sendingEmailOTP ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />}
{sendingEmailOTP ? t("accountVerification.sending") : t("accountVerification.sendOtp")}
</button>
)}
</div>
{showEmailInput && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
className="space-y-3"
>
<p className="text-xs text-gray-500">أدخل الرمز المكون من 6 أرقام الذي تم إرساله إلى بريدك الإلكتروني</p>
<motion.div initial={{ opacity: 0, height: 0 }} 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">
<input
type="text"
value={emailCode}
onChange={(e) => setEmailCode(e.target.value)}
placeholder="رمز التحقق"
placeholder={t("accountVerification.otpPlaceholder")}
maxLength={6}
className="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none text-center text-lg tracking-widest"
/>
@ -211,20 +172,12 @@ export default function AccountVerificationPage() {
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"
>
{verifyingEmail ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Key className="w-4 h-4" />
)}
تحقق
{verifyingEmail ? <Loader2 className="w-4 h-4 animate-spin" /> : <Key className="w-4 h-4" />}
{t("accountVerification.verify")}
</button>
</div>
<button
onClick={handleSendEmailOTP}
disabled={sendingEmailOTP}
className="text-xs text-amber-600 hover:text-amber-700"
>
إعادة إرسال الرمز
<button onClick={handleSendEmailOTP} disabled={sendingEmailOTP} className="text-xs text-amber-600 hover:text-amber-700">
{t("accountVerification.resendCode")}
</button>
</motion.div>
)}
@ -232,7 +185,7 @@ export default function AccountVerificationPage() {
{emailVerified && (
<div className="flex items-center gap-2 text-green-600 bg-green-50 px-4 py-3 rounded-xl">
<CheckCircle className="w-5 h-5" />
<span className="text-sm font-medium">تم توثيق البريد الإلكتروني بنجاح</span>
<span className="text-sm font-medium">{t("accountVerification.emailVerifiedSuccess")}</span>
</div>
)}
</div>
@ -241,23 +194,19 @@ export default function AccountVerificationPage() {
<motion.div variants={itemVariants} className="bg-white rounded-2xl shadow-sm overflow-hidden">
<div className="px-6 py-4 border-b border-gray-100 flex items-center gap-3">
<Phone className="w-5 h-5 text-amber-600" />
<h2 className="text-lg font-semibold text-gray-800">رقم الهاتف</h2>
<h2 className="text-lg font-semibold text-gray-800">{t("accountVerification.phoneSection")}</h2>
{phoneVerified && (
<span className="flex items-center gap-1 text-xs text-green-600 bg-green-50 px-2 py-0.5 rounded-full">
<CheckCircle className="w-3 h-3" />
موثق
{t("accountVerification.verified")}
</span>
)}
</div>
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<div>
<p className="text-sm text-gray-600">{user?.phone || 'رقم هاتف غير مسجل'}</p>
<p className="text-xs text-gray-400 mt-1">
{phoneVerified
? 'رقم هاتفك موثق ويمكنك استخدامه لتسجيل الدخول'
: 'يرجى توثيق رقم هاتفك لحماية حسابك'}
</p>
<p className="text-sm text-gray-600">{user?.phone || t("accountVerification.phoneNotRegistered")}</p>
<p className="text-xs text-gray-400 mt-1">{phoneVerified ? t("accountVerification.phoneVerifiedDesc") : t("accountVerification.phoneVerifyDesc")}</p>
</div>
{!phoneVerified && !showPhoneInput && (
<button
@ -265,29 +214,21 @@ export default function AccountVerificationPage() {
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"
>
{sendingPhoneOTP ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Send className="w-4 h-4" />
)}
{sendingPhoneOTP ? 'جاري الإرسال...' : 'إرسال رمز التحقق'}
{sendingPhoneOTP ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />}
{sendingPhoneOTP ? t("accountVerification.sending") : t("accountVerification.sendOtp")}
</button>
)}
</div>
{showPhoneInput && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
className="space-y-3"
>
<p className="text-xs text-gray-500">أدخل الرمز المكون من 6 أرقام الذي تم إرساله إلى هاتفك</p>
<motion.div initial={{ opacity: 0, height: 0 }} 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">
<input
type="text"
value={phoneCode}
onChange={(e) => setPhoneCode(e.target.value)}
placeholder="رمز التحقق"
placeholder={t("accountVerification.otpPlaceholder")}
maxLength={6}
className="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none text-center text-lg tracking-widest"
/>
@ -296,20 +237,12 @@ export default function AccountVerificationPage() {
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"
>
{verifyingPhone ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Key className="w-4 h-4" />
)}
تحقق
{verifyingPhone ? <Loader2 className="w-4 h-4 animate-spin" /> : <Key className="w-4 h-4" />}
{t("accountVerification.verify")}
</button>
</div>
<button
onClick={handleSendPhoneOTP}
disabled={sendingPhoneOTP}
className="text-xs text-amber-600 hover:text-amber-700"
>
إعادة إرسال الرمز
<button onClick={handleSendPhoneOTP} disabled={sendingPhoneOTP} className="text-xs text-amber-600 hover:text-amber-700">
{t("accountVerification.resendCode")}
</button>
</motion.div>
)}
@ -317,7 +250,7 @@ export default function AccountVerificationPage() {
{phoneVerified && (
<div className="flex items-center gap-2 text-green-600 bg-green-50 px-4 py-3 rounded-xl">
<CheckCircle className="w-5 h-5" />
<span className="text-sm font-medium">تم توثيق رقم الهاتف بنجاح</span>
<span className="text-sm font-medium">{t("accountVerification.phoneVerifiedSuccess")}</span>
</div>
)}
</div>
@ -326,20 +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">
<div className="flex items-center gap-3 mb-3">
<Shield className="w-6 h-6" />
<h3 className="text-lg font-semibold">لماذا يجب توثيق حسابك؟</h3>
<h3 className="text-lg font-semibold">{t("accountVerification.whyVerify")}</h3>
</div>
<ul className="space-y-2 text-sm text-amber-50">
<ul className="space-y-2 text-sm " style={{ color: "#0f172a", fontWeight: "400" }}>
<li className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 flex-shrink-0" />
حماية حسابك من الوصول غير المصرح به
{t("accountVerification.reason1")}
</li>
<li className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 flex-shrink-0" />
استعادة كلمة المرور بسهولة في حال فقدانها
{t("accountVerification.reason2")}
</li>
<li className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 flex-shrink-0" />
زيادة الثقة مع مالكي العقارات والمستأجرين
{t("accountVerification.reason3")}
</li>
</ul>
</motion.div>

View File

@ -6,7 +6,7 @@ import Link from 'next/link';
export default function Error({ error, reset }) {
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">
<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" />

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

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

View File

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

View File

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

View File

@ -1,16 +1,19 @@
'use client';
"use client";
import { useState } from 'react';
import { motion } from 'framer-motion';
import { useRouter } from 'next/navigation';
import toast, { Toaster } from 'react-hot-toast';
import { Lock, Eye, EyeOff, ArrowLeft, Shield } from 'lucide-react';
import { changePassword } from '../utils/api';
import AuthService from '../services/AuthService';
import { useState } from "react";
import { motion } from "framer-motion";
import { useRouter } from "next/navigation";
import toast, { Toaster } from "react-hot-toast";
import { Lock, Eye, EyeOff, ArrowLeft, Shield } from "lucide-react";
import { useTranslation } from "react-i18next";
import { changePassword } from "../utils/api";
import AuthService from "../services/AuthService";
import InteractiveBackground from "../components/Animation/Background";
export default function ChangePasswordPage() {
const router = useRouter();
const [form, setForm] = useState({ oldPassword: '', newPassword: '', confirmPassword: '' });
const { t } = useTranslation();
const [form, setForm] = useState({ oldPassword: "", newPassword: "", confirmPassword: "" });
const [show, setShow] = useState({ old: false, new: false, confirm: false });
const [isLoading, setIsLoading] = useState(false);
@ -22,162 +25,124 @@ export default function ChangePasswordPage() {
e.preventDefault();
if (!AuthService.isAuthenticated()) {
toast.error('يرجى تسجيل الدخول أولاً');
router.push('/login');
toast.error(t("changePassword.loginRequired"));
router.push("/login");
return;
}
if (form.newPassword !== form.confirmPassword) {
toast.error('كلمة المرور الجديدة وتأكيدها غير متطابقتين');
toast.error(t("changePassword.passwordsDoNotMatch"));
return;
}
if (form.newPassword.length < 6) {
toast.error('كلمة المرور الجديدة يجب أن تكون 6 أحرف على الأقل');
toast.error(t("validation.passwordMinLength"));
return;
}
setIsLoading(true);
try {
await changePassword(form.oldPassword, form.newPassword);
toast.success('تم تغيير كلمة المرور بنجاح');
setTimeout(() => router.push('/profile'), 1200);
toast.success(t("changePassword.changeSuccess"));
setTimeout(() => router.push("/profile"), 1200);
} catch (err) {
toast.error(err?.message || 'فشل تغيير كلمة المرور');
toast.error(err?.message || t("changePassword.changeFailed"));
} finally {
setIsLoading(false);
}
};
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 (
<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} />
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
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"
>
<InteractiveBackground />
<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 ">
<motion.div initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} className="mb-6 relative z-10">
<button onClick={() => router.push("/profile")} className="flex items-center relative z-10 gap-2 text-gray-600 hover:text-amber-600 transition-colors">
<ArrowLeft className="w-5 h-5" />
<span>العودة للملف الشخصي</span>
<span>{t("changePassword.backToProfile")}</span>
</button>
</motion.div>
<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">
<motion.div
initial={{ scale: 0 }}
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"
>
<Shield className="w-8 h-8 text-white" />
</motion.div>
<motion.h1
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
className="text-3xl font-bold text-white mb-2"
>
تغيير كلمة المرور
<motion.h1 initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} className="text-3xl font-bold text-white mb-2">
{t("changePassword.title")}
</motion.h1>
<motion.p
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.1 }}
className="text-amber-100"
>
أدخل كلمة المرور الحالية والجديدة
<motion.p initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} transition={{ delay: 0.1 }} className="text-amber-100">
{t("changePassword.subtitle")}
</motion.p>
</div>
<form onSubmit={handleSubmit} className="p-8 space-y-6">
<form onSubmit={handleSubmit} className="p-8 space-y-6 " style={{ backgroundColor: "#0f172a" }}>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
كلمة المرور الحالية
</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{t("changePassword.currentPasswordLabel")}</label>
<div className="relative">
<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" />
</div>
<input
type={show.old ? 'text' : 'password'}
type={show.old ? "text" : "password"}
value={form.oldPassword}
onChange={handleChange('oldPassword')}
onChange={handleChange("oldPassword")}
className={inputClass}
placeholder="أدخل كلمة المرور الحالية"
placeholder={t("changePassword.currentPasswordPlaceholder")}
required
/>
<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"
>
<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">
{show.old ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
كلمة المرور الجديدة
</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{t("changePassword.newPasswordLabel")}</label>
<div className="relative">
<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" />
</div>
<input
type={show.new ? 'text' : 'password'}
type={show.new ? "text" : "password"}
value={form.newPassword}
onChange={handleChange('newPassword')}
onChange={handleChange("newPassword")}
className={inputClass}
placeholder="أدخل كلمة المرور الجديدة"
placeholder={t("changePassword.newPasswordPlaceholder")}
required
minLength={6}
/>
<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"
>
<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">
{show.new ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
تأكيد كلمة المرور الجديدة
</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{t("changePassword.confirmNewPasswordLabel")}</label>
<div className="relative">
<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" />
</div>
<input
type={show.confirm ? 'text' : 'password'}
type={show.confirm ? "text" : "password"}
value={form.confirmPassword}
onChange={handleChange('confirmPassword')}
onChange={handleChange("confirmPassword")}
className={inputClass}
placeholder="أعد إدخال كلمة المرور الجديدة"
placeholder={t("changePassword.confirmPasswordPlaceholder")}
required
minLength={6}
/>
<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"
>
<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">
{show.confirm ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
@ -193,10 +158,10 @@ export default function ChangePasswordPage() {
{isLoading ? (
<div className="flex items-center justify-center gap-2">
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
<span>جاري الحفظ...</span>
<span>{t("changePassword.saving")}</span>
</div>
) : (
'تغيير كلمة المرور'
t("changePassword.changePassword")
)}
</motion.button>
</form>

View File

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

@ -1,12 +1,13 @@
"use client";
import Link from "next/link";
import { Home, Building, Calendar, Heart, Bell, Settings } from "lucide-react";
import { usePathname } from "next/navigation";
import { Home, Building2, Building, CalendarCheck, CalendarDays, ClipboardList, CreditCard, BookOpenCheck } from "lucide-react";
import React, { useEffect, useState } from "react";
import { useNotifications } from "@/app/contexts/NotificationsContext";
export default function BottomNav({ isOwner }) {
const { unreadCount } = useNotifications();
import { useTranslation } from "react-i18next";
export default function BottomNav({ isOwner, isOwnerOrAgent }) {
const { t } = useTranslation();
const pathname = usePathname();
const [isMounted, setIsMounted] = useState(false);
const bookingsHref = isOwner ? "/owner/reservations" : "/reservations";
@ -15,43 +16,95 @@ export default function BottomNav({ isOwner }) {
}, []);
const items = [
{ href: "/", label: "الرئيسية", icon: Home },
{ href: "/properties", label: "عقاراتنا", icon: Building },
{ href: bookingsHref, label: "الحجوزات", icon: Calendar },
{ href: "/favorites", label: "المفضلة", icon: Heart },
{ href: "/notifications", label: "الإشعارات", icon: Bell, badge: isMounted ? unreadCount : 0 },
{ href: "/settings", label: "الإعدادات", icon: Settings },
{ href: "/", label: t("home"), icon: Home },
{ href: "/properties", label: t("ourProperties"), icon: Building2 },
...(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 },
...(isOwnerOrAgent ? [{ href: "/owner/account-book", label: t("accountBook"), icon: BookOpenCheck }] : []),
];
const isActive = (href) => {
if (href === "/") return pathname === "/";
return pathname.startsWith(href);
};
return (
<nav className="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-50">
<div className="bg-white/95 backdrop-blur-sm border border-gray-200 rounded-3xl shadow-lg px-2 py-2 flex items-center gap-3">
{items.map((it) => {
const Icon = it.icon;
return (
<Link
key={it.href}
href={it.href}
className="relative group flex flex-col items-center justify-center px-2.5 py-2 text-gray-700 hover:text-amber-600 transition-colors"
>
<div className="relative">
<Icon className="w-6 h-6" />
{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">
{it.badge > 9 ? "9+" : it.badge}
</div>
)}
</div>
<div className="absolute -top-10 left-1/2 -translate-x-1/2 bg-gray-800 text-white text-xs rounded-lg px-2.5 py-1.5 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none shadow-lg">
{it.label}
</div>
<div className="mt-1 text-xs text-center" aria-hidden>
{it.label}
</div>
</Link>
);
})}
</div>
</nav>
<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="
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) => {
const Icon = it.icon;
const active = isActive(it.href);
return (
<Link
key={it.href}
href={it.href}
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">
<Icon className="h-5 w-5 sm:h-6 sm:w-6" />
{it.badge > 0 && (
<div
className="
absolute -right-2 -top-2
flex h-6 w-7 sm:h-5 sm:w-5
items-center justify-center
rounded-full
bg-red-500
text-[9px] sm:text-xs
font-bold
text-white
"
>
{it.badge > 9 ? "9+" : it.badge}
</div>
)}
</div>
<div className="mt-0.5 text-[10px] sm:mt-1 text-center sm:text-xs">{it.label}</div>
</Link>
);
})}
</div>
</nav>
</div>
);
}

View File

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

View File

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

@ -3,7 +3,7 @@ import { usePathname } from 'next/navigation';
import Link from 'next/link';
import { LogIn, UserPlus } from 'lucide-react';
export function NavLink({ href, children }) {
export function NavLink({ href, children, light }) {
const pathname = usePathname();
const isActive = pathname === href || pathname.startsWith(href + '/');
return (
@ -11,19 +11,19 @@ export function NavLink({ href, children }) {
href={href}
className={`px-5 py-3 rounded-lg font-semibold transition-all duration-300 text-lg relative group ${
isActive
? 'text-amber-600 bg-amber-50'
: 'text-gray-700 hover:text-amber-600 hover:bg-gray-100'
? light ? 'text-amber-300 bg-white/10' : 'text-amber-600 bg-amber-50'
: light ? 'text-white/90 hover:text-amber-300 hover:bg-white/10' : 'text-gray-700 hover:text-amber-600 hover:bg-gray-100'
}`}
>
{children}
<span className={`absolute bottom-0 left-1/2 transform -translate-x-1/2 h-0.5 transition-all duration-300 ${
isActive ? 'w-4/5 bg-amber-600' : 'w-0 bg-amber-600 group-hover:w-4/5'
isActive ? 'w-4/5 bg-amber-400' : 'w-0 bg-amber-400 group-hover:w-4/5'
}`}></span>
</Link>
);
}
export function MobileNavLink({ href, children, onClick }) {
export function MobileNavLink({ href, children, onClick, light }) {
const pathname = usePathname();
const isActive = pathname === href || pathname.startsWith(href + '/');
return (
@ -32,8 +32,8 @@ export function MobileNavLink({ href, children, onClick }) {
onClick={onClick}
className={`block px-3 py-2 rounded-md text-base font-medium transition-colors ${
isActive
? 'text-amber-600 bg-amber-50'
: 'text-gray-700 hover:text-amber-600 hover:bg-gray-50'
? light ? 'text-amber-300 bg-white/10' : 'text-amber-600 bg-amber-50'
: light ? 'text-white/80 hover:text-amber-300 hover:bg-white/10' : 'text-gray-700 hover:text-amber-600 hover:bg-gray-50'
}`}
>
{children}

View File

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

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 L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import { useState, useEffect, useRef } from "react";
import L from "leaflet";
import "leaflet/dist/leaflet.css";
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
iconRetinaUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png",
iconUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png",
shadowUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png",
});
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);
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>',
maxZoom: 19,
}).addTo(map);
@ -41,32 +41,32 @@ export default function PropertyMapWithMarkers({ properties = [], onPropertyClic
useEffect(() => {
if (!mapInstanceRef.current || !mapLoaded) return;
markersRef.current.forEach(marker => marker.remove());
markersRef.current.forEach((marker) => marker.remove());
markersRef.current = [];
properties.forEach(property => {
properties.forEach((property) => {
if (property.lat && property.lng) {
const marker = L.marker([property.lat, property.lng]).addTo(mapInstanceRef.current);
const popupContent = `
<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>
<p style="font-size: 14px; color: #666; margin-bottom: 8px;">${property.address || property.location?.address || ''}</p>
<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>
<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>
</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;">
${property.type ? `<p>النوع: ${property.type}</p>` : ''}
${property.bedrooms > 0 ? `<p>غرف نوم: ${property.bedrooms}</p>` : ''}
${property.bathrooms > 0 ? `<p>حمامات: ${property.bathrooms}</p>` : ''}
${property.type ? `<p>النوع: ${property.type}</p>` : ""}
${property.bedrooms > 0 ? `<p>غرف نوم: ${property.bedrooms}</p>` : ""}
${property.bathrooms > 0 ? `<p>حمامات: ${property.bathrooms}</p>` : ""}
</div>
</div>
`;
marker.bindPopup(popupContent);
marker.on('click', () => {
marker.on("click", () => {
if (onPropertyClick) {
onPropertyClick(property);
}
@ -83,9 +83,9 @@ export default function PropertyMapWithMarkers({ properties = [], onPropertyClic
}, [properties, mapLoaded]);
const formatPrice = (property) => {
if (property.priceUnit === 'monthly') {
if (property.priceUnit === "monthly") {
return `${property.price?.toLocaleString() || 0} ل.س/شهر`;
} else if (property.priceUnit === 'daily') {
} else if (property.priceUnit === "daily") {
return `${property.price?.toLocaleString() || 0} ل.س/يوم`;
} else {
return `${property.price?.toLocaleString() || 0} ل.س`;
@ -97,4 +97,4 @@ export default function PropertyMapWithMarkers({ properties = [], onPropertyClic
<div ref={mapRef} className="w-full h-full z-0" />
</div>
);
}
}

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,319 +1,117 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { motion } from 'framer-motion';
import { useTranslation } from 'react-i18next';
import { Search, MapPin, Home, DollarSign, ShieldCheck } from 'lucide-react';
export default function HeroSearch({ onSearch, isAuthenticated }) {
"use client";
import { useState } from "react";
import { motion } from "framer-motion";
import { useTranslation } from "react-i18next";
import { Search, MapPin, Home, DollarSign } from "lucide-react";
import { useFilterOptions } from "@/app/hooks/UseFilterOfHeroSearch";
import ShowLoginDialog from "./showLoginDialog";
import FilterSelect from "./FilterSelect";
export default function HeroSearch({ onSearch }) {
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({
city: 'all',
propertyType: 'all',
priceRange: 'all',
identityType: 'syrian',
ownerSource: 'all',
rentPeriod: 'all',
availableToday: false
city: "all",
propertyType: "all",
priceRange: "all",
identityType: "syrian",
ownerSource: "all",
rentPeriod: "all",
availableToday: false,
});
const [showLoginDialog, setShowLoginDialog] = useState(false);
const cities = [
{ id: 'all', label: 'جميع المدن' },
{ id: 'دمشق', label: 'دمشق' },
{ id: 'حلب', label: 'حلب' },
{ id: 'حمص', label: 'حمص' },
{ id: 'اللاذقية', label: 'اللاذقية' },
{ id: 'درعا', label: 'درعا' }
];
const propertyTypes = [
{ id: 'all', label: 'الكل' },
{ id: 'apartment', label: 'شقق سكنية' },
{ id: 'studio', label: 'استوديو' },
{ id: 'commercial', label: 'عقار تجاري' },
{ id: 'villa', label: 'فيلا / مزرعة' }
];
const priceRanges = [
{ id: 'all', label: 'جميع الأسعار' },
{ id: '0-500', label: 'أقل من 50$' },
{ id: '500-1000', label: '50$ - 100$' },
{ id: '1000-2000', label: '100$ - 200$' },
{ id: '2000-3000', label: '200$ - 300$' },
{ id: '3000+', label: 'أكثر من 300$' }
];
const identityTypes = [
{ id: 'syrian', label: 'هوية سورية' },
{ id: 'passport', label: 'جواز سفر' }
];
const ownerSources = [
{ id: 'all', label: 'الكل' },
{ id: 'owner', label: 'من المالك' },
{ id: 'agency', label: 'من مكتب عقاري' }
];
const rentPeriods = [
{ id: 'all', label: 'الكل' },
{ id: 'daily', label: 'إيجار يومي' },
{ id: 'monthly', label: 'إيجار شهري' }
];
const handleTabClick = (tab) => {
setActiveTab(tab);
if ((tab === 'rent' || tab === 'sell') && !isAuthenticated) {
setShowLoginDialog(true);
return;
}
handleSearch();
};
const handleSearch = () => {
if ((activeTab === 'rent' || activeTab === 'sell') && !isAuthenticated) {
setShowLoginDialog(true);
return;
}
onSearch({
...filters,
mode: activeTab,
city: filters.city || 'all',
propertyType: filters.propertyType || 'all',
priceRange: filters.priceRange || 'all',
ownerSource: filters.ownerSource || 'all',
rentPeriod: filters.rentPeriod || 'all'
city: filters.city || "all",
propertyType: filters.propertyType || "all",
priceRange: filters.priceRange || "all",
ownerSource: filters.ownerSource || "all",
rentPeriod: filters.rentPeriod || "all",
});
};
return (
<>
<motion.div
className="bg-white/10 backdrop-blur-lg rounded-2xl p-6 sm:p-8 border border-white/20 shadow-2xl"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.5 }}
>
<div className="flex flex-wrap gap-2 mb-8">
{['rent', 'buy', 'sell'].map((tab) => (
<motion.button
key={tab}
onClick={() => handleTabClick(tab)}
className={`px-4 py-2 rounded-lg font-medium text-sm transition-all ${
activeTab === tab
? 'bg-amber-500 text-white'
: 'bg-white/20 text-white hover:bg-white/30'
}`}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
{t(`${tab}Tab`)}
</motion.button>
))}
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium text-white mb-2">
<div className="flex items-center gap-1">
<MapPin className="w-4 h-4" />
{t("cityStreetLabel")}
</div>
</label>
<select
value={filters.city}
onChange={(e) => setFilters({...filters, city: e.target.value})}
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'left 1rem center',
backgroundSize: '1rem',
paddingLeft: '2.5rem'
}}
>
{cities.map(city => (
<option key={city.id} value={city.id}>{city.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-white mb-2">
<div className="flex items-center gap-1">
<Home className="w-4 h-4" />
{t("rentTypeLabel")}
</div>
</label>
<select
value={filters.propertyType}
onChange={(e) => setFilters({...filters, propertyType: e.target.value})}
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'left 1rem center',
backgroundSize: '1rem',
paddingLeft: '2.5rem'
}}
>
{propertyTypes.map(type => (
<option key={type.id} value={type.id}>{type.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-white mb-2">
<div className="flex items-center gap-1">
<DollarSign className="w-4 h-4" />
{t("priceLabel")}
</div>
</label>
<select
value={filters.priceRange}
onChange={(e) => setFilters({...filters, priceRange: e.target.value})}
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'left 1rem center',
backgroundSize: '1rem',
paddingLeft: '2.5rem'
}}
>
{priceRanges.map(range => (
<option key={range.id} value={range.id}>{range.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-white mb-2">
<div className="flex items-center gap-1">
نوع الوثيقة
</div>
</label>
<select
value={filters.identityType}
onChange={(e) => setFilters({...filters, identityType: e.target.value})}
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'left 1rem center',
backgroundSize: '1rem',
paddingLeft: '2.5rem'
}}
>
{identityTypes.map(type => (
<option key={type.id} value={type.id}>{type.label}</option>
))}
</select>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mt-4">
<div>
<label className="block text-sm font-medium text-white mb-2">مصدر العرض</label>
<select
value={filters.ownerSource}
onChange={(e) => setFilters({ ...filters, ownerSource: e.target.value })}
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'left 1rem center',
backgroundSize: '1rem',
paddingLeft: '2.5rem'
}}
>
{ownerSources.map((source) => (
<option key={source.id} value={source.id}>
{source.label}
</option>
))}
</select>
</div>
<motion.div
className="bg-white/10 backdrop-blur-lg rounded-2xl p-6 sm:p-8 border border-white/20 shadow-2xl"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.5 }}
>
<div className="flex flex-wrap gap-2 mb-8">
{["rent", "buy", "sell"].map((tab) => {
const isActive = activeTab === tab;
<div>
<label className="block text-sm font-medium text-white mb-2">نوع الإيجار</label>
<select
value={filters.rentPeriod}
onChange={(e) => setFilters({ ...filters, rentPeriod: e.target.value })}
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'left 1rem center',
backgroundSize: '1rem',
paddingLeft: '2.5rem'
}}
>
{rentPeriods.map((period) => (
<option key={period.id} value={period.id}>
{period.label}
</option>
))}
</select>
</div>
<div className="md:col-span-2 flex flex-col justify-between p-4 rounded-2xl border border-dashed border-white/30 bg-white/5">
<label className="mt-4 flex items-center gap-3 text-white text-sm">
<input
type="checkbox"
checked={filters.availableToday}
onChange={(e) => setFilters({ ...filters, availableToday: e.target.checked })}
className="w-5 h-5 text-amber-500 rounded border-gray-300 bg-white"
/>
<span className="font-medium">عرض فقط العقارات المتاحة من اليوم</span>
</label>
</div>
</div>
<div className="mt-6">
<motion.button
onClick={handleSearch}
className="w-full bg-amber-500 hover:bg-amber-600 text-white font-bold py-4 px-6 rounded-xl transition-all duration-300 flex items-center justify-center text-base gap-3 shadow-lg hover:shadow-xl"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Search className="w-5 h-5" />
{t("searchButton")}
</motion.button>
</div>
</motion.div>
{showLoginDialog && !isAuthenticated && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 px-4 py-8">
<div className="w-full max-w-md rounded-3xl bg-white p-6 shadow-2xl border border-gray-200">
<div className="flex items-center gap-3 mb-5">
<ShieldCheck className="w-7 h-7 text-amber-500" />
<div>
<h3 className="text-lg font-semibold text-gray-900">يرجى تسجيل الدخول</h3>
<p className="text-sm text-gray-600">للوصول إلى خيارات التأجير والبيع، يجب أن تكون مسجلاً.</p>
</div>
</div>
<div className="space-y-4">
<div className="rounded-2xl bg-gray-50 p-4">
<p className="text-sm text-gray-700">اضغط على تسجيل الدخول لاستكمال البحث أو إدارة عقاراتك.</p>
</div>
<div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
return (
<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"
key={tab}
onClick={() => handleTabClick(tab)}
className={`relative px-5 py-2.5 rounded-lg font-medium text-sm transition-colors duration-200 outline-none select-none ${isActive ? "text-white" : "text-amber-50 hover:text-white"}`}
>
إغلاق
{isActive && <motion.div layoutId="activeTabBackground" className="absolute inset-0 bg-amber-500 rounded-lg shadow-md" transition={{ type: "spring", stiffness: 500, damping: 35 }} />}
<span className="relative z-10">{t(`${tab}Tab`)}</span>
</button>
<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"
>
تسجيل الدخول
</Link>
</div>
);
})}
</div>
{/* city */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<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"))} />
{activeTab === "rent" && (
<FilterSelect
icon={<Home className="w-4 h-4" />}
value={filters.propertyType}
onChange={(e) => setFilters({ ...filters, city: e.target.value })}
option={propertyTypes}
label={t("rentTypeLabel")}
/>
)}
{/* price */}
<FilterSelect
icon={<DollarSign className="w-4 h-4" />}
value={filters.priceRange}
onChange={(e) => setFilters({ ...filters, priceRange: e.target.value })}
option={priceRanges}
label={t("priceLabel")}
/>
{/* type */}
<FilterSelect value={filters.identityType} onChange={(e) => setFilters({ ...filters, identityType: e.target.value })} option={identityTypes} label={t("identityTypeLabel")} />
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mt-4">
{/* owener */}
<FilterSelect value={filters.ownerSource} onChange={(e) => setFilters({ ...filters, ownerSource: e.target.value })} option={ownerSources} label={t("ownerSourceLabel")} />
{activeTab === "rent" && <FilterSelect value={filters.rentPeriod} onChange={(e) => setFilters({ ...filters, rentPeriod: e.target.value })} option={rentPeriods} label={t("rentTypeLabel")} />}
<div className={`${activeTab === "rent" ? "md:col-span-2" : "md:col-span-3"} flex flex-col justify-between p-4 rounded-2xl border border-dashed border-white/30 bg-white/5`}>
{/* checkbox */}
<label className="mt-4 flex items-center gap-3 text-amber-50 text-sm">
<input
type="checkbox"
checked={filters.availableToday}
onChange={(e) => setFilters({ ...filters, availableToday: e.target.checked })}
className="w-5 h-5 text-amber-500 rounded border-gray-300 bg-white"
/>
<span className="font-medium">{t("availableToday")}</span>
</label>
</div>
</div>
</div>
)}
<div className="mt-6">
<motion.button
onClick={handleSearch}
className="w-full bg-amber-500 hover:bg-amber-600 text-white font-bold py-4 px-6 rounded-xl transition-all duration-300 flex items-center justify-center text-base gap-3 shadow-lg hover:shadow-xl"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Search className="w-5 h-5" />
{t("searchButton")}
</motion.button>
</div>
</motion.div>
{showLoginDialog && !isAuthenticated && <ShowLoginDialog setShowLoginDialog={setShowLoginDialog} />}
</>
);
}
}

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,85 @@
'use client';
import { useEffect, useRef } from 'react';
export default function AddPropertyMap({ mapCenter, mapZoom, selectedLocation, onMapClick, onMarkerDragEnd }) {
const containerRef = useRef(null);
const mapRef = useRef(null);
const markerRef = useRef(null);
const LRef = useRef(null);
useEffect(() => {
async function init() {
const L = await import('leaflet');
LRef.current = L;
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
});
if (!containerRef.current || mapRef.current) return;
const map = L.map(containerRef.current, {
center: mapCenter,
zoom: mapZoom,
doubleClickZoom: false,
});
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors',
}).addTo(map);
map.on('click', (e) => {
const pos = [e.latlng.lat, e.latlng.lng];
placeMarker(pos);
map.setView(pos, mapZoom);
onMapClick(pos);
});
mapRef.current = map;
setTimeout(() => map.invalidateSize(), 200);
}
init();
return () => {
if (mapRef.current) {
mapRef.current.remove();
mapRef.current = null;
}
};
}, []);
function placeMarker(pos) {
const L = LRef.current;
if (!L || !mapRef.current) return;
if (markerRef.current) {
markerRef.current.setLatLng(pos);
} else {
markerRef.current = L.marker(pos, { draggable: true }).addTo(mapRef.current);
markerRef.current.on('dragend', (ev) => {
const p = ev.target.getLatLng();
onMarkerDragEnd(p.lat, p.lng);
});
}
}
useEffect(() => {
const map = mapRef.current;
if (!map) return;
map.setView(mapCenter, mapZoom);
}, [mapCenter, mapZoom]);
useEffect(() => {
if (!selectedLocation || !mapRef.current) return;
const pos = [selectedLocation.lat, selectedLocation.lng];
mapRef.current.setView(pos, mapZoom);
placeMarker(pos);
}, [selectedLocation]);
return <div ref={containerRef} className="w-full h-full z-0" />;
}

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

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

View File

@ -0,0 +1,32 @@
"use client";
import { createContext, useContext, useState, useEffect } from "react";
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState("dark");
useEffect(() => {
const stored = localStorage.getItem("theme");
if (stored === "dark" || (!stored && window.matchMedia("(prefers-color-scheme: dark)").matches)) {
setTheme("dark");
document.documentElement.classList.add("dark");
}
}, []);
const toggleTheme = () => {
const next = theme === "light" ? "dark" : "light";
setTheme(next);
document.documentElement.classList.toggle("dark", next === "dark");
localStorage.setItem("theme", next);
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export const useTheme = () => useContext(ThemeContext);

View File

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

View File

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

View File

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

View File

@ -18,7 +18,21 @@ const City = Object.freeze({
QAMISHLI: 'القامشلي',
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
const CitiesList = Object.freeze(Object.values(City));
@ -35,4 +49,22 @@ function extractCity(address) {
return '';
}
export { City, CitiesList, extractCity };
const CityTranslationKeys = Object.freeze({
[City.DAMASCUS]: 'city.damascus',
[City.ALEPPO]: 'city.aleppo',
[City.HOMS]: 'city.homs',
[City.LATAKIA]: 'city.latakia',
[City.DARAA]: 'city.daraa',
[City.TARTOUS]: 'city.tartous',
[City.SUWEIDA]: 'city.suweida',
[City.DEIR_EZZOR]: 'city.deirEzzor',
[City.RAQQA]: 'city.raqqa',
[City.IDLIB]: 'city.idlib',
[City.HASAKAH]: 'city.hasakah',
[City.QAMISHLI]: 'city.qamishli',
[City.RURAL_DAMASCUS]: 'city.ruralDamascus',
});
export { City, CitiesList, extractCity, CityTranslationKeys, };

View File

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

View File

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

View File

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

19
app/enums/DocumentType.js Normal file
View File

@ -0,0 +1,19 @@
/**
* Document type enum for owner property submission.
* Values are intentionally English keys so the UI can store normalized identifiers.
*/
const DocumentType = Object.freeze({
PASSPORT: 'Passport',
IDCARD: 'IDCard',
BOTH: 'Both',
});
const DocumentTypeLabels = Object.freeze({
[DocumentType.PASSPORT]: 'جواز سفر',
[DocumentType.IDCARD]: 'بطاقة شخصية',
[DocumentType.BOTH]: 'كلاهما',
});
const DocumentTypeList = Object.freeze(Object.values(DocumentType));
export { DocumentType, DocumentTypeLabels, DocumentTypeList };

39
app/enums/Governorate.js Normal file
View File

@ -0,0 +1,39 @@
/**
* Syrian governorates enum using English normalized values.
* Labels are provided separately for Arabic display.
*/
const Governorate = Object.freeze({
DAMASCUS: 'Damascus',
ALEPPO: 'Aleppo',
LATAKIA: 'Latakia',
HAMA: 'Hama',
TARTOUS: 'Tartous',
DEIR_EZZOR: 'DeirEzzor',
RAQQA: 'Raqqa',
IDLIB: 'Idlib',
DRAA: 'Daraa',
SUWEIDA: 'Suweida',
HASAKAH: 'Hasakah',
QUNEITRA: 'Quneitra',
RURAL_DAMASCUS: 'RuralDamascus',
});
const GovernorateLabels = Object.freeze({
[Governorate.DAMASCUS]: 'دمشق',
[Governorate.ALEPPO]: 'حلب',
[Governorate.LATAKIA]: 'اللاذقية',
[Governorate.HAMA]: 'حماة',
[Governorate.TARTOUS]: 'طرطوس',
[Governorate.DEIR_EZZOR]: 'دير الزور',
[Governorate.RAQQA]: 'الرقة',
[Governorate.IDLIB]: 'إدلب',
[Governorate.DRAA]: 'درعا',
[Governorate.SUWEIDA]: 'السويداء',
[Governorate.HASAKAH]: 'الحسكة',
[Governorate.QUNEITRA]: 'القنيطرة',
[Governorate.RURAL_DAMASCUS]: 'ريف دمشق',
});
const GovernorateList = Object.freeze(Object.values(Governorate));
export { Governorate, GovernorateLabels, GovernorateList };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,18 +7,28 @@ const UserRole = Object.freeze({
GUEST: 'guest',
CUSTOMER: 'customer',
OWNER: 'owner',
AGENT: 'agent',
});
const UserRoleLabels = Object.freeze({
[UserRole.GUEST]: 'زائر',
[UserRole.CUSTOMER]: 'مستأجر',
[UserRole.OWNER]: 'مالك عقار',
[UserRole.AGENT]: 'وسيط عقاري',
});
const UserRoleColors = Object.freeze({
[UserRole.GUEST]: 'gray',
[UserRole.CUSTOMER]: 'blue',
[UserRole.OWNER]: 'amber',
[UserRole.AGENT]: 'purple',
});
export { UserRole, UserRoleLabels, UserRoleColors };
const UserRoleTranslationKeys = Object.freeze({
[UserRole.GUEST]: 'userRole.guest',
[UserRole.CUSTOMER]: 'userRole.customer',
[UserRole.OWNER]: 'userRole.owner',
[UserRole.AGENT]: 'userRole.agent',
});
export { UserRole, UserRoleLabels, UserRoleColors, UserRoleTranslationKeys };

View File

@ -18,6 +18,8 @@ export { RentPropertyType, RentPropertyTypeLabels } from './RentPropertyType';
export { RentType, RentTypeLabels } from './RentType';
export { PropertyService, PropertyServiceLabels, PropertyServicesList } from './PropertyService';
export { PropertyTerm, PropertyTermLabels, PropertyTermsList } from './PropertyTerm';
export { DocumentType, DocumentTypeLabels, DocumentTypeList } from './DocumentType';
export { Governorate, GovernorateLabels, GovernorateList } from './Governorate';
export { Currency, CurrencyLabels, CurrencySymbols } from './Currency';
export { DeviceType } from './DeviceType';
export { SalePropertiesStatus, SalePropertiesStatusLabels } from './SalePropertiesStatus';

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -1,15 +1,18 @@
'use client';
"use client";
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { motion } from 'framer-motion';
import Link from 'next/link';
import Image from 'next/image';
import { Heart, MapPin, Bed, Bath, Square, X, ImageIcon } from 'lucide-react';
import { useFavorites } from '@/app/contexts/FavoritesContext';
import AuthService from '@/app/services/AuthService';
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/navigation";
import { motion } from "framer-motion";
import Link from "next/link";
import Image from "next/image";
import { Heart, MapPin, Bed, Bath, Square, X, ImageIcon } from "lucide-react";
import { useFavorites } from "@/app/contexts/FavoritesContext";
import AuthService from "@/app/services/AuthService";
import Loading from "../loading";
export default function FavoritesPage() {
const { t } = useTranslation();
const router = useRouter();
const { favorites, isLoading: favoritesLoading, removeFavorite } = useFavorites();
@ -23,38 +26,28 @@ export default function FavoritesPage() {
}, [router]);
const formatCurrency = (amount) => {
return amount?.toLocaleString() + ' ل.س';
return amount?.toLocaleString() + " " + t("currency-syp-suffix");
};
if (favoritesLoading && favorites.length === 0) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<p className="text-gray-600">جاري التحميل...</p>
</div>
</div>
);
return <Loading />;
}
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="container mx-auto px-4 max-w-6xl">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">المفضلة</h1>
<p className="text-gray-600">العقارات التي قمت بحفظها</p>
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t("favorites")}</h1>
<p className="text-gray-600">{t("saved-properties")}</p>
</div>
{favorites.length === 0 ? (
<div className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
<Heart className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد عقارات في المفضلة</h3>
<p className="text-gray-500 mb-6">يمكنك إضافة العقارات التي تعجبك بالنقر على أيقونة القلب</p>
<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"
>
استعرض العقارات
<h3 className="text-xl font-bold text-gray-700 mb-2">{t("no-favorites")}</h3>
<p className="text-gray-500 mb-6">{t("add-favorites-hint")}</p>
<Link href="/properties" className="inline-flex items-center gap-2 bg-amber-500 text-white px-6 py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors">
{t("browse-properties")}
</Link>
</div>
) : (
@ -68,13 +61,7 @@ export default function FavoritesPage() {
>
<div className="relative h-48 bg-gray-100">
{property.images && property.images[0] ? (
<Image
src={property.images[0]}
alt={property.title}
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
<Image 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">
<ImageIcon className="w-12 h-12 text-gray-400" />
@ -93,7 +80,7 @@ export default function FavoritesPage() {
<div>
<div className="flex items-center gap-2 mb-2">
<span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-lg text-xs font-medium">
{property.type === 'apartment' ? 'شقة' : property.type === 'villa' ? 'فيلا' : 'بيت'}
{property.type === "apartment" ? t("buildingType.apartment") : property.type === "villa" ? t("buildingType.villa") : t("buildingType.house")}
</span>
</div>
<h3 className="font-bold text-gray-900 mb-1 line-clamp-1">{property.title}</h3>
@ -106,7 +93,7 @@ export default function FavoritesPage() {
</div>
<div className="text-left">
<div className="text-xl font-bold text-gray-900">{formatCurrency(property.price)}</div>
<div className="text-xs text-gray-500">/{property.priceUnit === 'daily' ? 'يوم' : 'شهر'}</div>
<div className="text-xs text-gray-500">/{property.priceUnit === "daily" ? t("day") : t("month")}</div>
</div>
</div>
@ -127,11 +114,8 @@ export default function FavoritesPage() {
</div>
</div>
<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"
>
عرض التفاصيل
<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">
{t("viewDetails")}
</Link>
</div>
</motion.div>
@ -141,4 +125,4 @@ export default function FavoritesPage() {
</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

@ -6,10 +6,13 @@ import { useRouter } from 'next/navigation';
import Link from 'next/link';
import toast, { Toaster } from 'react-hot-toast';
import { Mail, Key, Lock, CheckCircle, ArrowLeft, RefreshCw } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { requestForgetPasswordOtp, verifyForgetPasswordOtp } from '../utils/api';
import InteractiveBackground from '../components/Animation/Background';
export default function ForgotPasswordPage() {
const router = useRouter();
const { t } = useTranslation();
const [step, setStep] = useState(1);
const [email, setEmail] = useState('');
const [code, setCode] = useState('');
@ -20,17 +23,17 @@ export default function ForgotPasswordPage() {
e.preventDefault();
if (!email.trim()) {
toast.error('يرجى إدخال البريد الإلكتروني');
toast.error(t('forgotPassword.emailRequired'));
return;
}
setIsLoading(true);
try {
await requestForgetPasswordOtp(email);
toast.success('تم إرسال رمز التحقق إلى بريدك الإلكتروني');
toast.success(t('forgotPassword.otpSent'));
setStep(2);
} catch (err) {
toast.error(err?.message || 'فشل إرسال رمز التحقق');
toast.error(err?.message || t('forgotPassword.otpSendFailed'));
} finally {
setIsLoading(false);
}
@ -40,22 +43,22 @@ export default function ForgotPasswordPage() {
e.preventDefault();
if (!code.trim()) {
toast.error('يرجى إدخال رمز التحقق');
toast.error(t('forgotPassword.otpRequired'));
return;
}
if (newPassword.length < 6) {
toast.error('كلمة المرور الجديدة يجب أن تكون 6 أحرف على الأقل');
toast.error(t('validation.passwordMinLength'));
return;
}
setIsLoading(true);
try {
await verifyForgetPasswordOtp(email, code, newPassword);
toast.success('تم إعادة تعيين كلمة المرور بنجاح');
toast.success(t('forgotPassword.resetSuccess'));
setTimeout(() => router.push('/login'), 1200);
} catch (err) {
toast.error(err?.message || 'فشل التحقق من الرمز');
toast.error(err?.message || t('forgotPassword.otpVerifyFailed'));
} finally {
setIsLoading(false);
}
@ -64,8 +67,9 @@ export default function ForgotPasswordPage() {
const inputClass = "w-full pr-12 pl-4 py-3 bg-white/10 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400 transition-all";
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} />
<InteractiveBackground/>
<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 -bottom-40 -left-40 w-80 h-80 bg-orange-400 rounded-full opacity-20 blur-3xl animate-pulse delay-1000"></div>
@ -84,7 +88,7 @@ export default function ForgotPasswordPage() {
>
<Link href="/login" className="flex items-center gap-2 text-gray-600 hover:text-amber-600 transition-colors group">
<ArrowLeft className="w-5 h-5 group-hover:-translate-x-1 transition-transform" />
<span>العودة لتسجيل الدخول</span>
<span>{t('forgotPassword.backToLogin')}</span>
</Link>
</motion.div>
@ -95,7 +99,7 @@ export default function ForgotPasswordPage() {
animate={{ y: 0, opacity: 1 }}
className="text-3xl font-bold text-white mb-2"
>
استعادة كلمة المرور
{t('forgotPassword.title')}
</motion.h1>
<motion.p
initial={{ y: 20, opacity: 0 }}
@ -103,7 +107,7 @@ export default function ForgotPasswordPage() {
transition={{ delay: 0.1 }}
className="text-amber-100"
>
{step === 1 ? 'أدخل بريدك الإلكتروني لاستلام رمز التحقق' : 'أدخل رمز التحقق وكلمة المرور الجديدة'}
{step === 1 ? t('forgotPassword.step1Instruction') : t('forgotPassword.step2Instruction')}
</motion.p>
</div>
@ -120,7 +124,7 @@ export default function ForgotPasswordPage() {
>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
البريد الإلكتروني
{t('forgotPassword.emailLabel')}
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
@ -131,7 +135,7 @@ export default function ForgotPasswordPage() {
value={email}
onChange={(e) => setEmail(e.target.value)}
className={inputClass}
placeholder="أدخل بريدك الإلكتروني"
placeholder={t('forgotPassword.emailPlaceholder')}
dir="ltr"
required
/>
@ -148,10 +152,10 @@ export default function ForgotPasswordPage() {
{isLoading ? (
<div className="flex items-center justify-center gap-2">
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
<span>جاري الإرسال...</span>
<span>{t('forgotPassword.sending')}</span>
</div>
) : (
'إرسال رمز التحقق'
t('forgotPassword.sendOtp')
)}
</motion.button>
</motion.form>
@ -168,7 +172,7 @@ export default function ForgotPasswordPage() {
>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
رمز التحقق
{t('forgotPassword.otpLabel')}
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
@ -179,7 +183,7 @@ export default function ForgotPasswordPage() {
value={code}
onChange={(e) => setCode(e.target.value)}
className={inputClass}
placeholder="أدخل رمز التحقق"
placeholder={t('forgotPassword.otpPlaceholder')}
dir="ltr"
required
/>
@ -188,7 +192,7 @@ export default function ForgotPasswordPage() {
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
كلمة المرور الجديدة
{t('forgotPassword.newPasswordLabel')}
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
@ -199,7 +203,7 @@ export default function ForgotPasswordPage() {
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className={inputClass}
placeholder="أدخل كلمة المرور الجديدة"
placeholder={t('forgotPassword.newPasswordPlaceholder')}
required
minLength={6}
/>
@ -216,10 +220,10 @@ export default function ForgotPasswordPage() {
{isLoading ? (
<div className="flex items-center justify-center gap-2">
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
<span>جاري التحقق...</span>
<span>{t('forgotPassword.verifying')}</span>
</div>
) : (
'إعادة تعيين كلمة المرور'
t('forgotPassword.resetPassword')
)}
</motion.button>
@ -230,7 +234,7 @@ export default function ForgotPasswordPage() {
className="text-sm text-amber-600 hover:text-amber-700 transition-colors flex items-center justify-center gap-1 mx-auto"
>
<RefreshCw className="w-4 h-4" />
<span>تغيير البريد الإلكتروني</span>
<span>{t('forgotPassword.changeEmail')}</span>
</button>
</div>
</motion.form>

View File

@ -1,5 +1,7 @@
@import "tailwindcss";
@custom-variant dark (&:is(.dark *));
/* ─── Madani Arabic Font ─── */
@font-face {
font-family: 'Madani Arabic';
@ -92,6 +94,26 @@
}
}
.dark {
--background: #0f172a;
--foreground: #f5e6d3;
color-scheme: dark;
}
.dark {
--color-white: #1e293b;
--color-gray-50: #0f172a;
--color-gray-100: #1e293b;
--color-gray-200: #334155;
--color-gray-300: #475569;
--color-gray-400: #64748b;
--color-gray-500: #94a3b8;
--color-gray-600: #cbd5e1;
--color-gray-700: #e2e8f0;
--color-gray-800: #f1f5f9;
--color-gray-900: #f8fafc;
}
html, body {
font-family: 'Madani Arabic', 'Noto Sans Arabic', 'Cairo', Arial, sans-serif;
}
@ -135,6 +157,13 @@ body {
transform: scale(1.1);
}
.nav-icon {
color: #1e293b;
}
.dark .nav-icon {
color: #ffffff;
}
.property-tooltip {
background: white !important;
color: #1f2937 !important;

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,6 +1,9 @@
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import ClientLayout from "./ClientLayout";
import { ThemeProvider } from "./contexts/ThemeContext";
import { NotificationsProvider } from "./contexts/NotificationsContext";
import { FavoritesProvider } from "./contexts/FavoritesContext";
const geistSans = Geist({
variable: "--font-geist-sans",
@ -24,7 +27,7 @@ export const metadata = {
export default function Layout({ children }) {
return (
<html lang="ar" dir="rtl">
<html lang="ar" dir="rtl" className="overflow-x-hidden">
<head>
<link
rel="preload"
@ -52,7 +55,13 @@ export default function Layout({ children }) {
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
style={{ fontFamily: "'Madani Arabic', 'Noto Sans Arabic', 'Cairo', Arial, sans-serif" }}
>
<ClientLayout>{children}</ClientLayout>
<ThemeProvider>
<NotificationsProvider>
<FavoritesProvider>
<ClientLayout>{children}</ClientLayout>
</FavoritesProvider>
</NotificationsProvider>
</ThemeProvider>
</body>
</html>
);

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

View File

@ -6,7 +6,7 @@ import Link from 'next/link';
export default function Error({ error, reset }) {
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">
<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" />

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,735 +1,8 @@
"use client";
import { useState } from "react";
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";
import Login from "../components/LoginComponent/Login";
export const metadata = {
title: "صفحة تسجيل الدخول ",
describtion: "صفحة تسجيل الدخول في Sweet Home",
};
export default function LoginPage() {
const router = useRouter();
// Step: 'login' | 'otp'
const [step, setStep] = useState("login");
const [loginMethod, setLoginMethod] = useState("email"); // 'email' | 'phone'
const [showPassword, setShowPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [formData, setFormData] = useState({
credential: "",
password: "",
rememberMe: false,
});
const [otpCode, setOtpCode] = useState("");
const [otpError, setOtpError] = useState("");
const [errors, setErrors] = useState({});
const validateForm = () => {
const newErrors = {};
if (!formData.credential) {
newErrors.credential =
loginMethod === "email"
? "البريد الإلكتروني مطلوب"
: "رقم الهاتف مطلوب";
// } else if (loginMethod === 'email' && !isEmail(formData.credential)) {
// newErrors.credential = 'البريد الإلكتروني غير صالح';
// } else if (loginMethod === 'phone' && !isPhoneNumber(formData.credential)) {
newErrors.credential = "رقم الهاتف غير صالح";
}
if (!formData.password) {
newErrors.password = "كلمة المرور مطلوبة";
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleLogin = async (e) => {
e.preventDefault();
if (!validateForm()) return;
setIsLoading(true);
setErrors({});
try {
const loginFn = loginMethod === "email" ? loginWithEmail : loginWithPhone;
console.log(
"[Login] Attempting login via",
loginMethod,
":",
formData.credential,
);
const result = await loginFn(formData.credential, formData.password);
console.log("[Login] Response status:", result.status);
if (result.status === 200) {
const token =
typeof result.data === "string"
? result.data
: result.data?.token || result.data?.accessToken;
AuthService.addToken(token);
console.log("[Login] Token stored");
// Fetch user profile to get full name
const authUser = AuthService.getUser();
if (authUser?.id) {
try {
const isOwner = AuthService.isOwner();
const fetchFn = isOwner ? getOwnerByUserId : getCustomerByUserId;
const profile = await fetchFn(authUser.id);
if (profile) {
AuthService.cacheUser({
name:
profile.fullName ||
profile.name ||
`${profile.firstName || ""} ${profile.lastName || ""}`.trim(),
email: profile.email || authUser.email,
phone: profile.phone || profile.phoneNumber || authUser.phone,
});
console.log("[Login] User profile cached");
}
} catch (err) {
console.warn("[Login] Failed to fetch profile:", err);
}
}
const userRole = AuthService.isOwner() ? "owner" : "customer";
console.log("[Login] User role:", userRole);
setIsSuccess(true);
toast.success("تم تسجيل الدخول بنجاح!", {
style: { background: "#dcfce7", color: "#166534" },
});
setTimeout(() => {
router.push("/");
}, 1500);
} else if (result.status === 206) {
console.log("[Login] 206 — OTP required");
const tempToken =
typeof result.data === "string"
? result.data
: result.data?.token || result.data?.accessToken;
if (tempToken) {
AuthService.addToken(tempToken);
console.log("[Login] Temp token stored for OTP");
}
toast("يرجى إدخال رمز التحقق", {
icon: "🔐",
style: { background: "#fef3c7", color: "#92400e" },
});
// Send OTP
try {
if (loginMethod === "email") {
await sendEmailOTP();
} else {
await sendPhoneOTP();
}
console.log("[Login] OTP sent successfully");
} catch (otpErr) {
console.warn("[Login] OTP send failed, proceeding anyway:", otpErr);
}
setStep("otp");
} else {
// Other error
console.error("[Login] Unexpected status:", result.status, result.data);
toast.error(
result.data?.message || result.data || "بيانات الدخول غير صحيحة",
{
style: { background: "#fee2e2", color: "#991b1b" },
},
);
}
} catch (err) {
console.error("[Login] Error:", err);
toast.error(err.message || "حدث خطأ في الاتصال", {
style: { background: "#fee2e2", color: "#991b1b" },
});
} finally {
setIsLoading(false);
}
};
const handleVerifyOTP = async (e) => {
e.preventDefault();
if (!otpCode || otpCode.length < 4) {
setOtpError("يرجى إدخال رمز التحقق");
return;
}
setIsLoading(true);
setOtpError("");
try {
const verifyFn = loginMethod === "email" ? verifyEmail : verifyPhone;
console.log("[OTP] Verifying code:", otpCode);
const result = await verifyFn(otpCode);
console.log("[OTP] Verify response status:", result.status);
if (result.ok) {
const finalToken =
typeof result.data === "string"
? result.data
: result.data?.token || result.data?.accessToken;
if (finalToken && typeof finalToken === "string") {
AuthService.addToken(finalToken);
console.log("[OTP] Final token stored");
}
setIsSuccess(true);
toast.success("تم التحقق بنجاح!", {
style: { background: "#dcfce7", color: "#166534" },
});
setTimeout(() => {
console.log("[OTP] Redirecting to home");
router.push("/");
}, 1500);
} else {
console.error("[OTP] Verification failed:", result.data);
setOtpError(result.data?.message || "رمز التحقق غير صحيح");
}
} catch (err) {
console.error("[OTP] Error:", err);
setOtpError(err.message || "حدث خطأ في التحقق");
} finally {
setIsLoading(false);
}
};
const resendOTP = async () => {
try {
console.log("[OTP] Resending OTP via", loginMethod);
if (loginMethod === "email") {
await sendEmailOTP();
} else {
await sendPhoneOTP();
}
toast.success("تم إرسال رمز التحقق مجدداً", {
style: { background: "#dcfce7", color: "#166534" },
});
} catch (err) {
console.error("[OTP] Resend failed:", err);
toast.error("فشل إرسال الرمز");
}
};
// Auto-detect login method from input
const handleCredentialChange = (value) => {
setFormData({ ...formData, credential: value });
if (errors.credential) setErrors({ ...errors, credential: null });
// Auto-switch method
// if (isEmail(value)) {
// setLoginMethod('email');
// } else if (isPhoneNumber(value)) {
// setLoginMethod('phone');
// }
};
const particles = Array.from({ length: 20 }, (_, i) => ({
id: i,
x: Math.random() * 100,
y: Math.random() * 100,
size: Math.random() * 3 + 1,
duration: Math.random() * 15 + 10,
delay: Math.random() * 5,
}));
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.1, delayChildren: 0.2 },
},
};
const itemVariants = {
hidden: { y: 20, opacity: 0 },
visible: {
y: 0,
opacity: 1,
transition: { type: "spring", stiffness: 100 },
},
};
return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center p-4 relative overflow-hidden">
<Toaster position="top-center" reverseOrder={false} />
{/* Particles */}
<div className="absolute inset-0 overflow-hidden">
{particles.map((p) => (
<motion.div
key={p.id}
className="absolute rounded-full bg-amber-500/20"
style={{
left: `${p.x}%`,
top: `${p.y}%`,
width: p.size,
height: p.size,
}}
animate={{
y: [0, -20, 0],
x: [0, 10, -10, 0],
opacity: [0.2, 0.4, 0.2],
}}
transition={{
duration: p.duration,
repeat: Infinity,
delay: p.delay,
ease: "linear",
}}
/>
))}
</div>
{/* Glow orbs */}
<motion.div
className="absolute top-20 left-20 w-64 h-64 bg-amber-500/10 rounded-full blur-3xl"
animate={{ scale: [1, 1.2, 1], x: [0, 30, 0], y: [0, -20, 0] }}
transition={{ duration: 12, repeat: Infinity }}
/>
<motion.div
className="absolute bottom-20 right-20 w-80 h-80 bg-blue-500/10 rounded-full blur-3xl"
animate={{ scale: [1, 1.3, 1], x: [0, -30, 0], y: [0, 20, 0] }}
transition={{ duration: 15, repeat: Infinity }}
/>
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="relative w-full max-w-md z-10"
>
{/* Back link */}
<motion.div variants={itemVariants} className="absolute -top-16 left-0">
<Link
href="/"
className="group flex items-center gap-2 text-gray-400 hover:text-white transition-colors"
>
<motion.div
whileHover={{ x: -5 }}
transition={{ type: "spring", stiffness: 400 }}
>
<ArrowLeft className="w-4 h-4" />
</motion.div>
<span>العودة للرئيسية</span>
</Link>
</motion.div>
<motion.div
variants={itemVariants}
className="bg-white/10 backdrop-blur-2xl rounded-3xl shadow-2xl border border-white/10 overflow-hidden"
>
{/* Header */}
<div className="bg-gradient-to-r from-amber-500 to-amber-600 p-8 text-center relative overflow-hidden">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: "spring" }}
className="absolute -top-10 -right-10 w-40 h-40 bg-white/10 rounded-full"
/>
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.3, type: "spring" }}
className="absolute -bottom-10 -left-10 w-40 h-40 bg-white/10 rounded-full"
/>
<motion.div
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.2 }}
className="relative z-10"
>
<motion.div
animate={{ rotate: [0, 10, -10, 0] }}
transition={{ duration: 2, repeat: Infinity }}
className="w-20 h-20 mx-auto mb-4 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-sm"
>
{step === "otp" ? (
<KeyRound className="w-10 h-10 text-white" />
) : (
<Home className="w-10 h-10 text-white" />
)}
</motion.div>
<h1 className="text-3xl font-bold text-white mb-2">SweetHome</h1>
<p className="text-amber-100">
{step === "otp" ? "أدخل رمز التحقق" : "مرحباً بعودتك!"}
</p>
</motion.div>
</div>
<div className="p-8">
<AnimatePresence mode="wait">
{step === "login" ? (
<motion.form
key="login"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
onSubmit={handleLogin}
className="space-y-6"
>
{/* Login method tabs */}
<div className="flex gap-2 bg-white/5 p-1 rounded-xl">
<button
type="button"
onClick={() => {
setLoginMethod("email");
setFormData({ ...formData, credential: "" });
}}
className={`flex-1 py-2.5 rounded-lg text-sm font-medium transition-all flex items-center justify-center gap-2 ${
loginMethod === "email"
? "bg-amber-500 text-white shadow-lg"
: "text-gray-400 hover:text-white"
}`}
>
<Mail className="w-4 h-4" />
بريد إلكتروني
</button>
<button
type="button"
onClick={() => {
setLoginMethod("phone");
setFormData({ ...formData, credential: "" });
}}
className={`flex-1 py-2.5 rounded-lg text-sm font-medium transition-all flex items-center justify-center gap-2 ${
loginMethod === "phone"
? "bg-amber-500 text-white shadow-lg"
: "text-gray-400 hover:text-white"
}`}
>
<Phone className="w-4 h-4" />
رقم الهاتف
</button>
</div>
{/* Credential input */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
{loginMethod === "email"
? "البريد الإلكتروني"
: "رقم الهاتف"}
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
{loginMethod === "email" ? (
<Mail
className={`w-5 h-5 transition-colors ${errors.credential ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`}
/>
) : (
<Phone
className={`w-5 h-5 transition-colors ${errors.credential ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`}
/>
)}
</div>
<input
type="text"
// type={loginMethod === 'email' ? 'email' : 'tel'}
value={formData.credential}
onChange={(e) => handleCredentialChange(e.target.value)}
className={`w-full pr-12 pl-4 py-4 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.credential
? "border-red-500"
: "border-gray-700"
}`}
placeholder={
loginMethod === "email"
? "example@email.com"
: "+963XXXXXXXXX"
}
dir="ltr"
/>
</div>
{errors.credential && (
<motion.p
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="text-red-500 text-sm mt-1"
>
{errors.credential}
</motion.p>
)}
</div>
{/* Password */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
كلمة المرور
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock
className={`w-5 h-5 transition-colors ${errors.password ? "text-red-500" : "text-gray-400 group-focus-within:text-amber-500"}`}
/>
</div>
<input
type={showPassword ? "text" : "password"}
value={formData.password}
onChange={(e) => {
setFormData({
...formData,
password: e.target.value,
});
if (errors.password)
setErrors({ ...errors, password: null });
}}
className={`w-full pr-12 pl-12 py-4 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.password ? "border-red-500" : "border-gray-700"
}`}
placeholder="أدخل كلمة المرور"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute inset-y-0 left-0 pl-3 flex items-center"
>
{showPassword ? (
<EyeOff className="w-5 h-5 text-gray-400 hover:text-gray-300 transition-colors" />
) : (
<Eye className="w-5 h-5 text-gray-400 hover:text-gray-300 transition-colors" />
)}
</button>
</div>
{errors.password && (
<motion.p
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="text-red-500 text-sm mt-1"
>
{errors.password}
</motion.p>
)}
</div>
{/* Remember + Forgot */}
<div className="flex items-center justify-between">
<label className="flex items-center gap-2 cursor-pointer group">
<input
type="checkbox"
checked={formData.rememberMe}
onChange={(e) =>
setFormData({
...formData,
rememberMe: e.target.checked,
})
}
className="w-4 h-4 rounded border-gray-600 bg-white/5 text-amber-500 focus:ring-amber-500 focus:ring-offset-0"
/>
<span className="text-sm text-gray-400 group-hover:text-white transition-colors">
تذكرني
</span>
</label>
<Link
href="/forgot-password"
className="text-sm text-amber-400 hover:text-amber-300 transition-colors"
>
نسيت كلمة المرور؟
</Link>
</div>
{/* Submit */}
<motion.button
type="submit"
disabled={isLoading || isSuccess}
className="relative w-full bg-gradient-to-r from-amber-500 to-amber-600 text-white py-4 rounded-xl font-bold text-lg overflow-hidden group disabled:opacity-50 disabled:cursor-not-allowed"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<span className="relative z-10 flex items-center justify-center gap-2">
{isLoading ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
جاري تسجيل الدخول...
</>
) : isSuccess ? (
<>
<CheckCircle className="w-5 h-5" />
تم بنجاح!
</>
) : (
<>
<LogIn className="w-5 h-5" />
تسجيل الدخول
</>
)}
</span>
</motion.button>
</motion.form>
) : (
/* OTP Verification Step */
<motion.form
key="otp"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
onSubmit={handleVerifyOTP}
className="space-y-6"
>
<div className="text-center mb-4">
<Shield className="w-12 h-12 text-amber-500 mx-auto mb-3" />
<p className="text-gray-300 text-sm">
تم إرسال رمز التحقق إلى{" "}
<span className="text-white font-medium" dir="ltr">
{formData.credential}
</span>
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
رمز التحقق
</label>
<input
type="text"
value={otpCode}
onChange={(e) => {
setOtpCode(e.target.value);
if (otpError) setOtpError("");
}}
className={`w-full px-4 py-4 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white text-center text-2xl tracking-[0.5em] placeholder-gray-500 transition-all ${
otpError ? "border-red-500" : "border-gray-700"
}`}
placeholder="______"
maxLength={6}
dir="ltr"
/>
{otpError && (
<motion.p
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="text-red-500 text-sm mt-1 text-center"
>
{otpError}
</motion.p>
)}
</div>
<motion.button
type="submit"
disabled={isLoading || isSuccess}
className="relative w-full bg-gradient-to-r from-amber-500 to-amber-600 text-white py-4 rounded-xl font-bold text-lg disabled:opacity-50 disabled:cursor-not-allowed"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<span className="flex items-center justify-center gap-2">
{isLoading ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
جاري التحقق...
</>
) : isSuccess ? (
<>
<CheckCircle className="w-5 h-5" />
تم بنجاح!
</>
) : (
<>
<KeyRound className="w-5 h-5" />
تحقق
</>
)}
</span>
</motion.button>
<div className="flex items-center justify-between text-sm">
<button
type="button"
onClick={() => {
setStep("login");
setOtpCode("");
setOtpError("");
console.log("[OTP] Going back to login");
}}
className="text-gray-400 hover:text-white transition-colors"
>
العودة
</button>
<button
type="button"
onClick={resendOTP}
className="text-amber-400 hover:text-amber-300 transition-colors"
>
إعادة إرسال الرمز
</button>
</div>
</motion.form>
)}
</AnimatePresence>
<motion.p
variants={itemVariants}
className="text-center text-gray-400 mt-6"
>
ليس لديك حساب؟{" "}
<Link
href="/auth/choose-role"
className="text-amber-400 hover:text-amber-300 font-medium transition-colors"
>
إنشاء حساب جديد
</Link>
</motion.p>
</div>
</motion.div>
<motion.p
variants={itemVariants}
className="text-center text-gray-500 text-xs mt-4"
>
بتسجيل الدخول، أنت توافق على{" "}
<Link
href="/terms"
className="text-amber-400 hover:text-amber-300 transition-colors"
>
شروط الاستخدام
</Link>{" "}
و{" "}
<Link
href="/privacy"
className="text-amber-400 hover:text-amber-300 transition-colors"
>
سياسة الخصوصية
</Link>
</motion.p>
</motion.div>
</div>
);
return <Login />;
}

View File

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

View File

@ -1,13 +1,16 @@
'use client';
"use client";
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { motion, AnimatePresence } from 'framer-motion';
import { Bell, CheckCircle, XCircle, Calendar, MessageCircle, CheckCheck, Loader2 } from 'lucide-react';
import AuthService from '@/app/services/AuthService';
import { getUserNotifications } from '@/app/utils/api';
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion";
import { Bell, CheckCircle, XCircle, Calendar, MessageCircle, CheckCheck, Loader2 } from "lucide-react";
import AuthService from "@/app/services/AuthService";
import { getUserNotifications } from "@/app/utils/api";
import Loading from "../loading";
export default function NotificationsPage() {
const { t } = useTranslation();
const router = useRouter();
const [notifications, setNotifications] = useState([]);
const [unreadCount, setUnreadCount] = useState(0);
@ -16,7 +19,7 @@ export default function NotificationsPage() {
useEffect(() => {
if (!AuthService.isAuthenticated()) {
router.push('/login');
router.push("/login");
return;
}
fetchNotifications();
@ -31,36 +34,27 @@ export default function NotificationsPage() {
setNotifications(items);
setUnreadCount(items.length);
} catch (err) {
console.error('Error fetching notifications:', err);
setError(err.message || 'فشل تحميل الإشعارات');
console.error("Error fetching notifications:", err);
setError(err.message || t("fetch-notifications-failed"));
} finally {
setIsLoading(false);
}
};
const markAsRead = (id) => {
setNotifications(prev =>
prev.map(n => (n.id === id ? { ...n, read: true } : n))
);
setUnreadCount(prev => Math.max(0, prev - 1));
setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, read: true } : n)));
setUnreadCount((prev) => Math.max(0, prev - 1));
};
const markAllAsRead = () => {
setNotifications(prev => prev.map(n => ({ ...n, read: true })));
setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
setUnreadCount(0);
};
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="text-center"
>
<Loader2 className="w-12 h-12 text-amber-500 mx-auto mb-4 animate-spin" />
<p className="text-gray-600">جاري التحميل...</p>
</motion.div>
<div>
<Loading />
</div>
);
}
@ -68,13 +62,9 @@ export default function NotificationsPage() {
if (error) {
return (
<div className="min-h-screen flex items-center justify-center">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center"
>
<motion.div 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" />
<h3 className="text-xl font-bold text-gray-700 mb-2">خطأ في التحميل</h3>
<h3 className="text-xl font-bold text-gray-700 mb-2">{t("loading-error")}</h3>
<p className="text-gray-500 mb-4">{error}</p>
<motion.button
whileHover={{ scale: 1.05 }}
@ -82,7 +72,7 @@ export default function NotificationsPage() {
onClick={fetchNotifications}
className="px-6 py-2 bg-amber-500 text-white rounded-xl font-medium hover:bg-amber-600 transition-colors"
>
إعادة المحاولة
{t("retry")}
</motion.button>
</motion.div>
</div>
@ -92,16 +82,10 @@ export default function NotificationsPage() {
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="container mx-auto px-4 max-w-4xl">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="flex justify-between items-center mb-8"
>
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="flex justify-between items-center mb-8">
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">الإشعارات</h1>
<p className="text-gray-600">
{unreadCount > 0 ? `لديك ${unreadCount} إشعار غير مقروء` : 'جميع الإشعارات مقروءة'}
</p>
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t("notifications")}</h1>
<p className="text-gray-600">{unreadCount > 0 ? t("unread-notifications", { count: unreadCount }) : t("all-notifications-read")}</p>
</div>
{unreadCount > 0 && (
<motion.button
@ -111,20 +95,16 @@ export default function NotificationsPage() {
className="flex items-center gap-2 px-4 py-2 bg-white border border-gray-200 rounded-xl text-sm font-medium text-gray-700 hover:bg-gray-50 transition-all shadow-sm"
>
<CheckCheck className="w-4 h-4 text-amber-500" />
تحديد الكل كمقروء
{t("mark-all-read")}
</motion.button>
)}
</motion.div>
{notifications.length === 0 ? (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300"
>
<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">
<Bell className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد إشعارات</h3>
<p className="text-gray-500">ستظهر هنا الإشعارات المتعلقة بحجوزاتك ومدفوعاتك</p>
<h3 className="text-xl font-bold text-gray-700 mb-2">{t("no-notifications")}</h3>
<p className="text-gray-500">{t("notifications-empty-hint")}</p>
</motion.div>
) : (
<div className="space-y-4">
@ -135,32 +115,20 @@ export default function NotificationsPage() {
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
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 }}
onClick={() => markAsRead(notification.id)}
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'
}`}
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"}`}
>
<div className="p-5 flex gap-4">
<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 ? (
<Bell className="w-6 h-6 text-amber-600" />
) : (
<CheckCircle className="w-6 h-6 text-gray-400" />
)}
<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 ? <Bell className="w-6 h-6 text-amber-600" /> : <CheckCircle className="w-6 h-6 text-gray-400" />}
</div>
<div className="flex-1 min-w-0">
<div className="flex justify-between items-start gap-4">
<div className="min-w-0">
<h3 className={`text-base ${!notification.read ? 'font-bold text-gray-900' : 'font-medium text-gray-700'}`}>
{notification.title}
</h3>
{notification.message && (
<p className="text-gray-500 text-sm mt-1 line-clamp-2">{notification.message}</p>
)}
<h3 className={`text-base ${!notification.read ? "font-bold text-gray-900" : "font-medium text-gray-700"}`}>{notification.title}</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">
{notification.date && (
<span className="text-xs text-gray-400 flex items-center gap-1">
@ -176,9 +144,7 @@ export default function NotificationsPage() {
)}
</div>
</div>
{!notification.read && (
<span className="w-2.5 h-2.5 bg-amber-500 rounded-full shrink-0 mt-2" />
)}
{!notification.read && <span className="w-2.5 h-2.5 bg-amber-500 rounded-full shrink-0 mt-2" />}
</div>
</div>
</div>

View File

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

View File

@ -1,57 +1,50 @@
'use client';
"use client";
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { useRouter } from 'next/navigation';
import {
DollarSign,
TrendingUp,
Calendar,
Users,
Building,
Download,
Loader2,
ArrowLeft,
} from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
import AuthService from '@/app/services/AuthService';
import { getOwnerStatistics } from '@/app/utils/api';
import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import { useTranslation } from "react-i18next";
import { DollarSign, TrendingUp, Calendar, Loader2, Star } from "lucide-react";
import toast, { Toaster } from "react-hot-toast";
import AuthService from "@/app/services/AuthService";
import { getOwnerStatistics } from "@/app/utils/api";
import Loading from "@/app/loading";
const StatCard = ({ title, value, icon: Icon, color, subtitle }) => (
<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"
>
const StatCard = ({ title, value, icon: Icon, color, subtitle, isNA }) => (
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-all">
<div className="flex items-center justify-between mb-4">
<div className={`w-12 h-12 ${color} rounded-xl flex items-center justify-center`}>
<Icon className="w-6 h-6 text-white" />
</div>
</div>
<h3 className="text-sm text-gray-500 mb-1">{title}</h3>
<div className="text-2xl font-bold text-gray-900">{value}</div>
<div className="text-2xl font-bold text-gray-900">{isNA ? "N/A" : value}</div>
{subtitle && <p className="text-xs text-gray-400 mt-1">{subtitle}</p>}
</motion.div>
);
export default function OwnerAccountBookPage() {
const router = useRouter();
const { t, i18n } = useTranslation();
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [stats, setStats] = useState({
totalRevenue: 0,
totalReservations: 0,
activeProperties: 0,
totalRevenue: null,
totalReservations: null,
activeProperties: null,
financialRevenue: null,
financialCommission: null,
financialBalance: null,
directRevenue: null,
directCommission: null,
directBalance: null,
directRating: null,
directPostponements: null,
});
const [transactions, setTransactions] = useState([]);
useEffect(() => {
if (AuthService.isGuest()) {
router.push('/auth/choose-role');
return;
}
if (!AuthService.isOwner()) {
router.push('/');
return;
}
@ -66,171 +59,82 @@ export default function OwnerAccountBookPage() {
async function fetchData() {
try {
const data = await getOwnerStatistics();
if (data) {
setStats({
totalRevenue: data.totalRevenue ?? 0,
totalReservations: data.totalReservations ?? 0,
activeProperties: data.activeProperties ?? 0,
totalRevenue: data.totalRevenue ?? null,
totalReservations: data.totalReservations ?? null,
activeProperties: data.activeProperties ?? null,
financialRevenue: data.financialRevenue ?? data.totalRevenue ?? null,
financialCommission: data.financialCommission ?? null,
financialBalance: data.financialBalance ?? null,
directRevenue: data.directRevenue ?? null,
directCommission: data.directCommission ?? null,
directBalance: data.directBalance ?? null,
directRating: data.directRating ?? null,
directPostponements: data.directPostponements ?? null,
});
if (data.transactions) {
setTransactions(data.transactions);
}
}
} catch {
toast.error('تعذر تحميل إحصائيات الحساب');
toast.error(t("accountBook.loadError"));
} finally {
setIsLoading(false);
}
}
fetchData();
}, [router]);
}, []);
const formatCurrency = (amount) => {
const num = Number(amount) || 0;
return num.toLocaleString() + ' ل.س';
const num = Number(amount);
if (isNaN(num)) return "N/A";
return num.toLocaleString() + " " + t("currency-syp-suffix");
};
const handleExport = () => {
toast.success('جاري تصدير البيانات...');
const formatNumber = (val) => {
const num = Number(val);
if (isNaN(num)) return "N/A";
return num.toLocaleString();
};
const formatRating = (val) => {
const num = Number(val);
if (isNaN(num)) return "N/A";
return num.toFixed(1);
};
const isNA = (val) => val === null || val === undefined || val === "";
if (isLoading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<Loader2 className="w-12 h-12 text-amber-500 animate-spin mx-auto mb-4" />
<p className="text-gray-600">جاري تحميل بيانات الحساب...</p>
</div>
</div>
);
return <Loading />;
}
return (
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">
<div className="min-h-screen bg-gray-50 py-8" dir={i18n.language === "ar" ? "rtl" : "ltr"}>
<Toaster position="top-center" reverseOrder={false} />
<div className="container mx-auto px-4 max-w-7xl">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4"
>
<div className="flex items-center gap-4">
<button
onClick={() => router.back()}
className="p-2 hover:bg-gray-200 rounded-xl transition-colors"
>
<ArrowLeft className="w-5 h-5 text-gray-600" />
</button>
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">دفتر الحسابات</h1>
<p className="text-gray-600">
مرحباً {user?.name}، نظرة عامة على حساباتك
</p>
</div>
</div>
<button
onClick={handleExport}
className="px-5 py-2.5 bg-amber-500 text-white rounded-xl font-medium hover:bg-amber-600 transition-colors flex items-center gap-2 shadow-sm"
>
<Download className="w-5 h-5" />
تصدير
</button>
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} 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>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
<StatCard
title="إجمالي الإيرادات"
value={formatCurrency(stats.totalRevenue)}
icon={DollarSign}
color="bg-green-500"
/>
<StatCard
title="إجمالي الحجوزات"
value={stats.totalReservations}
icon={Calendar}
color="bg-blue-500"
/>
<StatCard
title="العقارات النشطة"
value={stats.activeProperties}
icon={Building}
color="bg-amber-500"
/>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden"
>
<div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-lg font-bold text-gray-900">المعاملات المالية</h2>
<motion.div initial={{ opacity: 0, y: 20 }} 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">
<StatCard title={t("accountBook.revenue")} value={formatCurrency(stats.financialRevenue)} 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>
</motion.div>
<motion.div initial={{ opacity: 0, y: 20 }} 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">
<StatCard title={t("accountBook.revenue")} value={formatCurrency(stats.directRevenue)} icon={TrendingUp} color="bg-emerald-500" 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>
{transactions.length > 0 ? (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 text-sm">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-3 text-center text-xs font-semibold text-gray-500 uppercase tracking-wider">
التاريخ
</th>
<th className="px-4 py-3 text-center text-xs font-semibold text-gray-500 uppercase tracking-wider">
البيان
</th>
<th className="px-4 py-3 text-center text-xs font-semibold text-gray-500 uppercase tracking-wider">
المبلغ
</th>
<th className="px-4 py-3 text-center text-xs font-semibold text-gray-500 uppercase tracking-wider">
الحالة
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-100">
{transactions.map((tx, idx) => (
<tr
key={tx.id || idx}
className={`hover:bg-amber-50/40 transition-colors ${
idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'
}`}
>
<td className="px-4 py-3 whitespace-nowrap text-center text-gray-700">
{tx.date}
</td>
<td className="px-4 py-3 whitespace-nowrap text-center text-gray-800 font-medium">
{tx.description}
</td>
<td className="px-4 py-3 whitespace-nowrap text-center font-mono font-semibold text-gray-800">
{formatCurrency(tx.amount)}
</td>
<td className="px-4 py-3 whitespace-nowrap text-center">
<span
className={`inline-flex items-center px-2 py-1 rounded-lg text-xs font-medium ${
tx.status === 'completed'
? 'bg-green-100 text-green-800'
: tx.status === 'pending'
? 'bg-yellow-100 text-yellow-800'
: 'bg-red-100 text-red-800'
}`}
>
{tx.status === 'completed'
? 'مكتمل'
: tx.status === 'pending'
? 'قيد الانتظار'
: 'ملغي'}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="p-12 text-center">
<DollarSign className="w-12 h-12 text-gray-300 mx-auto mb-4" />
<p className="text-gray-500">لا توجد معاملات مالية بعد</p>
</div>
)}
</motion.div>
</div>
</div>

View File

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

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

View File

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

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

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