Compare commits
14 Commits
b9a4c6157a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5592d9fbd2 | |||
| 4b53669a35 | |||
| 00dde588cb | |||
| 4924e7c2f3 | |||
| 9d3d02c88a | |||
| 1fa2fabb38 | |||
| 793ec51c9a | |||
| e3e9aa81a8 | |||
| 989a7f580f | |||
| f7afef03d4 | |||
| f09561ef00 | |||
| e42b0421bd | |||
| 9602154ba4 | |||
| 9b4f9979b6 |
40
.gitea/workflows/deployer.yaml
Normal file
40
.gitea/workflows/deployer.yaml
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
name: Build frontend
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Clone repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
repository: amer2004/SweetHome
|
||||||
|
github-server-url: http://45.93.137.91:3000
|
||||||
|
|
||||||
|
- name: Stopping server
|
||||||
|
run: sudo systemctl stop sweetHome
|
||||||
|
|
||||||
|
- name: Copy repository to output file
|
||||||
|
run: |
|
||||||
|
sudo cp -r $GITHUB_WORKSPACE/* /opt/sweetHome/
|
||||||
|
sudo chown -R $(whoami) /opt/sweetHome
|
||||||
|
|
||||||
|
- name: Setup Node.js 22.x
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 22.x
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: /opt/sweetHome
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Build next project
|
||||||
|
working-directory: /opt/sweetHome
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Starting the server
|
||||||
|
run: |
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl start sweetHome
|
||||||
@ -80,7 +80,7 @@ export default function BlockedPage() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="mt-8 w-full bg-red-600 hover:bg-red-700 text-white rounded-2xl py-4 font-bold transition-colors flex items-center justify-center gap-3"
|
className="mt-8 w-full cursor-pointer bg-red-600 hover:bg-red-700 text-white rounded-2xl py-4 font-bold transition-colors flex items-center justify-center gap-3"
|
||||||
>
|
>
|
||||||
<LogOut className="w-5 h-5" />
|
<LogOut className="w-5 h-5" />
|
||||||
{t("blocked.logoutButton")}
|
{t("blocked.logoutButton")}
|
||||||
|
|||||||
@ -2,11 +2,9 @@
|
|||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { Home, Building, Calendar, CreditCard, Briefcase, BookOpen } from "lucide-react";
|
import { Home, Building2, Building, CalendarCheck, CalendarDays, ClipboardList, CreditCard, BookOpenCheck } from "lucide-react";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { label } from "framer-motion/client";
|
|
||||||
|
|
||||||
export default function BottomNav({ isOwner, isOwnerOrAgent }) {
|
export default function BottomNav({ isOwner, isOwnerOrAgent }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@ -19,18 +17,20 @@ export default function BottomNav({ isOwner, isOwnerOrAgent }) {
|
|||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{ href: "/", label: t("home"), icon: Home },
|
{ href: "/", label: t("home"), icon: Home },
|
||||||
{ href: "/properties", label: t("ourProperties"), icon: Building },
|
{ href: "/properties", label: t("ourProperties"), icon: Building2 },
|
||||||
|
|
||||||
...(isOwnerOrAgent
|
...(isOwnerOrAgent
|
||||||
? [
|
? [
|
||||||
{ href: "/owner/properties", label: t("myProperties"), icon: Briefcase },
|
{ href: "/owner/properties", label: t("myProperties"), icon: Building },
|
||||||
{ href: "/owner/bookings", label: "عقاراتي المحجوزة", icon: Briefcase },
|
{ href: "/owner/bookings", label: t("Myseizedproperties"), icon: CalendarCheck },
|
||||||
{ href: "/reservations", label: "حجوزاتي", icon: Calendar },
|
{ href: "/reservations", label: t("Mybookings"), icon: CalendarDays },
|
||||||
]
|
]
|
||||||
: [{ href: "/booked-properties", label: "حجوزاتي", icon: Briefcase }]),
|
: [{ href: "/booked-properties", label: t("Mybookings"), icon: CalendarDays }]),
|
||||||
{ href: bookingsHref, label: "طلبات الحجز", icon: Calendar },
|
|
||||||
|
|
||||||
|
{ href: bookingsHref, label: t("Orders"), icon: ClipboardList },
|
||||||
{ href: "/payments", label: t("payments"), icon: CreditCard },
|
{ href: "/payments", label: t("payments"), icon: CreditCard },
|
||||||
...(isOwnerOrAgent ? [{ href: "/owner/account-book", label: t("accountBook"), icon: BookOpen }] : []),
|
|
||||||
|
...(isOwnerOrAgent ? [{ href: "/owner/account-book", label: t("accountBook"), icon: BookOpenCheck }] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
const isActive = (href) => {
|
const isActive = (href) => {
|
||||||
@ -39,27 +39,67 @@ export default function BottomNav({ isOwner, isOwnerOrAgent }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed bottom-0 left-0 right-0 z-50 flex justify-center pb-4 pt-2 pointer-events-none">
|
<div className="fixed bottom-0 left-0 right-0 z-50 flex justify-center px-5 pb-4 pt-4 pointer-events-none">
|
||||||
<nav className="bg-gray-50 rounded-3xl px-4 py-2 shadow-[0_4px_24px_rgba(251,146,60,0.25)] pointer-events-auto">
|
<nav
|
||||||
<div className="flex items-center justify-center gap-1">
|
className="
|
||||||
|
w-fit max-w-full
|
||||||
|
rounded-3xl
|
||||||
|
bg-gray-50
|
||||||
|
px-5 py-3 sm:px-4
|
||||||
|
shadow-[0_4px_24px_rgba(251,146,60,0.25)]
|
||||||
|
pointer-events-auto
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex items-center sm:justify-center gap-0.5 sm:gap-1 overflow-x-auto max-w-full
|
||||||
|
[&::-webkit-scrollbar]:h-1.5
|
||||||
|
[&::-webkit-scrollbar-track]:bg-gray-100
|
||||||
|
[&::-webkit-scrollbar-thumb]:bg-gray-700
|
||||||
|
[&::-webkit-scrollbar-thumb]:rounded-full hover:[&::-webkit-scrollbar-thumb]:bg-gray-700"
|
||||||
|
>
|
||||||
{items.map((it) => {
|
{items.map((it) => {
|
||||||
const Icon = it.icon;
|
const Icon = it.icon;
|
||||||
const active = isActive(it.href);
|
const active = isActive(it.href);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={it.href}
|
key={it.href}
|
||||||
href={it.href}
|
href={it.href}
|
||||||
className={`relative flex flex-col items-center justify-center px-3 py-2 rounded-xl transition-all duration-200 ease-out hover:shadow-md hover:bg-white/80 ${active ? "bg-white shadow-sm text-amber-600" : "text-gray-700 hover:text-amber-600"}`}
|
style={{ padding: "3px", width: "115px" }}
|
||||||
|
className={`
|
||||||
|
flex
|
||||||
|
flex-col
|
||||||
|
items-center
|
||||||
|
justify-center
|
||||||
|
rounded-xl
|
||||||
|
transition-all
|
||||||
|
duration-200
|
||||||
|
ease-out
|
||||||
|
${active ? "bg-white shadow-sm text-amber-600" : "text-gray-700 hover:bg-white/80 hover:text-amber-600 hover:shadow-md"}
|
||||||
|
`}
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Icon className="w-6 h-6" />
|
<Icon className="h-5 w-5 sm:h-6 sm:w-6" />
|
||||||
|
|
||||||
{it.badge > 0 && (
|
{it.badge > 0 && (
|
||||||
<div className="absolute -top-2 -right-2 bg-red-500 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center">{it.badge > 9 ? "9+" : it.badge}</div>
|
<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>
|
||||||
<div className="mt-1 text-xs text-center" aria-hidden>
|
|
||||||
{it.label}
|
<div className="mt-0.5 text-[10px] sm:mt-1 text-center sm:text-xs">{it.label}</div>
|
||||||
</div>
|
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -19,17 +19,34 @@ export default function LoginForm({ loginMethod, errors, formData, isLoading, is
|
|||||||
<Phone 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>
|
</div>
|
||||||
|
{loginMethod === "email" ? (
|
||||||
|
<>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="email"
|
||||||
// type={loginMethod === 'email' ? 'email' : 'tel'}
|
// type={loginMethod === 'email' ? 'email' : 'tel'}
|
||||||
value={formData.credential}
|
value={formData.credential}
|
||||||
onChange={(e) => handleCredentialChange(e.target.value)}
|
onChange={(e) => handleCredentialChange(e.target.value)}
|
||||||
className={`w-full pr-12 pl-4 py-4 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${
|
className={`w-full pr-12 pl-4 py-4 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-amber-50 placeholder-gray-500 transition-all ${
|
||||||
errors.credential ? "border-red-500" : "border-gray-700"
|
errors.credential ? "border-red-500" : "border-gray-700"
|
||||||
}`}
|
}`}
|
||||||
placeholder={loginMethod === "email" ? t("emailPlaceholder") : t("phonePlaceholder")}
|
placeholder={t("emailPlaceholder")}
|
||||||
dir="ltr"
|
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>
|
</div>
|
||||||
{errors.credential && (
|
{errors.credential && (
|
||||||
<motion.p initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} className="text-red-500 text-sm mt-1">
|
<motion.p initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} className="text-red-500 text-sm mt-1">
|
||||||
|
|||||||
@ -28,8 +28,6 @@ export default function OtpComponent({ isLoading, setIsLoading, isSuccess, setIs
|
|||||||
if (result?.ok) {
|
if (result?.ok) {
|
||||||
try {
|
try {
|
||||||
await AuthService.cacheCurrentUser(getOwnerByUserId, getCustomerByUserId);
|
await AuthService.cacheCurrentUser(getOwnerByUserId, getCustomerByUserId);
|
||||||
console.log("Decoded:", AuthService.decodeToken());
|
|
||||||
console.log("User:", AuthService.getUser());
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("Failed to cache user", err);
|
console.warn("Failed to cache user", err);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState, useRef } from "react";
|
import { useEffect, useState, useRef } from "react";
|
||||||
import { initializeApp, getApps } from "firebase/app";
|
import { initializeApp, getApps } from "firebase/app";
|
||||||
import { getMessaging, getToken, onMessage } from "firebase/messaging";
|
import { getMessaging, getToken, onMessage } from "firebase/messaging";
|
||||||
import AuthService from "../services/AuthService";
|
import AuthService from "../services/AuthService";
|
||||||
import { setFCMToken } from "../utils/api";
|
import { setFCMToken } from "../utils/api";
|
||||||
|
|
||||||
|
//فاير بيز
|
||||||
const firebaseConfig = {
|
const firebaseConfig = {
|
||||||
apiKey: "AIzaSyBZV7KBLRJSTApahfrO8lBesmIM3zNRSaY",
|
apiKey: "AIzaSyBZV7KBLRJSTApahfrO8lBesmIM3zNRSaY",
|
||||||
authDomain: "sweet-home-b2766.firebaseapp.com",
|
authDomain: "sweet-home-b2766.firebaseapp.com",
|
||||||
@ -16,6 +16,7 @@ const firebaseConfig = {
|
|||||||
measurementId: "G-M2V95NBJLX",
|
measurementId: "G-M2V95NBJLX",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//عم نعمل تهيئة للاعدادات
|
||||||
const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
|
const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
|
||||||
|
|
||||||
export default function NotificationHandler() {
|
export default function NotificationHandler() {
|
||||||
@ -26,11 +27,10 @@ export default function NotificationHandler() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function checkAuth() {
|
function checkAuth() {
|
||||||
if (initialized.current) return;
|
if (initialized.current) return;
|
||||||
|
|
||||||
if (!AuthService.getToken()) return;
|
if (!AuthService.getToken()) return;
|
||||||
initialized.current = true;
|
initialized.current = true;
|
||||||
|
|
||||||
if ("Notification" in window) {
|
if ("Notification" in window) {
|
||||||
|
//عم نشوف اذا حط سماح او رفض
|
||||||
if (Notification.permission === "default") {
|
if (Notification.permission === "default") {
|
||||||
setShowPrompt(true);
|
setShowPrompt(true);
|
||||||
} else if (Notification.permission === "granted") {
|
} else if (Notification.permission === "granted") {
|
||||||
|
|||||||
@ -6,7 +6,6 @@ const resources = {
|
|||||||
en: {
|
en: {
|
||||||
translation: {
|
translation: {
|
||||||
/*Block*/
|
/*Block*/
|
||||||
|
|
||||||
subject: "Subject",
|
subject: "Subject",
|
||||||
"blocked.fillAllFields": "Please fill in all required fields",
|
"blocked.fillAllFields": "Please fill in all required fields",
|
||||||
"blocked.reportSent": "Message sent successfully",
|
"blocked.reportSent": "Message sent successfully",
|
||||||
@ -314,7 +313,9 @@ const resources = {
|
|||||||
|
|
||||||
/* ─── Bottom nav ─── */
|
/* ─── Bottom nav ─── */
|
||||||
payments: "Payments",
|
payments: "Payments",
|
||||||
|
Myseizedproperties:"Seized Properties",
|
||||||
|
Mybookings:"My bookings",
|
||||||
|
Orders:"Orders",
|
||||||
/* ─── Footer ─── */
|
/* ─── Footer ─── */
|
||||||
footerDescription: "Your trusted platform for finding and listing premium properties across Syria.",
|
footerDescription: "Your trusted platform for finding and listing premium properties across Syria.",
|
||||||
quickLinks: "Quick Links",
|
quickLinks: "Quick Links",
|
||||||
@ -1658,10 +1659,16 @@ const resources = {
|
|||||||
"calendar.mockTenant1": "Ahmed Mohammed",
|
"calendar.mockTenant1": "Ahmed Mohammed",
|
||||||
"calendar.mockTenant2": "Sara Ahmed",
|
"calendar.mockTenant2": "Sara Ahmed",
|
||||||
"calendar.mockTenant3": "Mohammed Ali",
|
"calendar.mockTenant3": "Mohammed Ali",
|
||||||
|
"loading":"Loading",
|
||||||
|
"neaebymosque":"Distance to nearest mosque (km)",
|
||||||
|
"commission":"Commission",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ar: {
|
ar: {
|
||||||
translation: {
|
translation: {
|
||||||
|
"loading":"جار التحميل",
|
||||||
|
"neaebymosque":"المسافة الى اقرب جامع",
|
||||||
|
"commission":"كمسيون",
|
||||||
/* Block*/
|
/* Block*/
|
||||||
"blocked.fillAllFields": "يرجى تعبئة جميع الحقول المطلوبة",
|
"blocked.fillAllFields": "يرجى تعبئة جميع الحقول المطلوبة",
|
||||||
"blocked.reportSent": "تم إرسال الرسالة بنجاح",
|
"blocked.reportSent": "تم إرسال الرسالة بنجاح",
|
||||||
@ -1776,6 +1783,7 @@ const resources = {
|
|||||||
"chooseRole.createAgentAccount": "إنشاء حساب وكيل",
|
"chooseRole.createAgentAccount": "إنشاء حساب وكيل",
|
||||||
"chooseRole.alreadyHaveAccount": "لديك حساب بالفعل؟",
|
"chooseRole.alreadyHaveAccount": "لديك حساب بالفعل؟",
|
||||||
|
|
||||||
|
Myseizedproperties:"عقاراتي المحجوزة",
|
||||||
/* ─── Role badges ─── */
|
/* ─── Role badges ─── */
|
||||||
visitor: "زائر",
|
visitor: "زائر",
|
||||||
owner: "مالك",
|
owner: "مالك",
|
||||||
@ -2032,6 +2040,8 @@ const resources = {
|
|||||||
|
|
||||||
/* ─── Bottom nav ─── */
|
/* ─── Bottom nav ─── */
|
||||||
payments: "المدفوعات",
|
payments: "المدفوعات",
|
||||||
|
Orders:"الطلبات",
|
||||||
|
Mybookings:"حجوزاتي",
|
||||||
|
|
||||||
/* ─── Footer ─── */
|
/* ─── Footer ─── */
|
||||||
footerDescription: "منصتك الموثوقة لإيجاد وإدراج العقارات الفاخرة في جميع أنحاء سوريا.",
|
footerDescription: "منصتك الموثوقة لإيجاد وإدراج العقارات الفاخرة في جميع أنحاء سوريا.",
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center p-4" style={{ backgroundColor: "#08172f" }}>
|
<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 initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1.3 }} transition={{ duration: 0.4 }} className="text-center flex flex-col items-center justify-center gap-4">
|
||||||
<motion.div
|
<motion.div
|
||||||
animate={{
|
animate={{
|
||||||
@ -16,18 +18,20 @@ export default function Loading() {
|
|||||||
repeat: Infinity,
|
repeat: Infinity,
|
||||||
ease: "easeInOut",
|
ease: "easeInOut",
|
||||||
}}
|
}}
|
||||||
className="relative w-36 h-36 flex items-center justify-center"
|
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-[3] drop-shadow-[0_0_12px_rgba(245,158,11,0.2)]" xmlns="http://www.w3.org/2000/svg">
|
<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" />
|
<polygon points="50,5 95,27.5 95,72.5 50,95 5,72.5 5,27.5" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center p-2 text-center">
|
<div className="absolute inset-0 flex items-center justify-center p-2 text-center">
|
||||||
<span className="text-amber-400 font-bold text-sm tracking-wide select-none">Sweet Home</span>
|
<span className="text-amber-400 font-bold text-[10px] tracking-wide select-none">Sweet Home</span>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<p className="text-amber-400 text-sm font-bold tracking-wide animate-pulse">... جاري التحميل</p>
|
<p className="text-amber-400 text-sm font-bold tracking-wide animate-pulse" suppressHydrationWarning>
|
||||||
|
{t("loading")}
|
||||||
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -6,8 +6,9 @@ import { useRouter } from "next/navigation";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Calendar, User, Mail, Phone, CheckCircle, XCircle, Clock, MapPin, Bed, Bath, Square, CalendarDays, ChevronLeft, ChevronRight, Eye, Loader2, Search } from "lucide-react";
|
import { Calendar, User, Mail, Phone, CheckCircle, XCircle, Clock, MapPin, Bed, Bath, Square, CalendarDays, ChevronLeft, ChevronRight, Eye, Loader2, Search } from "lucide-react";
|
||||||
import toast, { Toaster } from "react-hot-toast";
|
import toast, { Toaster } from "react-hot-toast";
|
||||||
import { getMyRentListings, getOwnerReservationRequests } from "@/app/utils/api";
|
import { getRentProperty, getOwnerReservationsByStatuses, getRentProperties } from "@/app/utils/api";
|
||||||
import useAuth from "@/app/hooks/useAuth";
|
import useAuth from "@/app/hooks/useAuth";
|
||||||
|
import Loading from "@/app/loading";
|
||||||
|
|
||||||
const OwnerBookingCalendar = ({ property, onDateSelect, selectedDates }) => {
|
const OwnerBookingCalendar = ({ property, onDateSelect, selectedDates }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -382,22 +383,63 @@ export default function OwnerBookingsPage() {
|
|||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await getOwnerReservationRequests();
|
const [response, rentProps] = await Promise.all([getOwnerReservationsByStatuses([0, 1, 2, 3, 4]), getRentProperties().catch(() => [])]);
|
||||||
|
|
||||||
let rawData = response;
|
let rawData = response;
|
||||||
|
|
||||||
if (response && response.data) {
|
if (response && response.data) {
|
||||||
rawData = response.data;
|
rawData = response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// تحويل البيانات إلى مصفوفة وتصفية العناصر الفارغة (null أو undefined)
|
|
||||||
const rawList = Array.isArray(rawData) ? rawData : rawData ? [rawData] : [];
|
const rawList = Array.isArray(rawData) ? rawData : rawData ? [rawData] : [];
|
||||||
|
|
||||||
const list = rawList.filter(Boolean);
|
const list = rawList.filter(Boolean);
|
||||||
|
|
||||||
const mappedBookings = list.map((item) => {
|
// Rent properties
|
||||||
|
const propsList = Array.isArray(rentProps) ? rentProps : [];
|
||||||
|
|
||||||
|
// مهم: تعريف propMap هون
|
||||||
|
const propMap = {};
|
||||||
|
|
||||||
|
propsList.forEach((rp) => {
|
||||||
|
const info = rp?.propertyInformation ?? {};
|
||||||
|
|
||||||
|
const propertyData = {
|
||||||
|
...info,
|
||||||
|
|
||||||
|
// إذا كانت موجودة داخل rp
|
||||||
|
dailyRent: rp?.dailyRent,
|
||||||
|
monthlyRent: rp?.monthlyRent,
|
||||||
|
deposit: rp?.deposit,
|
||||||
|
|
||||||
|
allowedPaymentPeriod: rp?.allowedPaymentPeriod,
|
||||||
|
};
|
||||||
|
|
||||||
|
propMap[rp.propertyInformationId] = propertyData;
|
||||||
|
propMap[rp.propertyInformation?.id] = propertyData;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("PROP MAP:", propMap);
|
||||||
|
|
||||||
|
// ربط الـ reservation بالعقار عن طريق propertyId
|
||||||
|
const enriched = list.map((item) => {
|
||||||
|
if (item.propertyId && propMap[item.propertyId]) {
|
||||||
|
item._prop = propMap[item.propertyId];
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("ENRICHED BOOKINGS:", enriched);
|
||||||
|
|
||||||
|
const mappedBookings = enriched.map((item) => {
|
||||||
|
const property = item._prop || {};
|
||||||
|
|
||||||
let parsedDetails = {};
|
let parsedDetails = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (item?.propertyInformation?.detailsJSON) {
|
if (property?.detailsJSON) {
|
||||||
parsedDetails = JSON.parse(item.propertyInformation.detailsJSON);
|
parsedDetails = JSON.parse(property.detailsJSON);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error parsing detailsJSON", e);
|
console.error("Error parsing detailsJSON", e);
|
||||||
@ -405,31 +447,48 @@ export default function OwnerBookingsPage() {
|
|||||||
|
|
||||||
const statusMap = {
|
const statusMap = {
|
||||||
0: "pending",
|
0: "pending",
|
||||||
1: "confirmed",
|
1: "pending",
|
||||||
2: "completed",
|
2: "confirmed",
|
||||||
3: "cancelled",
|
3: "completed",
|
||||||
|
4: "cancelled",
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusKey = statusMap[item?.propertyInformation?.status] || "pending";
|
const statusKey = statusMap[item?.status] || "pending";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: item?.id,
|
id: item?.id,
|
||||||
dailyRent: item?.dailyRent,
|
|
||||||
monthlyRent: item?.monthlyRent,
|
propertyId: item?.propertyId,
|
||||||
deposit: item?.deposit,
|
|
||||||
|
// هون صاروا ياخدوا من property
|
||||||
|
dailyRent: property?.dailyRent || 0,
|
||||||
|
monthlyRent: property?.monthlyRent || 0,
|
||||||
|
deposit: property?.deposit || 0,
|
||||||
|
|
||||||
createdAt: item?.createdAt,
|
createdAt: item?.createdAt,
|
||||||
startDate: item?.createdAt?.split("T")[0],
|
|
||||||
|
startDate: item?.startDate?.split("T")[0],
|
||||||
|
endDate: item?.endDate?.split("T")[0],
|
||||||
|
|
||||||
status: statusKey,
|
status: statusKey,
|
||||||
propertyTitle: parsedDetails.description || `${t("property")} #${item?.id}`,
|
|
||||||
location: item?.propertyInformation?.address || "",
|
propertyTitle: parsedDetails.description || `${t("property")} #${item?.propertyId}`,
|
||||||
images: item?.propertyInformation?.images || [],
|
|
||||||
|
location: property?.address || "",
|
||||||
|
|
||||||
|
images: property?.images || [],
|
||||||
|
|
||||||
propertyDetails: {
|
propertyDetails: {
|
||||||
bedrooms: item?.propertyInformation?.numberOfBedRooms || 0,
|
bedrooms: property?.numberOfBedRooms || 0,
|
||||||
bathrooms: item?.propertyInformation?.numberOfBathRooms || 0,
|
bathrooms: property?.numberOfBathRooms || 0,
|
||||||
salons: item?.propertyInformation?.numberOfSalons || 0,
|
salons: property?.numberOfSalons || 0,
|
||||||
area: item?.propertyInformation?.space || 0,
|
area: property?.space || 0,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
propertyInformation: property,
|
||||||
|
|
||||||
parsedDetails,
|
parsedDetails,
|
||||||
|
|
||||||
tenantName: item?.tenantName || null,
|
tenantName: item?.tenantName || null,
|
||||||
tenantPhone: item?.tenantPhone || null,
|
tenantPhone: item?.tenantPhone || null,
|
||||||
tenantEmail: item?.tenantEmail || null,
|
tenantEmail: item?.tenantEmail || null,
|
||||||
@ -475,14 +534,7 @@ export default function OwnerBookingsPage() {
|
|||||||
}, [bookings]);
|
}, [bookings]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return <Loading />;
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
|
||||||
<div className="text-center">
|
|
||||||
<Loader2 className="w-12 h-12 text-amber-500 animate-spin mx-auto mb-4" />
|
|
||||||
<p className="text-gray-600">{t("loading")}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
|
|
||||||
export default function Loading() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center">
|
|
||||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-center">
|
|
||||||
<div className="w-14 h-14 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
|
||||||
<p className="text-gray-400 text-lg">جاري التحميل...</p>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -123,6 +123,7 @@ export default function AddPropertyPage() {
|
|||||||
dailyPrice: "",
|
dailyPrice: "",
|
||||||
monthlyPrice: "",
|
monthlyPrice: "",
|
||||||
deposit: "",
|
deposit: "",
|
||||||
|
commission: "",
|
||||||
allowedPaymentPeriod: "",
|
allowedPaymentPeriod: "",
|
||||||
|
|
||||||
city: Governorate.DAMASCUS,
|
city: Governorate.DAMASCUS,
|
||||||
@ -141,6 +142,7 @@ export default function AddPropertyPage() {
|
|||||||
nearbyUniversity: "",
|
nearbyUniversity: "",
|
||||||
nearbyPark: "",
|
nearbyPark: "",
|
||||||
nearbyMall: "",
|
nearbyMall: "",
|
||||||
|
neaebymosque: "",
|
||||||
|
|
||||||
roomAreaType: "Private room",
|
roomAreaType: "Private room",
|
||||||
roomPeopleAllowed: "",
|
roomPeopleAllowed: "",
|
||||||
@ -561,6 +563,7 @@ export default function AddPropertyPage() {
|
|||||||
} else if (formData.offerType === "both") {
|
} else if (formData.offerType === "both") {
|
||||||
if (!formData.dailyPrice) newErrors.dailyPrice = t("addProperty.dailyPriceRequired");
|
if (!formData.dailyPrice) newErrors.dailyPrice = t("addProperty.dailyPriceRequired");
|
||||||
if (!formData.monthlyPrice) newErrors.monthlyPrice = t("addProperty.monthlyPriceRequired");
|
if (!formData.monthlyPrice) newErrors.monthlyPrice = t("addProperty.monthlyPriceRequired");
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -680,6 +683,7 @@ export default function AddPropertyPage() {
|
|||||||
const isRent = purpose === "rent";
|
const isRent = purpose === "rent";
|
||||||
const details = {
|
const details = {
|
||||||
description: formData.description || "",
|
description: formData.description || "",
|
||||||
|
commission: parseFloat(formData.commission) || 0,
|
||||||
services: selectedServices,
|
services: selectedServices,
|
||||||
serviceDetails: selectedServices.reduce((acc, s) => ({ ...acc, [s]: formData.serviceDetails[s] || "in general" }), {}),
|
serviceDetails: selectedServices.reduce((acc, s) => ({ ...acc, [s]: formData.serviceDetails[s] || "in general" }), {}),
|
||||||
propertyCondition: formData.furnished ? "WithFurniture" : "WithoutFurniture",
|
propertyCondition: formData.furnished ? "WithFurniture" : "WithoutFurniture",
|
||||||
@ -693,6 +697,7 @@ export default function AddPropertyPage() {
|
|||||||
university: formData.nearbyUniversity || "",
|
university: formData.nearbyUniversity || "",
|
||||||
park: formData.nearbyPark || "",
|
park: formData.nearbyPark || "",
|
||||||
mall: formData.nearbyMall || "",
|
mall: formData.nearbyMall || "",
|
||||||
|
mosque: formData.neaebymosque || "",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -747,15 +752,13 @@ export default function AddPropertyPage() {
|
|||||||
try {
|
try {
|
||||||
if (purpose === "sale") {
|
if (purpose === "sale") {
|
||||||
const payload = {
|
const payload = {
|
||||||
|
|
||||||
propInfo,
|
propInfo,
|
||||||
price: parseFloat(formData.salePrice),
|
price: parseFloat(formData.salePrice),
|
||||||
deposit: parseFloat(formData.deposit) || 0,
|
deposit: parseFloat(formData.deposit) || 0,
|
||||||
|
|
||||||
};
|
};
|
||||||
console.log("ff", payload);
|
|
||||||
const res = await addSaleProperty(payload);
|
const res = await addSaleProperty(payload);
|
||||||
console.log(res);
|
|
||||||
toast.success(t("toast.salePropertySuccess"));
|
toast.success(t("toast.salePropertySuccess"));
|
||||||
} else {
|
} else {
|
||||||
const rentTypeMap = { daily: RentType.DAILY, monthly: RentType.MONTHLY, both: RentType.MONTHLY };
|
const rentTypeMap = { daily: RentType.DAILY, monthly: RentType.MONTHLY, both: RentType.MONTHLY };
|
||||||
@ -773,7 +776,9 @@ export default function AddPropertyPage() {
|
|||||||
type: formData.furnished ? RentPropertyType.FURNISHED : RentPropertyType.UNFURNISHED,
|
type: formData.furnished ? RentPropertyType.FURNISHED : RentPropertyType.UNFURNISHED,
|
||||||
allowedPaymentPeriod: formData.allowedPaymentPeriod || "1.00:00:00",
|
allowedPaymentPeriod: formData.allowedPaymentPeriod || "1.00:00:00",
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await addRentProperty(payload);
|
const res = await addRentProperty(payload);
|
||||||
|
|
||||||
toast.success(t("toast.rentPropertySuccess"));
|
toast.success(t("toast.rentPropertySuccess"));
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -862,7 +867,7 @@ export default function AddPropertyPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setFormData({ ...formData, propertyType: type.id })}
|
onClick={() => setFormData({ ...formData, propertyType: type.id })}
|
||||||
className={`p-4 border rounded-xl flex flex-col items-center gap-2 transition-all ${
|
className={`p-4 border rounded-xl flex flex-col items-center gap-2 transition-all ${
|
||||||
formData.propertyType === type.id ? "border-amber-500 bg-amber-50 text-amber-700" : "border-gray-200 hover:border-amber-200 hover:bg-amber-50/50"
|
formData.propertyType === type.id ? "border-amber-500 bg-[#1E293B] text-amber-700" : "border-gray-200 hover:border-amber-200 hover:bg-amber-50/50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Icon className="w-6 h-6" />
|
<Icon className="w-6 h-6" />
|
||||||
@ -895,7 +900,7 @@ export default function AddPropertyPage() {
|
|||||||
<select
|
<select
|
||||||
value={formData.documentType}
|
value={formData.documentType}
|
||||||
onChange={(e) => setFormData({ ...formData, documentType: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, documentType: e.target.value })}
|
||||||
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
className="w-full px-4 bg-[#1E293B] py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
||||||
>
|
>
|
||||||
{DocumentTypeList.map((type) => (
|
{DocumentTypeList.map((type) => (
|
||||||
<option key={type} value={type}>
|
<option key={type} value={type}>
|
||||||
@ -1037,19 +1042,19 @@ export default function AddPropertyPage() {
|
|||||||
const Icon = service.icon;
|
const Icon = service.icon;
|
||||||
const isSelected = formData.services[service.id];
|
const isSelected = formData.services[service.id];
|
||||||
return (
|
return (
|
||||||
<div key={service.id} className={`border rounded-xl transition-all ${isSelected ? "border-amber-500 bg-amber-50" : "border-gray-200"}`}>
|
<div key={service.id} className={`border rounded-xl transition-all ${isSelected ? "border-amber-500 bg-[#1E293B]" : "border-gray-200"}`}>
|
||||||
<label className="flex items-center gap-3 p-3 cursor-pointer">
|
<label className="flex items-center gap-3 p-3 cursor-pointer">
|
||||||
<input type="checkbox" checked={isSelected} onChange={() => toggleService(service.id)} className="w-4 h-4 text-amber-500 rounded" />
|
<input type="checkbox" checked={isSelected} onChange={() => toggleService(service.id)} className="w-4 h-4 bg-[#1E293B] text-amber-500 rounded" />
|
||||||
<Icon className={`w-5 h-5 ${isSelected ? "text-amber-600" : "text-gray-400"}`} />
|
<Icon className={`w-5 h-5 ${isSelected ? "text-amber-600" : "text-gray-400"}`} />
|
||||||
<span className={`text-sm font-medium ${isSelected ? "text-amber-700" : "text-gray-600"}`}>{service.label}</span>
|
<span className={`text-sm font-medium ${isSelected ? "text-amber-700" : "text-gray-600"}`}>{service.label}</span>
|
||||||
</label>
|
</label>
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<div className="px-3 pb-3">
|
<div className="px-3 pb-3 bg-[#1E293B]">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.serviceDetails[service.id] || ""}
|
value={formData.serviceDetails[service.id] || ""}
|
||||||
onChange={(e) => updateServiceDetail(service.id, e.target.value)}
|
onChange={(e) => updateServiceDetail(service.id, e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-amber-500"
|
className="w-full px-3 py-2 border bg-[#1E293B] border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-amber-500"
|
||||||
placeholder={t("addProperty.serviceDetailPlaceholder")}
|
placeholder={t("addProperty.serviceDetailPlaceholder")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -1060,7 +1065,7 @@ export default function AddPropertyPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-xl p-6 border border-gray-200 mt-6">
|
<div className="bg-[#1E293B] rounded-xl p-6 border border-gray-200 mt-6">
|
||||||
<h3 className="text-lg font-bold text-gray-800 mb-4 flex items-center gap-2">
|
<h3 className="text-lg font-bold text-gray-800 mb-4 flex items-center gap-2">
|
||||||
<MapPin className="w-5 h-5 text-amber-600" />
|
<MapPin className="w-5 h-5 text-amber-600" />
|
||||||
{t("addProperty.nearbyServicesTitle")}
|
{t("addProperty.nearbyServicesTitle")}
|
||||||
@ -1090,6 +1095,19 @@ export default function AddPropertyPage() {
|
|||||||
placeholder={t("addProperty.nearbyPlaceholder")}
|
placeholder={t("addProperty.nearbyPlaceholder")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">{t("neaebymosque")}</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="0.1"
|
||||||
|
required
|
||||||
|
value={formData.neaebymosque}
|
||||||
|
onChange={(e) => setFormData({ ...formData, neaebymosque: e.target.value })}
|
||||||
|
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
||||||
|
placeholder={t("addProperty.nearbyPlaceholder")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">{t("addProperty.nearbyRestaurantLabel")}</label>
|
<label className="block text-sm font-medium text-gray-700 mb-2">{t("addProperty.nearbyRestaurantLabel")}</label>
|
||||||
<input
|
<input
|
||||||
@ -1151,7 +1169,7 @@ export default function AddPropertyPage() {
|
|||||||
<label
|
<label
|
||||||
key={term.id}
|
key={term.id}
|
||||||
className={`flex items-center gap-2 p-3 border rounded-xl cursor-pointer transition-all ${
|
className={`flex items-center gap-2 p-3 border rounded-xl cursor-pointer transition-all ${
|
||||||
formData.terms[term.id] ? "border-amber-500 bg-amber-50" : "border-gray-200 hover:border-amber-200 hover:bg-amber-50/50"
|
formData.terms[term.id] ? "border-amber-500 bg-[#1E293B]" : "border-gray-200 hover:border-amber-200 hover:bg-amber-50/50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<input type="checkbox" checked={formData.terms[term.id]} onChange={() => toggleTerm(term.id)} className="hidden" />
|
<input type="checkbox" checked={formData.terms[term.id]} onChange={() => toggleTerm(term.id)} className="hidden" />
|
||||||
@ -1208,7 +1226,7 @@ export default function AddPropertyPage() {
|
|||||||
{step === 3 && purpose === "sale" && (
|
{step === 3 && purpose === "sale" && (
|
||||||
<motion.div variants={fadeInUp} className="space-y-8">
|
<motion.div variants={fadeInUp} className="space-y-8">
|
||||||
<div className="text-center mb-6">
|
<div className="text-center mb-6">
|
||||||
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
<div className="w-20 h-20 bg-amber-400 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||||
<DollarSign className="w-10 h-10 text-amber-600" />
|
<DollarSign className="w-10 h-10 text-amber-600" />
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">{t("addProperty.salePriceTitle")}</h2>
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">{t("addProperty.salePriceTitle")}</h2>
|
||||||
@ -1238,6 +1256,33 @@ export default function AddPropertyPage() {
|
|||||||
placeholder={t("addProperty.salePricePlaceholder")}
|
placeholder={t("addProperty.salePricePlaceholder")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
{t("commission")} ({currencySymbol})
|
||||||
|
</label>
|
||||||
|
<div className="relative ">
|
||||||
|
<span className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400 font-bold text-sm flex items-center justify-center">{currencySymbol}</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
value={formData.commission || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
|
||||||
|
if (val === "" || parseFloat(val) >= 0) {
|
||||||
|
setFormData({ ...formData, commission: val });
|
||||||
|
}
|
||||||
|
(formData.commission);
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (["-", "e", "E", "+"].includes(e.key)) e.preventDefault();
|
||||||
|
}}
|
||||||
|
className="w-full pr-12 pl-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
||||||
|
placeholder={t("addProperty.depositPlaceholder")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{errors.salePrice && <p className="text-red-500 text-sm mt-1">{errors.salePrice}</p>}
|
{errors.salePrice && <p className="text-red-500 text-sm mt-1">{errors.salePrice}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -1248,7 +1293,7 @@ export default function AddPropertyPage() {
|
|||||||
<select
|
<select
|
||||||
value={selectedCurrencyId}
|
value={selectedCurrencyId}
|
||||||
onChange={(e) => setSelectedCurrencyId(parseInt(e.target.value))}
|
onChange={(e) => setSelectedCurrencyId(parseInt(e.target.value))}
|
||||||
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
className="w-full px-4 py-3 border border-gray-300 bg-[#1E293B] rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
||||||
>
|
>
|
||||||
<option value={Currency.USD}>{t("addProperty.usdOption")}</option>
|
<option value={Currency.USD}>{t("addProperty.usdOption")}</option>
|
||||||
<option value={Currency.SYP}>{t("addProperty.sypOption")}</option>
|
<option value={Currency.SYP}>{t("addProperty.sypOption")}</option>
|
||||||
@ -1280,7 +1325,7 @@ export default function AddPropertyPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setFormData({ ...formData, offerType: type.id })}
|
onClick={() => setFormData({ ...formData, offerType: type.id })}
|
||||||
className={`p-4 border rounded-xl flex flex-col items-center gap-2 transition-all ${
|
className={`p-4 border rounded-xl flex flex-col items-center gap-2 transition-all ${
|
||||||
formData.offerType === type.id ? "border-amber-500 bg-amber-50 text-amber-700" : "border-gray-200 hover:border-amber-200 hover:bg-amber-50/50"
|
formData.offerType === type.id ? "border-amber-500 bg-[#1E293B] text-amber-700" : "border-gray-200 hover:border-amber-200 hover:bg-amber-50/50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Icon className="w-6 h-6" />
|
<Icon className="w-6 h-6" />
|
||||||
@ -1290,7 +1335,7 @@ export default function AddPropertyPage() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* كمسيون */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
{t("addProperty.currencyLabel")} <span className="text-red-500">*</span>
|
{t("addProperty.currencyLabel")} <span className="text-red-500">*</span>
|
||||||
@ -1298,7 +1343,7 @@ export default function AddPropertyPage() {
|
|||||||
<select
|
<select
|
||||||
value={selectedCurrencyId}
|
value={selectedCurrencyId}
|
||||||
onChange={(e) => setSelectedCurrencyId(parseInt(e.target.value))}
|
onChange={(e) => setSelectedCurrencyId(parseInt(e.target.value))}
|
||||||
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
className="w-full px-4 py-3 border bg-[#1E293B] border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
||||||
>
|
>
|
||||||
<option value={Currency.USD}>{t("addProperty.usdOption")}</option>
|
<option value={Currency.USD}>{t("addProperty.usdOption")}</option>
|
||||||
<option value={Currency.SYP}>{t("addProperty.sypOption")}</option>
|
<option value={Currency.SYP}>{t("addProperty.sypOption")}</option>
|
||||||
@ -1365,6 +1410,33 @@ export default function AddPropertyPage() {
|
|||||||
placeholder={t("addProperty.depositPlaceholder")}
|
placeholder={t("addProperty.depositPlaceholder")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* كمسيون */}
|
||||||
|
<div className="mt-3">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
{t("commission")} ({currencySymbol})
|
||||||
|
</label>
|
||||||
|
<div className="relative ">
|
||||||
|
<span className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400 font-bold text-sm flex items-center justify-center">{currencySymbol}</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
value={formData.commission || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
if (val === "" || parseFloat(val) >= 0) {
|
||||||
|
setFormData({ ...formData, commission: val });
|
||||||
|
}
|
||||||
|
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (["-", "e", "E", "+"].includes(e.key)) e.preventDefault();
|
||||||
|
}}
|
||||||
|
className="w-full pr-12 pl-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
||||||
|
placeholder={t("addProperty.depositPlaceholder")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
@ -1420,6 +1492,30 @@ export default function AddPropertyPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-3">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
{t("commission")} ({currencySymbol})
|
||||||
|
</label>
|
||||||
|
<div className="relative ">
|
||||||
|
<span className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400 font-bold text-sm flex items-center justify-center">{currencySymbol}</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
value={formData.commission || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
if (val === "" || parseFloat(val) >= 0) {
|
||||||
|
setFormData({ ...formData, commission: val });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (["-", "e", "E", "+"].includes(e.key)) e.preventDefault();
|
||||||
|
}}
|
||||||
|
className="w-full pr-12 pl-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
||||||
|
placeholder={t("addProperty.depositPlaceholder")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -1499,6 +1595,30 @@ export default function AddPropertyPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-3">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
{t("commission")} ({currencySymbol})
|
||||||
|
</label>
|
||||||
|
<div className="relative ">
|
||||||
|
<span className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400 font-bold text-sm flex items-center justify-center">{currencySymbol}</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
value={formData.commission || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
if (val === "" || parseFloat(val) >= 0) {
|
||||||
|
setFormData({ ...formData, commission: val });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (["-", "e", "E", "+"].includes(e.key)) e.preventDefault();
|
||||||
|
}}
|
||||||
|
className="w-full pr-12 pl-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
||||||
|
placeholder={t("addProperty.depositPlaceholder")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
@ -1515,7 +1635,7 @@ export default function AddPropertyPage() {
|
|||||||
<p className="text-gray-600">{t("addProperty.locationAndImagesSubtitle")}</p>
|
<p className="text-gray-600">{t("addProperty.locationAndImagesSubtitle")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-3xl border border-amber-100 bg-linear-to-br from-amber-50 via-white to-orange-50 p-4 sm:p-5 md:p-6 shadow-sm">
|
<div className="rounded-3xl border border-amber-100 p-4 sm:p-5 md:p-6 shadow-sm">
|
||||||
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between mb-5">
|
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between mb-5">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 text-amber-700 font-semibold mb-1">
|
<div className="flex items-center gap-2 text-amber-700 font-semibold mb-1">
|
||||||
@ -1589,7 +1709,7 @@ export default function AddPropertyPage() {
|
|||||||
{t("addProperty.confirmLocation")}
|
{t("addProperty.confirmLocation")}
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<div className="rounded-2xl border border-amber-100 bg-amber-50 px-4 py-3 text-sm text-amber-800 flex items-start gap-2">
|
<div className="rounded-2xl border border-amber-100 bg-[#1E293B] px-4 py-3 text-sm text-amber-800 flex items-start gap-2">
|
||||||
<Info className="w-4 h-4 mt-0.5 shrink-0" />
|
<Info className="w-4 h-4 mt-0.5 shrink-0" />
|
||||||
<span>{t("addProperty.clickMapHint")}</span>
|
<span>{t("addProperty.clickMapHint")}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
|
|
||||||
export default function Loading() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center">
|
|
||||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-center">
|
|
||||||
<div className="w-14 h-14 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
|
||||||
<p className="text-gray-400 text-lg">جاري التحميل...</p>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -665,6 +665,7 @@ import {
|
|||||||
import toast, { Toaster } from 'react-hot-toast';
|
import toast, { Toaster } from 'react-hot-toast';
|
||||||
import AuthService from '../../services/AuthService';
|
import AuthService from '../../services/AuthService';
|
||||||
import { getRentProperties } from '../../utils/api';
|
import { getRentProperties } from '../../utils/api';
|
||||||
|
import Loading from '@/app/loading';
|
||||||
|
|
||||||
const API_BASE =
|
const API_BASE =
|
||||||
process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api';
|
process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api';
|
||||||
@ -1433,9 +1434,7 @@ export default function OwnerReservationRequestsPage() {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
<Loading/>
|
||||||
<Loader2 className="w-12 h-12 text-amber-500 animate-spin" />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import toast, { Toaster } from "react-hot-toast";
|
import toast, { Toaster } from "react-hot-toast";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||||
import {
|
import {
|
||||||
MapPin,
|
MapPin,
|
||||||
Bed,
|
Bed,
|
||||||
@ -54,6 +54,7 @@ import {
|
|||||||
Dog,
|
Dog,
|
||||||
CassetteTape,
|
CassetteTape,
|
||||||
Info,
|
Info,
|
||||||
|
Theater,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { getRentProperty, getSaleProperty, getSalePropertyById, bookReservation, getAvailableDateRanges, getOwnerContactInformation, getMyRentListings, getMySaleListings } from "../../utils/api";
|
import { getRentProperty, getSaleProperty, getSalePropertyById, bookReservation, getAvailableDateRanges, getOwnerContactInformation, getMyRentListings, getMySaleListings } from "../../utils/api";
|
||||||
import AuthService from "../../services/AuthService";
|
import AuthService from "../../services/AuthService";
|
||||||
@ -158,6 +159,7 @@ function mapApiDetail(item) {
|
|||||||
const dailyPrice = item.dailyRent || 0;
|
const dailyPrice = item.dailyRent || 0;
|
||||||
const monthlyPrice = item.monthlyRent || 0;
|
const monthlyPrice = item.monthlyRent || 0;
|
||||||
const salePrice = item.price || 0;
|
const salePrice = item.price || 0;
|
||||||
|
const commission = item.commission || 0;
|
||||||
const price = isRent ? monthlyPrice || dailyPrice : salePrice;
|
const price = isRent ? monthlyPrice || dailyPrice : salePrice;
|
||||||
const priceUnit = isRent ? (monthlyPrice ? "monthly" : "daily") : "sale";
|
const priceUnit = isRent ? (monthlyPrice ? "monthly" : "daily") : "sale";
|
||||||
|
|
||||||
@ -243,9 +245,10 @@ function mapApiDetail(item) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PropertyDetailsPage() {
|
export default function PropertyDetailsPage({ type }) {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { isFavorite, addFavorite, removeFavorite } = useFavorites();
|
const { isFavorite, addFavorite, removeFavorite } = useFavorites();
|
||||||
|
|
||||||
@ -270,6 +273,7 @@ export default function PropertyDetailsPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = params.id;
|
const id = params.id;
|
||||||
|
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
@ -277,7 +281,7 @@ export default function PropertyDetailsPage() {
|
|||||||
try {
|
try {
|
||||||
let data = null;
|
let data = null;
|
||||||
try {
|
try {
|
||||||
data = await getRentProperty(id);
|
data = type === "rent" ? await getRentProperty(id) : (await getSalePropertyById(id)) || (await getSaleProperty(id));
|
||||||
} catch {}
|
} catch {}
|
||||||
if (!data) {
|
if (!data) {
|
||||||
try {
|
try {
|
||||||
@ -424,6 +428,7 @@ export default function PropertyDetailsPage() {
|
|||||||
}
|
}
|
||||||
setBookingLoading(true);
|
setBookingLoading(true);
|
||||||
setBookingError(null);
|
setBookingError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const propInfoId = property._raw?.propertyInformationId || property.id;
|
const propInfoId = property._raw?.propertyInformationId || property.id;
|
||||||
const startDate = new Date(selectedStart + "T00:00:00.000").toISOString();
|
const startDate = new Date(selectedStart + "T00:00:00.000").toISOString();
|
||||||
@ -525,8 +530,8 @@ export default function PropertyDetailsPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="absolute bottom-4 right-4 flex gap-2 z-10">
|
<div className="absolute bottom-4 right-4 flex gap-2 z-10">
|
||||||
<span className="bg-black/70 text-white px-3 py-1 rounded-full text-sm font-medium backdrop-blur-sm">{t("propertyStatus." + property.statusLabel)}</span>
|
<span className="bg-gray-100 text-black px-3 py-1 rounded-full text-sm font-medium backdrop-blur-sm">{t("propertyStatus." + property.statusLabel)}</span>
|
||||||
<span className="bg-black/70 text-white px-3 py-1 rounded-full text-sm font-medium backdrop-blur-sm">{t("buildingType." + property.typeLabel)}</span>
|
<span className="bg-gray-100 text-black px-3 py-1 rounded-full text-sm font-medium backdrop-blur-sm">{t("buildingType." + property.typeLabel)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute bottom-4 left-4 bg-black/70 text-white px-3 py-1 rounded-full text-xs backdrop-blur-sm z-10">
|
<div className="absolute bottom-4 left-4 bg-black/70 text-white px-3 py-1 rounded-full text-xs backdrop-blur-sm z-10">
|
||||||
{currentImage + 1} / {property.images.length || 1}
|
{currentImage + 1} / {property.images.length || 1}
|
||||||
@ -584,7 +589,7 @@ export default function PropertyDetailsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Price */}
|
{/* Price */}
|
||||||
<div className="bg-amber-50 rounded-xl p-3 mb-4">
|
<div className="bg-[#1E293B] rounded-xl p-3 mb-4">
|
||||||
{property.isRent ? (
|
{property.isRent ? (
|
||||||
<div className="flex flex-wrap gap-6 items-end">
|
<div className="flex flex-wrap gap-6 items-end">
|
||||||
{property.priceDisplay.monthly > 0 && (
|
{property.priceDisplay.monthly > 0 && (
|
||||||
@ -604,13 +609,27 @@ export default function PropertyDetailsPage() {
|
|||||||
<span className="font-medium">{t("depositLabel")}</span> {formatCurrency(property.deposit)} {t("syp")}
|
<span className="font-medium">{t("depositLabel")}</span> {formatCurrency(property.deposit)} {t("syp")}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{property.details.commission > 0 && (
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
<span className="font-medium">{t("commission")}</span> {formatCurrency(property.details.commission)} {t("syp")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-2xl font-bold text-blue-600">{formatCurrency(property.price)}</span>
|
<span className="text-2xl font-bold text-blue-600">{formatCurrency(property.price)}</span>
|
||||||
<span className="text-gray-500 mr-1">{t("syp")}</span>
|
<span className="text-gray-500 mr-1">{t("syp")}</span>
|
||||||
<span className="text-sm text-gray-400 mr-2">{t("forSale")}</span>
|
<span className="text-sm text-gray-400 mr-2">{t("forSale")}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-2xl font-bold text-blue-600">{formatCurrency(property.details.commission)}</span>
|
||||||
|
<span className="text-gray-500 mr-1">{t("syp")}</span>
|
||||||
|
<span className="text-sm text-gray-400 mr-2">{t("commission")}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -856,12 +875,13 @@ export default function PropertyDetailsPage() {
|
|||||||
return (
|
return (
|
||||||
<div key={key} className="bg-gray-50 rounded-lg p-2 flex items-center gap-1.5">
|
<div key={key} className="bg-gray-50 rounded-lg p-2 flex items-center gap-1.5">
|
||||||
{key === "School" && <School className="w-3.5 h-3.5 text-amber-500 flex-shrink-0" />}
|
{key === "School" && <School className="w-3.5 h-3.5 text-amber-500 flex-shrink-0" />}
|
||||||
|
{key === "Mosque" && <Theater className="w-3.5 h-3.5 text-amber-500 flex-shrink-0" />}
|
||||||
{key === "Hospital" && <Hospital className="w-3.5 h-3.5 text-amber-500 flex-shrink-0" />}
|
{key === "Hospital" && <Hospital className="w-3.5 h-3.5 text-amber-500 flex-shrink-0" />}
|
||||||
{key === "Restaurant" && <Store className="w-3.5 h-3.5 text-amber-500 flex-shrink-0" />}
|
{key === "Restaurant" && <Store className="w-3.5 h-3.5 text-amber-500 flex-shrink-0" />}
|
||||||
{key === "University" && <GraduationCap className="w-3.5 h-3.5 text-amber-500 flex-shrink-0" />}
|
{key === "University" && <GraduationCap className="w-3.5 h-3.5 text-amber-500 flex-shrink-0" />}
|
||||||
{key === "Park" && <TreePine className="w-3.5 h-3.5 text-amber-500 flex-shrink-0" />}
|
{key === "Park" && <TreePine className="w-3.5 h-3.5 text-amber-500 flex-shrink-0" />}
|
||||||
{key === "Mall" && <Building className="w-3.5 h-3.5 text-amber-500 flex-shrink-0" />}
|
{key === "Mall" && <Building className="w-3.5 h-3.5 text-amber-500 flex-shrink-0" />}
|
||||||
{!["School", "Hospital", "Restaurant", "University", "Park", "Mall"].includes(key) && <MapPin className="w-3.5 h-3.5 text-amber-500 flex-shrink-0" />}
|
{!["School", "Hospital", "Restaurant", "University", "Park", "Mall", "Mosque"].includes(key) && <MapPin className="w-3.5 h-3.5 text-amber-500 flex-shrink-0" />}
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs font-medium text-gray-900">{t(proxKey(key))}</div>
|
<div className="text-xs font-medium text-gray-900">{t(proxKey(key))}</div>
|
||||||
<div className="text-[10px] text-gray-500">
|
<div className="text-[10px] text-gray-500">
|
||||||
@ -952,7 +972,7 @@ export default function PropertyDetailsPage() {
|
|||||||
<div className="grid grid-cols-2 gap-2 mb-3">
|
<div className="grid grid-cols-2 gap-2 mb-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => setPricingMode("daily")}
|
onClick={() => setPricingMode("daily")}
|
||||||
className={`p-2.5 rounded-xl text-center border-2 transition-all ${effectivePricingMode === "daily" ? "border-amber-500 bg-amber-50" : "border-gray-200 hover:border-gray-300"}`}
|
className={`p-2.5 rounded-xl text-center border-2 transition-all ${effectivePricingMode === "daily" ? "border-amber-500 bg-[#1E293B]" : "border-gray-200 hover:border-gray-300"}`}
|
||||||
>
|
>
|
||||||
<div className="text-xs font-bold text-gray-900">{t("dailyRentLabel")}</div>
|
<div className="text-xs font-bold text-gray-900">{t("dailyRentLabel")}</div>
|
||||||
<div className="text-sm font-bold text-amber-600">
|
<div className="text-sm font-bold text-amber-600">
|
||||||
@ -961,7 +981,7 @@ export default function PropertyDetailsPage() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setPricingMode("monthly")}
|
onClick={() => setPricingMode("monthly")}
|
||||||
className={`p-2.5 rounded-xl text-center border-2 transition-all ${effectivePricingMode === "monthly" ? "border-amber-500 bg-amber-50" : "border-gray-200 hover:border-gray-300"}`}
|
className={`p-2.5 rounded-xl text-center border-2 transition-all ${effectivePricingMode === "monthly" ? "border-amber-500 bg-[#1E293B]" : "border-gray-200 hover:border-gray-300"}`}
|
||||||
>
|
>
|
||||||
<div className="text-xs font-bold text-gray-900">{t("monthlyRentLabel")}</div>
|
<div className="text-xs font-bold text-gray-900">{t("monthlyRentLabel")}</div>
|
||||||
<div className="text-sm font-bold text-amber-600">
|
<div className="text-sm font-bold text-amber-600">
|
||||||
@ -1045,7 +1065,6 @@ export default function PropertyDetailsPage() {
|
|||||||
)}
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1053,7 +1072,7 @@ export default function PropertyDetailsPage() {
|
|||||||
{/* Login Dialog */}
|
{/* Login Dialog */}
|
||||||
{showLoginDialog && (
|
{showLoginDialog && (
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onClick={() => setShowLoginDialog(false)}>
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onClick={() => setShowLoginDialog(false)}>
|
||||||
<div className="bg-white rounded-2xl p-6 max-w-sm text-center mx-4" onClick={(e) => e.stopPropagation()}>
|
<div className="bg-[#1E293B] rounded-2xl p-6 max-w-sm text-center mx-4" onClick={(e) => e.stopPropagation()}>
|
||||||
<div className="w-16 h-16 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
<div className="w-16 h-16 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
<LogIn className="w-8 h-8 text-amber-600" />
|
<LogIn className="w-8 h-8 text-amber-600" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,15 +1,20 @@
|
|||||||
import PropertyDetail from "./PropertyDetail";
|
import PropertyDetail from "./PropertyDetail";
|
||||||
|
|
||||||
// Server-side API fetch for metadata (runs at request time on server)
|
async function fetchPropertyForMeta(id, type) {
|
||||||
async function fetchPropertyForMeta(id) {
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`http://45.93.137.91/api/RentProperties/GetRentProperties`, {
|
const endpoint = type === "sale" ? "http://45.93.137.91/api/SaleProperties/GetSaleProperties" : "http://45.93.137.91/api/RentProperties/GetRentProperties";
|
||||||
|
|
||||||
|
const res = await fetch(endpoint, {
|
||||||
next: { revalidate: 60 },
|
next: { revalidate: 60 },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) return null;
|
if (!res.ok) return null;
|
||||||
|
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
const json = JSON.parse(text);
|
const json = JSON.parse(text);
|
||||||
|
|
||||||
const items = Array.isArray(json?.data) ? json.data : Array.isArray(json) ? json : [];
|
const items = Array.isArray(json?.data) ? json.data : Array.isArray(json) ? json : [];
|
||||||
|
|
||||||
return items.find((p) => p.id == id) || items[0] || null;
|
return items.find((p) => p.id == id) || items[0] || null;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
@ -18,7 +23,9 @@ async function fetchPropertyForMeta(id) {
|
|||||||
|
|
||||||
function mapProperty(item) {
|
function mapProperty(item) {
|
||||||
const info = item.propertyInformation || item.PropertyInformation || {};
|
const info = item.propertyInformation || item.PropertyInformation || {};
|
||||||
|
|
||||||
let details = {};
|
let details = {};
|
||||||
|
|
||||||
if (typeof info.detailsJSON === "object" && info.detailsJSON) {
|
if (typeof info.detailsJSON === "object" && info.detailsJSON) {
|
||||||
details = info.detailsJSON;
|
details = info.detailsJSON;
|
||||||
} else if (typeof info.DetailsJSON === "object" && info.DetailsJSON) {
|
} else if (typeof info.DetailsJSON === "object" && info.DetailsJSON) {
|
||||||
@ -30,10 +37,25 @@ function mapProperty(item) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const price = item.monthlyRent || item.MonthlyRent || item.dailyRent || item.DailyRent || 0;
|
const price = item.monthlyRent || item.MonthlyRent || item.dailyRent || item.DailyRent || 0;
|
||||||
|
|
||||||
const priceUnit = item.monthlyRent || item.MonthlyRent ? "monthly" : "daily";
|
const priceUnit = item.monthlyRent || item.MonthlyRent ? "monthly" : "daily";
|
||||||
|
|
||||||
const buildingType = info.buildingType ?? info.BuildingType ?? 0;
|
const buildingType = info.buildingType ?? info.BuildingType ?? 0;
|
||||||
const type = { 0: "apartment", 1: "villa", 2: "house" }[buildingType] || "apartment";
|
|
||||||
const typeLabel = { 0: "apartment", 1: "villa", 2: "house" }[buildingType] || "apartment";
|
const type =
|
||||||
|
{
|
||||||
|
0: "apartment",
|
||||||
|
1: "villa",
|
||||||
|
2: "house",
|
||||||
|
}[buildingType] || "apartment";
|
||||||
|
|
||||||
|
const typeLabel =
|
||||||
|
{
|
||||||
|
0: "apartment",
|
||||||
|
1: "villa",
|
||||||
|
2: "house",
|
||||||
|
}[buildingType] || "apartment";
|
||||||
|
|
||||||
const address = info.address || info.Address || "";
|
const address = info.address || info.Address || "";
|
||||||
const bedrooms = info.numberOfBedRooms || info.NumberOfBedRooms || 0;
|
const bedrooms = info.numberOfBedRooms || info.NumberOfBedRooms || 0;
|
||||||
const bathrooms = info.numberOfBathRooms || info.NumberOfBathRooms || 0;
|
const bathrooms = info.numberOfBathRooms || info.NumberOfBathRooms || 0;
|
||||||
@ -56,9 +78,13 @@ function mapProperty(item) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateMetadata({ params }) {
|
export async function generateMetadata({ params, searchParams }) {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
const raw = await fetchPropertyForMeta(id);
|
const { type } = await searchParams;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const raw = await fetchPropertyForMeta(id, type);
|
||||||
|
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
return {
|
return {
|
||||||
@ -68,11 +94,13 @@ export async function generateMetadata({ params }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const p = mapProperty(raw);
|
const p = mapProperty(raw);
|
||||||
const priceStr = `${p.price.toLocaleString()} ل.س / ${p.priceUnit === "daily" ? "يوم" : "شهر"}`;
|
|
||||||
const propertyImage = p.image ? (p.image.startsWith("http") ? p.image : `http://45.93.137.91${p.image}`) : "";
|
|
||||||
const logoUrl = `http://45.93.137.91/logo.png`;
|
|
||||||
|
|
||||||
// Use property image if available, otherwise logo
|
const priceStr = `${p.price.toLocaleString()} ل.س / ${p.priceUnit === "daily" ? "يوم" : "شهر"}`;
|
||||||
|
|
||||||
|
const propertyImage = p.image ? (p.image.startsWith("http") ? p.image : `http://45.93.137.91${p.image}`) : "";
|
||||||
|
|
||||||
|
const logoUrl = "http://45.93.137.91/logo.png";
|
||||||
|
|
||||||
const ogImages = propertyImage
|
const ogImages = propertyImage
|
||||||
? [
|
? [
|
||||||
{ url: propertyImage, width: 1200, height: 630 },
|
{ url: propertyImage, width: 1200, height: 630 },
|
||||||
@ -83,14 +111,16 @@ export async function generateMetadata({ params }) {
|
|||||||
return {
|
return {
|
||||||
title: `${p.title} - ${priceStr}`,
|
title: `${p.title} - ${priceStr}`,
|
||||||
description: p.description,
|
description: p.description,
|
||||||
|
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: `${p.title} - ${priceStr}`,
|
title: `${p.title} - ${priceStr}`,
|
||||||
description: p.description,
|
description: p.description,
|
||||||
images: ogImages,
|
images: ogImages,
|
||||||
url: `http://45.93.137.91/property/${id}`,
|
url: `http://45.93.137.91/property/${id}?type=${type}`,
|
||||||
type: "website",
|
type: "website",
|
||||||
siteName: "SweetHome",
|
siteName: "SweetHome",
|
||||||
},
|
},
|
||||||
|
|
||||||
twitter: {
|
twitter: {
|
||||||
card: "summary_large_image",
|
card: "summary_large_image",
|
||||||
title: `${p.title} - ${priceStr}`,
|
title: `${p.title} - ${priceStr}`,
|
||||||
@ -100,6 +130,8 @@ export async function generateMetadata({ params }) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PropertyPage({ params }) {
|
export default async function PropertyPage({ params, searchParams }) {
|
||||||
return <PropertyDetail params={params} />;
|
const { type } = await searchParams;
|
||||||
|
|
||||||
|
return <PropertyDetail params={params} type={type} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -61,14 +61,13 @@ export default function TenantRegisterPage() {
|
|||||||
email: formData.email,
|
email: formData.email,
|
||||||
phoneNumber: formData.phone,
|
phoneNumber: formData.phone,
|
||||||
whatsAppNumber: formData.whatsapp,
|
whatsAppNumber: formData.whatsapp,
|
||||||
phone: formData.phone2,
|
phone: formData.phone2 || "" ,
|
||||||
nationalNumber: formData.nationalNumber,
|
nationalNumber: formData.nationalNumber,
|
||||||
password: formData.password,
|
password: formData.password,
|
||||||
customerType: formData.customerType,
|
customerType: formData.customerType,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const res = await addCustomer(payload, null, null);
|
const res = await addCustomer(payload, null, null);
|
||||||
console.log(res);
|
|
||||||
if (res.status === 200 || res.ok) {
|
if (res.status === 200 || res.ok) {
|
||||||
const apiMessage = res.message || res.data?.message || res.data[0]?.message;
|
const apiMessage = res.message || res.data?.message || res.data[0]?.message;
|
||||||
toast.success(apiMessage || t("register.accountCreated"), { duration: 4000 });
|
toast.success(apiMessage || t("register.accountCreated"), { duration: 4000 });
|
||||||
|
|||||||
@ -502,9 +502,6 @@ const getAuthToken = () => {
|
|||||||
return (
|
return (
|
||||||
AuthService.getToken?.() ||
|
AuthService.getToken?.() ||
|
||||||
AuthService.getAccessToken?.() ||
|
AuthService.getAccessToken?.() ||
|
||||||
localStorage.getItem("token") ||
|
|
||||||
localStorage.getItem("accessToken") ||
|
|
||||||
localStorage.getItem("authToken") ||
|
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -663,6 +660,7 @@ function CountdownTimer({ deadline }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
function PaymentDialog({
|
function PaymentDialog({
|
||||||
isOpen,
|
isOpen,
|
||||||
reservation,
|
reservation,
|
||||||
@ -940,7 +938,7 @@ function PaymentDialog({
|
|||||||
)}
|
)}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
|
{/* // */}
|
||||||
{/* Pay Cash button - commented out */}
|
{/* Pay Cash button - commented out */}
|
||||||
{/* <div className="px-6 py-4 border-b border-gray-100">
|
{/* <div className="px-6 py-4 border-b border-gray-100">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@ -25,12 +25,15 @@ import { getCustomerByUserId, getOwnerByUserId, loginWithEmail, loginWithPhone }
|
|||||||
const TOKEN_KEY = "auth_token";
|
const TOKEN_KEY = "auth_token";
|
||||||
const USER_KEY = "cached_user";
|
const USER_KEY = "cached_user";
|
||||||
|
|
||||||
|
const isSecureContext = () =>
|
||||||
|
typeof window !== "undefined" && window.location?.protocol === "https:";
|
||||||
|
|
||||||
const AuthService = Object.freeze({
|
const AuthService = Object.freeze({
|
||||||
addToken(token) {
|
addToken(token) {
|
||||||
if (!token || typeof token !== "string") return;
|
if (!token || typeof token !== "string") return;
|
||||||
Cookies.set(TOKEN_KEY, token, {
|
Cookies.set(TOKEN_KEY, token, {
|
||||||
expires: 7,
|
expires: 7,
|
||||||
secure: true,
|
secure: isSecureContext(),
|
||||||
sameSite: "lax",
|
sameSite: "lax",
|
||||||
path: "/",
|
path: "/",
|
||||||
});
|
});
|
||||||
@ -51,7 +54,7 @@ const AuthService = Object.freeze({
|
|||||||
cacheUser(user) {
|
cacheUser(user) {
|
||||||
Cookies.set(USER_KEY, JSON.stringify(user), {
|
Cookies.set(USER_KEY, JSON.stringify(user), {
|
||||||
expires: 7,
|
expires: 7,
|
||||||
secure: true,
|
secure: isSecureContext(),
|
||||||
sameSite: "lax",
|
sameSite: "lax",
|
||||||
path: "/",
|
path: "/",
|
||||||
});
|
});
|
||||||
|
|||||||
@ -192,7 +192,7 @@ export async function getSaleProperties() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getSaleProperty(id) {
|
export async function getSaleProperty(id) {
|
||||||
const items = await apiFetch("/SaleProperties/GetSaleProperties");
|
const items = await apiFetch(`/SaleProperties/GetSalePropertyById/${id}`);
|
||||||
if (!Array.isArray(items)) return items;
|
if (!Array.isArray(items)) return items;
|
||||||
return items.find((p) => p.id == id) || items[0];
|
return items.find((p) => p.id == id) || items[0];
|
||||||
}
|
}
|
||||||
@ -222,6 +222,9 @@ export async function getReservations() {
|
|||||||
return apiFetch("/Reservations/GetAllReservations");
|
return apiFetch("/Reservations/GetAllReservations");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export async function getReservation(id) {
|
export async function getReservation(id) {
|
||||||
return apiFetch(`/Reservations/GetReservation?id=${id}`);
|
return apiFetch(`/Reservations/GetReservation?id=${id}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,8 +38,7 @@ export async function requestNotificationPermission() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const token = await getToken(messaging, {
|
const token = await getToken(messaging, {
|
||||||
vapidKey:
|
vapidKey:"BGZ4Fo8rRhoTdStLGlCySDZOnAX4ekCA0e3HDWXL5uEi2kOnXynYjbaDbY15002phUrFqxBpPPFHgfH2VhrmFDU",
|
||||||
"BGZ4Fo8rRhoTdStLGlCySDZOnAX4ekCA0e3HDWXL5uEi2kOnXynYjbaDbY15002phUrFqxBpPPFHgfH2VhrmFDU",
|
|
||||||
serviceWorkerRegistration: registration,
|
serviceWorkerRegistration: registration,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -17,11 +17,6 @@ export const validateStep1 = (formData, setErrors, t) => {
|
|||||||
newErrors.email = t("register.emailInvalid");
|
newErrors.email = t("register.emailInvalid");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!formData.phone?.trim()) {
|
|
||||||
newErrors.phone = t("register.phoneRequired");
|
|
||||||
} else if (!validatePhone(formData.phone)) {
|
|
||||||
newErrors.phone = `${t("register.phoneInvalid")} (${t("register.phoneMustStartWith")} 09/05)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.whatsapp?.trim()) {
|
if (!formData.whatsapp?.trim()) {
|
||||||
newErrors.whatsapp = t("register.whatsappRequired");
|
newErrors.whatsapp = t("register.whatsappRequired");
|
||||||
|
|||||||
@ -9,7 +9,8 @@ const ownerRoutes = ["/owner", "/owner/account-book", "/owner/bookings", "/owner
|
|||||||
function getRolesFromToken(token) {
|
function getRolesFromToken(token) {
|
||||||
if (!token) return [];
|
if (!token) return [];
|
||||||
try {
|
try {
|
||||||
const base64 = token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/");
|
let base64 = token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
while (base64.length % 4 !== 0) base64 += "=";
|
||||||
const payload = JSON.parse(atob(base64));
|
const payload = JSON.parse(atob(base64));
|
||||||
const roles = payload["http://schemas.microsoft.com/ws/2008/06/identity/claims/role"];
|
const roles = payload["http://schemas.microsoft.com/ws/2008/06/identity/claims/role"];
|
||||||
if (Array.isArray(roles)) return roles;
|
if (Array.isArray(roles)) return roles;
|
||||||
@ -39,6 +40,7 @@ export function middleware(request) {
|
|||||||
return NextResponse.redirect(new URL("/", request.url));
|
return NextResponse.redirect(new URL("/", request.url));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (isOwnerRoute && !isOwner) {
|
if (isOwnerRoute && !isOwner) {
|
||||||
return NextResponse.redirect(new URL("/", request.url));
|
return NextResponse.redirect(new URL("/", request.url));
|
||||||
}
|
}
|
||||||
@ -47,5 +49,5 @@ export function middleware(request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: ["/((?!api|_next/static|_next/image|favicon.ico|.*\\.(?:png|jpg|jpeg|gif|svg|ico|webp|css|js|woff|woff2|ttf|eot)$).*)"],
|
matcher: ["/((?!api|_next/static|_next/image|favicon.ico|.*\\.(?:png|jpg|jpeg|gif|svg|ico|webp|css|js|woff|woff2|ttf|eot|apk)$).*)"],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
importScripts("https://www.gstatic.com/firebasejs/10.12.0/firebase-app-compat.js");
|
importScripts("https://www.gstatic.com/firebasejs/10.12.0/firebase-app-compat.js");
|
||||||
importScripts("https://www.gstatic.com/firebasejs/10.12.0/firebase-messaging-compat.js");
|
importScripts("https://www.gstatic.com/firebasejs/10.12.0/firebase-messaging-compat.js");
|
||||||
|
|
||||||
|
//من فاير بيز
|
||||||
firebase.initializeApp({
|
firebase.initializeApp({
|
||||||
apiKey: "AIzaSyBZV7KBLRJSTApahfrO8lBesmIM3zNRSaY",
|
apiKey: "AIzaSyBZV7KBLRJSTApahfrO8lBesmIM3zNRSaY",
|
||||||
authDomain: "sweet-home-b2766.firebaseapp.com",
|
authDomain: "sweet-home-b2766.firebaseapp.com",
|
||||||
@ -14,6 +15,7 @@ firebase.initializeApp({
|
|||||||
measurementId: "G-M2V95NBJLX",
|
measurementId: "G-M2V95NBJLX",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//ننشئ رسالة
|
||||||
const messaging = firebase.messaging();
|
const messaging = firebase.messaging();
|
||||||
|
|
||||||
// Handle background messages
|
// Handle background messages
|
||||||
@ -31,6 +33,7 @@ messaging.onBackgroundMessage((payload) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Handle notification click
|
// Handle notification click
|
||||||
|
//اذا ضغطنا ع الاشعار شو بصير
|
||||||
self.addEventListener("notificationclick", (event) => {
|
self.addEventListener("notificationclick", (event) => {
|
||||||
event.notification.close();
|
event.notification.close();
|
||||||
const url = event.notification.data?.url || "/";
|
const url = event.notification.data?.url || "/";
|
||||||
|
|||||||
Reference in New Issue
Block a user