2026-05-30 13:24:22 +03:00
|
|
|
"use client";
|
2026-03-30 13:44:52 +00:00
|
|
|
|
2026-05-30 13:24:22 +03:00
|
|
|
import { useState, useEffect, useMemo } from "react";
|
|
|
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|
|
|
|
import toast, { Toaster } from "react-hot-toast";
|
|
|
|
|
import Link from "next/link";
|
|
|
|
|
import { useParams, useRouter } from "next/navigation";
|
2026-03-30 13:44:52 +00:00
|
|
|
import {
|
2026-05-30 13:24:22 +03:00
|
|
|
MapPin,
|
|
|
|
|
Bed,
|
|
|
|
|
Bath,
|
|
|
|
|
Square,
|
|
|
|
|
DollarSign,
|
|
|
|
|
Heart,
|
|
|
|
|
Share2,
|
|
|
|
|
Phone,
|
|
|
|
|
Mail,
|
|
|
|
|
MessageCircle,
|
|
|
|
|
Calendar,
|
|
|
|
|
Shield,
|
|
|
|
|
Star,
|
|
|
|
|
ChevronLeft,
|
|
|
|
|
ChevronRight,
|
|
|
|
|
Check,
|
|
|
|
|
X,
|
|
|
|
|
Wifi,
|
|
|
|
|
Car,
|
|
|
|
|
Wind,
|
|
|
|
|
Camera,
|
|
|
|
|
Home,
|
|
|
|
|
Building2,
|
|
|
|
|
Users,
|
|
|
|
|
Clock,
|
|
|
|
|
FileText,
|
|
|
|
|
LogIn,
|
|
|
|
|
Loader2,
|
|
|
|
|
ArrowLeft,
|
|
|
|
|
ImageIcon,
|
|
|
|
|
ChevronDown,
|
|
|
|
|
Layers,
|
|
|
|
|
Sofa,
|
|
|
|
|
DoorOpen,
|
|
|
|
|
School,
|
|
|
|
|
Hospital,
|
|
|
|
|
Store,
|
|
|
|
|
TreePine,
|
|
|
|
|
Building,
|
|
|
|
|
GraduationCap,
|
|
|
|
|
ExternalLink,
|
|
|
|
|
Smile,
|
|
|
|
|
Ban,
|
|
|
|
|
Wine,
|
|
|
|
|
Dog,
|
|
|
|
|
CassetteTape,
|
|
|
|
|
Info,
|
|
|
|
|
} from "lucide-react";
|
|
|
|
|
import {
|
|
|
|
|
getRentProperty,
|
|
|
|
|
getSaleProperty,
|
|
|
|
|
getSalePropertyById,
|
|
|
|
|
bookReservation,
|
|
|
|
|
getAvailableDateRanges,
|
|
|
|
|
getOwnerContactInformation,
|
|
|
|
|
getMyRentListings,
|
|
|
|
|
getMySaleListings,
|
|
|
|
|
} from "../../utils/api";
|
|
|
|
|
import AuthService from "../../services/AuthService";
|
|
|
|
|
import { useFavorites } from "@/app/contexts/FavoritesContext";
|
|
|
|
|
import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from "../../enums";
|
|
|
|
|
import PropertyRatingList from "@/app/components/ratings/PropertyRatingList";
|
|
|
|
|
import { getPropertyAverageRating } from "../../utils/ratings";
|
|
|
|
|
import dynamic from "next/dynamic";
|
|
|
|
|
import "leaflet/dist/leaflet.css";
|
|
|
|
|
|
|
|
|
|
const MapContainer = dynamic(
|
|
|
|
|
() => import("react-leaflet").then((m) => m.MapContainer),
|
|
|
|
|
{ ssr: false },
|
|
|
|
|
);
|
|
|
|
|
const TileLayer = dynamic(
|
|
|
|
|
() => import("react-leaflet").then((m) => m.TileLayer),
|
|
|
|
|
{ ssr: false },
|
|
|
|
|
);
|
|
|
|
|
const Marker = dynamic(() => import("react-leaflet").then((m) => m.Marker), {
|
|
|
|
|
ssr: false,
|
|
|
|
|
});
|
|
|
|
|
const Popup = dynamic(() => import("react-leaflet").then((m) => m.Popup), {
|
|
|
|
|
ssr: false,
|
|
|
|
|
});
|
2026-04-26 13:46:30 +03:00
|
|
|
|
|
|
|
|
function formatCurrency(amount) {
|
2026-05-30 13:24:22 +03:00
|
|
|
if (!amount || isNaN(amount)) return "0";
|
|
|
|
|
return Number(amount).toLocaleString("ar-SA");
|
2026-03-30 16:28:09 +00:00
|
|
|
}
|
2026-03-30 13:44:52 +00:00
|
|
|
|
2026-05-25 21:27:39 +03:00
|
|
|
function buildImageUrl(img) {
|
2026-05-30 13:24:22 +03:00
|
|
|
if (!img) return "";
|
|
|
|
|
const apiBase =
|
|
|
|
|
typeof window !== "undefined"
|
|
|
|
|
? process.env.NEXT_PUBLIC_API_URL || "https://45.93.137.91.nip.io/api"
|
|
|
|
|
: "";
|
|
|
|
|
if (img.startsWith("http")) return img;
|
|
|
|
|
return `${apiBase}${img.startsWith("/") ? "" : "/Pictures/"}${img}`;
|
2026-03-30 16:28:09 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-25 21:53:57 +03:00
|
|
|
const serviceLabels = {
|
2026-05-30 13:24:22 +03:00
|
|
|
Electricity: "كهرباء",
|
|
|
|
|
Internet: "إنترنت",
|
|
|
|
|
Heating: "تدفئة",
|
|
|
|
|
Water: "ماء",
|
|
|
|
|
Pool: "مسبح",
|
|
|
|
|
PrivateGarden: "حديقة خاصة",
|
|
|
|
|
Parking: "موقف سيارات",
|
|
|
|
|
Security247: "حراسة 24 س",
|
|
|
|
|
CentralHeating: "تدفئة مركزية",
|
|
|
|
|
CentralAirConditioning: "تكييف مركزي",
|
|
|
|
|
EquippedKitchen: "مطبخ مجهز",
|
|
|
|
|
MaidsRoom: "غرفة خادمة",
|
|
|
|
|
Elevator: "مصعد",
|
|
|
|
|
Gym: "نادي رياضي",
|
|
|
|
|
Sauna: "ساونا",
|
|
|
|
|
Jacuzzi: "جاكوزي",
|
|
|
|
|
Balcony: "بلكونة",
|
|
|
|
|
Rooftop: "سطح",
|
|
|
|
|
Furnished: "مفروش",
|
|
|
|
|
AirConditioning: "تكييف",
|
|
|
|
|
SatelliteTV: "تلفاز",
|
|
|
|
|
Fireplace: "مدفأة",
|
|
|
|
|
StudyRoom: "غرفة دراسة",
|
|
|
|
|
Storage: "مستودع",
|
|
|
|
|
Laundry: "غرفة غسيل",
|
|
|
|
|
SmartHome: "منزل ذكي",
|
2026-05-25 21:53:57 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const termLabels = {
|
2026-05-30 13:24:22 +03:00
|
|
|
NoSmoking: "ممنوع التدخين",
|
|
|
|
|
NoAnimals: "ممنوع الحيوانات الأليفة",
|
|
|
|
|
NoParties: "ممنوع الحفلات",
|
|
|
|
|
NoAlcohol: "ممنوع الكحول",
|
|
|
|
|
SuitableForChildren: "مناسب للأطفال",
|
|
|
|
|
SuitableForFamilies: "مناسب للعائلات",
|
|
|
|
|
SuitableForStudents: "مناسب للطلاب",
|
|
|
|
|
SuitableForElderly: "مناسب لكبار السن",
|
|
|
|
|
OnlyFemales: "إناث فقط",
|
|
|
|
|
OnlyMales: "ذكور فقط",
|
2026-05-25 21:53:57 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const proximityLabels = {
|
2026-05-30 13:24:22 +03:00
|
|
|
School: "مدرسة",
|
|
|
|
|
Hospital: "مستشفى",
|
|
|
|
|
Restaurant: "مطعم",
|
|
|
|
|
University: "جامعة",
|
|
|
|
|
Park: "حديقة",
|
|
|
|
|
Mall: "مركز تسوق",
|
|
|
|
|
Supermarket: "سوبر ماركت",
|
|
|
|
|
Pharmacy: "صيدلية",
|
|
|
|
|
Mosque: "مسجد",
|
|
|
|
|
Bank: "بنك",
|
|
|
|
|
Airport: "مطار",
|
|
|
|
|
BusStation: "موقف باص",
|
2026-05-25 21:53:57 +03:00
|
|
|
};
|
|
|
|
|
|
2026-03-30 13:44:52 +00:00
|
|
|
function mapApiDetail(item) {
|
|
|
|
|
if (!item) return null;
|
|
|
|
|
const info = item.propertyInformation || {};
|
2026-05-30 13:24:22 +03:00
|
|
|
const details =
|
|
|
|
|
typeof info.detailsJSON === "object" && info.detailsJSON
|
|
|
|
|
? info.detailsJSON
|
|
|
|
|
: (() => {
|
|
|
|
|
try {
|
|
|
|
|
return JSON.parse(info.detailsJSON || "{}");
|
|
|
|
|
} catch {
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
})();
|
2026-04-28 13:17:57 -07:00
|
|
|
|
2026-05-25 21:27:39 +03:00
|
|
|
const isRent = item.dailyRent != null || item.monthlyRent != null;
|
|
|
|
|
const dailyPrice = item.dailyRent || 0;
|
|
|
|
|
const monthlyPrice = item.monthlyRent || 0;
|
|
|
|
|
const salePrice = item.price || 0;
|
2026-05-30 13:24:22 +03:00
|
|
|
const price = isRent ? monthlyPrice || dailyPrice : salePrice;
|
|
|
|
|
const priceUnit = isRent ? (monthlyPrice ? "monthly" : "daily") : "sale";
|
|
|
|
|
|
|
|
|
|
const propType =
|
|
|
|
|
BuildingTypeKeys[info.buildingType] ??
|
|
|
|
|
BuildingTypeKeys[item.type] ??
|
|
|
|
|
"apartment";
|
|
|
|
|
const typeLabel =
|
|
|
|
|
{
|
|
|
|
|
apartment: "شقة",
|
|
|
|
|
villa: "فيلا",
|
|
|
|
|
house: "بيت",
|
|
|
|
|
sweet: "سويت",
|
|
|
|
|
studio: "استوديو",
|
|
|
|
|
office: "مكتب",
|
|
|
|
|
shop: "متجر",
|
|
|
|
|
warehouse: "مستودع",
|
|
|
|
|
farms: "مزرعة",
|
|
|
|
|
room: "غرفة ضمن شقة",
|
|
|
|
|
}[propType] || "عقار";
|
|
|
|
|
const status =
|
|
|
|
|
PropertyStatusKeys[info.status] ??
|
|
|
|
|
PropertyStatusKeys[item.status] ??
|
|
|
|
|
"available";
|
|
|
|
|
const statusLabel =
|
|
|
|
|
{
|
|
|
|
|
available: "متاح",
|
|
|
|
|
booked: "محجوز",
|
|
|
|
|
notAvailable: "غير متاح",
|
|
|
|
|
maintenance: "صيانة",
|
|
|
|
|
}[status] || "متاح";
|
2026-04-28 13:17:57 -07:00
|
|
|
|
2026-05-25 21:27:39 +03:00
|
|
|
const rawImages = Array.isArray(info.images) ? info.images : [];
|
2026-05-25 21:53:57 +03:00
|
|
|
const photosFallback = Array.isArray(details.photos) ? details.photos : [];
|
|
|
|
|
const allRaw = rawImages.length > 0 ? rawImages : photosFallback;
|
|
|
|
|
const images = allRaw.length > 0 ? allRaw.map(buildImageUrl) : [];
|
2026-04-28 13:17:57 -07:00
|
|
|
|
2026-05-25 22:14:37 +03:00
|
|
|
// Normalize services: Flutter sends list of strings or object with boolean values
|
|
|
|
|
const rawServices = details.services || {};
|
2026-05-30 13:24:22 +03:00
|
|
|
const serviceList = Array.isArray(rawServices)
|
|
|
|
|
? rawServices
|
|
|
|
|
: Object.keys(rawServices).filter((k) => rawServices[k]);
|
2026-05-25 22:14:37 +03:00
|
|
|
const serviceDetails = details.serviceDetails || {};
|
|
|
|
|
const services = {};
|
2026-05-30 13:24:22 +03:00
|
|
|
serviceList.forEach((s) => {
|
|
|
|
|
services[s] = serviceDetails[s] || true;
|
|
|
|
|
});
|
2026-05-25 22:14:37 +03:00
|
|
|
|
2026-05-25 23:55:02 +03:00
|
|
|
const rawTerms = details.terms || {};
|
2026-05-30 13:24:22 +03:00
|
|
|
const terms = Array.isArray(rawTerms)
|
|
|
|
|
? rawTerms.reduce((acc, t) => ({ ...acc, [t]: true }), {})
|
|
|
|
|
: rawTerms;
|
2026-05-25 22:14:37 +03:00
|
|
|
|
|
|
|
|
// Try multiple key aliases like Flutter does
|
|
|
|
|
const floor = details.floorNumber ?? details.floor ?? 0;
|
2026-05-30 13:24:22 +03:00
|
|
|
const salons =
|
|
|
|
|
details.numberOfSalons ?? details.salonsCount ?? details.salons ?? 0;
|
|
|
|
|
const balconies =
|
|
|
|
|
details.numberOfBalconies ??
|
|
|
|
|
details.balconiesCount ??
|
|
|
|
|
details.balconies ??
|
|
|
|
|
0;
|
2026-05-25 23:55:02 +03:00
|
|
|
const rawProximity = details.nearbyDistances || details.proximity || {};
|
|
|
|
|
const proximity = {};
|
|
|
|
|
Object.entries(rawProximity).forEach(([k, v]) => {
|
|
|
|
|
if (!v) return;
|
|
|
|
|
const normalizedKey = k.charAt(0).toUpperCase() + k.slice(1);
|
|
|
|
|
proximity[normalizedKey] = v;
|
|
|
|
|
});
|
2026-05-25 22:14:37 +03:00
|
|
|
const roomDetails = details.room || details.roomDetails || {};
|
2026-03-30 13:44:52 +00:00
|
|
|
|
2026-05-30 13:24:22 +03:00
|
|
|
const displayType =
|
|
|
|
|
details.displayType ||
|
|
|
|
|
(isRent
|
|
|
|
|
? monthlyPrice && dailyPrice
|
|
|
|
|
? "Both"
|
|
|
|
|
: monthlyPrice
|
|
|
|
|
? "Monthly"
|
|
|
|
|
: "Daily"
|
|
|
|
|
: "Sale");
|
|
|
|
|
const propertyCondition = details.propertyCondition || "";
|
|
|
|
|
const furnished = propertyCondition.toLowerCase().includes("furniture")
|
|
|
|
|
? propertyCondition.toLowerCase() === "withfurniture"
|
|
|
|
|
: !!item.isFurnished;
|
2026-05-26 00:20:20 +03:00
|
|
|
|
2026-03-30 13:44:52 +00:00
|
|
|
return {
|
|
|
|
|
id: item.id,
|
2026-05-25 22:14:37 +03:00
|
|
|
propertyInformationId: info.id,
|
2026-05-25 21:53:57 +03:00
|
|
|
title: details.description || info.address || `عقار #${item.id}`,
|
2026-05-30 13:24:22 +03:00
|
|
|
description: details.description || info.description || "",
|
2026-03-30 13:44:52 +00:00
|
|
|
type: propType,
|
2026-05-25 21:27:39 +03:00
|
|
|
typeLabel,
|
|
|
|
|
price,
|
2026-04-26 13:46:30 +03:00
|
|
|
priceUnit,
|
2026-05-25 21:27:39 +03:00
|
|
|
priceDisplay: { daily: dailyPrice, monthly: monthlyPrice, sale: salePrice },
|
|
|
|
|
isRent,
|
2026-05-26 00:20:20 +03:00
|
|
|
displayType,
|
|
|
|
|
propertyCondition,
|
|
|
|
|
furnished,
|
2026-03-30 13:44:52 +00:00
|
|
|
location: {
|
2026-05-30 13:24:22 +03:00
|
|
|
city: extractCity(info.address) || "دمشق",
|
|
|
|
|
address: info.address || "",
|
2026-05-25 21:27:39 +03:00
|
|
|
lat: parseFloat(info.cordsX) || 0,
|
|
|
|
|
lng: parseFloat(info.cordsY) || 0,
|
2026-03-30 13:44:52 +00:00
|
|
|
},
|
|
|
|
|
bedrooms: info.numberOfBedRooms || 0,
|
|
|
|
|
bathrooms: info.numberOfBathRooms || 0,
|
|
|
|
|
area: info.space || 0,
|
2026-05-25 22:14:37 +03:00
|
|
|
floor,
|
|
|
|
|
salons,
|
|
|
|
|
balconies,
|
2026-03-30 13:44:52 +00:00
|
|
|
images,
|
|
|
|
|
status,
|
2026-05-25 21:27:39 +03:00
|
|
|
statusLabel,
|
|
|
|
|
services,
|
|
|
|
|
terms,
|
2026-05-25 21:53:57 +03:00
|
|
|
proximity,
|
|
|
|
|
roomDetails,
|
2026-05-25 21:27:39 +03:00
|
|
|
details,
|
2026-05-25 23:07:29 +03:00
|
|
|
bookedCount: details.bookedCount || 0,
|
2026-04-26 13:46:30 +03:00
|
|
|
deposit: item.deposit || 0,
|
2026-05-25 21:27:39 +03:00
|
|
|
currencyId: item.currencyId,
|
|
|
|
|
isSmokeAllow: item.isSmokeAllow,
|
|
|
|
|
isVisitorAllow: item.isVisitorAllow,
|
|
|
|
|
specializedFor: item.specializedFor,
|
2026-05-26 18:43:47 +03:00
|
|
|
ownerId: info.ownerId ?? info.userId ?? item.ownerId ?? item.userId ?? null,
|
2026-03-30 13:44:52 +00:00
|
|
|
_raw: item,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function PropertyDetailsPage() {
|
|
|
|
|
const params = useParams();
|
2026-05-25 21:27:39 +03:00
|
|
|
const router = useRouter();
|
2026-03-30 17:54:42 +00:00
|
|
|
const { isFavorite, addFavorite, removeFavorite } = useFavorites();
|
2026-04-26 13:46:30 +03:00
|
|
|
|
|
|
|
|
const [property, setProperty] = useState(null);
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
2026-03-30 13:44:52 +00:00
|
|
|
const [currentImage, setCurrentImage] = useState(0);
|
2026-04-26 13:46:30 +03:00
|
|
|
const [showLoginDialog, setShowLoginDialog] = useState(false);
|
2026-05-30 13:24:22 +03:00
|
|
|
const [bookingDates, setBookingDates] = useState({ start: "", end: "" });
|
2026-05-25 21:27:39 +03:00
|
|
|
const [bookingLoading, setBookingLoading] = useState(false);
|
2026-03-30 13:44:52 +00:00
|
|
|
const [bookingError, setBookingError] = useState(null);
|
|
|
|
|
const [bookingSuccess, setBookingSuccess] = useState(false);
|
|
|
|
|
const [availableRanges, setAvailableRanges] = useState([]);
|
2026-05-30 13:24:22 +03:00
|
|
|
const [bookingStep, setBookingStep] = useState("entry");
|
2026-05-26 16:38:16 +03:00
|
|
|
const [selectedStart, setSelectedStart] = useState(null);
|
|
|
|
|
const [selectedEnd, setSelectedEnd] = useState(null);
|
2026-05-30 13:24:22 +03:00
|
|
|
const [calendarMonth, setCalendarMonth] = useState(() =>
|
|
|
|
|
new Date().getMonth(),
|
|
|
|
|
);
|
|
|
|
|
const [calendarYear, setCalendarYear] = useState(() =>
|
|
|
|
|
new Date().getFullYear(),
|
|
|
|
|
);
|
|
|
|
|
const [pricingMode, setPricingMode] = useState("daily");
|
2026-05-26 17:31:01 +03:00
|
|
|
const [isOwnProperty, setIsOwnProperty] = useState(false);
|
2026-05-25 21:27:39 +03:00
|
|
|
const [favLoading, setFavLoading] = useState(false);
|
|
|
|
|
const [avgRating, setAvgRating] = useState(null);
|
|
|
|
|
const [showContact, setShowContact] = useState(false);
|
|
|
|
|
const [contactInfo, setContactInfo] = useState(null);
|
|
|
|
|
const [ownerData, setOwnerData] = useState(null);
|
2026-03-30 13:44:52 +00:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-05-25 21:27:39 +03:00
|
|
|
const id = params.id;
|
|
|
|
|
if (!id) return;
|
|
|
|
|
setLoading(true);
|
2026-04-28 13:17:57 -07:00
|
|
|
|
2026-05-25 21:27:39 +03:00
|
|
|
async function fetchProperty() {
|
2026-03-30 13:44:52 +00:00
|
|
|
try {
|
|
|
|
|
let data = null;
|
2026-05-30 13:24:22 +03:00
|
|
|
try {
|
|
|
|
|
data = await getRentProperty(id);
|
|
|
|
|
} catch {}
|
|
|
|
|
if (!data) {
|
|
|
|
|
try {
|
|
|
|
|
data =
|
|
|
|
|
(await getSalePropertyById(id)) || (await getSaleProperty(id));
|
|
|
|
|
} catch {}
|
|
|
|
|
}
|
|
|
|
|
if (!data) {
|
|
|
|
|
try {
|
|
|
|
|
data = await getSaleProperty(id);
|
|
|
|
|
} catch {}
|
|
|
|
|
}
|
2026-03-30 13:44:52 +00:00
|
|
|
|
|
|
|
|
if (data) {
|
2026-05-25 21:27:39 +03:00
|
|
|
const mapped = mapApiDetail(data);
|
|
|
|
|
setProperty(mapped);
|
|
|
|
|
if (mapped) fetchAvgRating(mapped.id);
|
2026-05-26 16:38:16 +03:00
|
|
|
if (mapped && mapped.isRent) {
|
|
|
|
|
try {
|
2026-05-30 13:24:22 +03:00
|
|
|
const propInfoId =
|
|
|
|
|
mapped._raw?.propertyInformationId || mapped.id;
|
2026-05-26 16:38:16 +03:00
|
|
|
const ranges = await getAvailableDateRanges(propInfoId);
|
|
|
|
|
if (ranges && Array.isArray(ranges)) {
|
|
|
|
|
setAvailableRanges(ranges);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
2026-05-30 13:24:22 +03:00
|
|
|
console.warn("Failed to fetch date ranges", e);
|
2026-05-26 16:38:16 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-26 19:18:49 +03:00
|
|
|
// Check if current user owns this property via their own listings
|
2026-05-26 19:04:57 +03:00
|
|
|
if (AuthService.isAuthenticated() && AuthService.isOwner()) {
|
|
|
|
|
try {
|
2026-05-26 20:24:37 +03:00
|
|
|
const [myRent, mySale] = await Promise.allSettled([
|
|
|
|
|
getMyRentListings(),
|
|
|
|
|
getMySaleListings(),
|
|
|
|
|
]);
|
2026-05-26 19:25:10 +03:00
|
|
|
const myPropIds = new Set();
|
2026-05-26 20:24:37 +03:00
|
|
|
const collectIds = (result) => {
|
2026-05-30 13:24:22 +03:00
|
|
|
if (result.status !== "fulfilled" || !result.value) return;
|
|
|
|
|
const list = Array.isArray(result.value)
|
|
|
|
|
? result.value
|
|
|
|
|
: [result.value];
|
|
|
|
|
list.filter(Boolean).forEach((p) => {
|
2026-05-26 20:24:37 +03:00
|
|
|
const info = p.propertyInformation || {};
|
|
|
|
|
if (info.id) myPropIds.add(Number(info.id));
|
|
|
|
|
if (p.id) myPropIds.add(Number(p.id));
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
collectIds(myRent);
|
|
|
|
|
collectIds(mySale);
|
2026-05-26 19:25:10 +03:00
|
|
|
const propInfoId = mapped._raw?.propertyInformation?.id;
|
2026-05-30 13:24:22 +03:00
|
|
|
if (
|
|
|
|
|
myPropIds.has(Number(mapped.id)) ||
|
|
|
|
|
(propInfoId && myPropIds.has(Number(propInfoId)))
|
|
|
|
|
) {
|
2026-05-26 19:18:49 +03:00
|
|
|
setIsOwnProperty(true);
|
2026-05-26 19:04:57 +03:00
|
|
|
}
|
|
|
|
|
} catch (e) {
|
2026-05-30 13:24:22 +03:00
|
|
|
console.warn("[OwnerCheck] failed:", e);
|
2026-05-26 18:22:21 +03:00
|
|
|
}
|
2026-05-26 17:31:01 +03:00
|
|
|
}
|
2026-03-30 13:44:52 +00:00
|
|
|
}
|
|
|
|
|
} catch (err) {
|
2026-05-30 13:24:22 +03:00
|
|
|
console.error("[PropertyDetail] Failed:", err);
|
2026-03-30 13:44:52 +00:00
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 21:27:39 +03:00
|
|
|
fetchProperty();
|
|
|
|
|
}, [params.id]);
|
2026-03-30 13:44:52 +00:00
|
|
|
|
2026-05-25 21:27:39 +03:00
|
|
|
const fetchAvgRating = async (propId) => {
|
|
|
|
|
try {
|
|
|
|
|
const avg = await getPropertyAverageRating(propId);
|
|
|
|
|
setAvgRating(avg);
|
|
|
|
|
} catch {}
|
2026-05-26 23:00:19 +03:00
|
|
|
};
|
2026-04-28 13:17:57 -07:00
|
|
|
|
2026-05-25 21:27:39 +03:00
|
|
|
const fetchContactInfo = async () => {
|
|
|
|
|
if (!property) return;
|
|
|
|
|
try {
|
2026-05-30 13:24:22 +03:00
|
|
|
const info = await getOwnerContactInformation(
|
|
|
|
|
property._raw?.propertyInformationId || property.id,
|
|
|
|
|
);
|
2026-05-25 21:27:39 +03:00
|
|
|
setContactInfo(info);
|
|
|
|
|
setShowContact(true);
|
|
|
|
|
} catch (err) {
|
2026-05-30 13:24:22 +03:00
|
|
|
toast.error("فشل تحميل معلومات الاتصال");
|
2026-03-30 13:44:52 +00:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-25 21:27:39 +03:00
|
|
|
const handleFavorite = async () => {
|
2026-05-30 13:24:22 +03:00
|
|
|
if (!AuthService.isAuthenticated()) {
|
|
|
|
|
setShowLoginDialog(true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-05-25 21:27:39 +03:00
|
|
|
if (!property) return;
|
|
|
|
|
setFavLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
if (isFavorite(property.id)) {
|
|
|
|
|
await removeFavorite(property.id);
|
2026-03-30 13:44:52 +00:00
|
|
|
} else {
|
2026-05-25 21:27:39 +03:00
|
|
|
await addFavorite(property.id);
|
2026-03-30 13:44:52 +00:00
|
|
|
}
|
2026-05-25 21:27:39 +03:00
|
|
|
} catch (err) {
|
2026-05-30 13:24:22 +03:00
|
|
|
toast.error("حدث خطأ");
|
2026-05-25 21:27:39 +03:00
|
|
|
} finally {
|
|
|
|
|
setFavLoading(false);
|
2026-03-30 13:44:52 +00:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-25 21:27:39 +03:00
|
|
|
const handleBookNow = async () => {
|
2026-05-30 13:24:22 +03:00
|
|
|
if (!AuthService.isAuthenticated()) {
|
|
|
|
|
setShowLoginDialog(true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-03-30 13:44:52 +00:00
|
|
|
if (!bookingDates.start || !bookingDates.end) {
|
2026-05-30 13:24:22 +03:00
|
|
|
setBookingError("يرجى تحديد تاريخ البداية والنهاية");
|
2026-04-28 12:57:06 -07:00
|
|
|
return;
|
|
|
|
|
}
|
2026-05-25 21:27:39 +03:00
|
|
|
setBookingLoading(true);
|
|
|
|
|
setBookingError(null);
|
2026-03-30 13:44:52 +00:00
|
|
|
try {
|
2026-05-30 13:24:22 +03:00
|
|
|
await bookReservation(
|
|
|
|
|
property._raw?.propertyInformationId || property.id,
|
|
|
|
|
bookingDates.start,
|
|
|
|
|
bookingDates.end,
|
|
|
|
|
);
|
2026-03-30 13:44:52 +00:00
|
|
|
setBookingSuccess(true);
|
2026-05-30 13:24:22 +03:00
|
|
|
toast.success("تم إرسال طلب الحجز بنجاح");
|
2026-03-30 13:44:52 +00:00
|
|
|
} catch (err) {
|
2026-05-30 13:24:22 +03:00
|
|
|
setBookingError(err.message || "فشل الحجز");
|
2026-04-28 13:17:57 -07:00
|
|
|
} finally {
|
|
|
|
|
setBookingLoading(false);
|
2026-03-30 13:44:52 +00:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-30 13:24:22 +03:00
|
|
|
const MONTHS_AR = [
|
|
|
|
|
"يناير",
|
|
|
|
|
"فبراير",
|
|
|
|
|
"مارس",
|
|
|
|
|
"أبريل",
|
|
|
|
|
"مايو",
|
|
|
|
|
"يونيو",
|
|
|
|
|
"يوليو",
|
|
|
|
|
"أغسطس",
|
|
|
|
|
"سبتمبر",
|
|
|
|
|
"أكتوبر",
|
|
|
|
|
"نوفمبر",
|
|
|
|
|
"ديسمبر",
|
|
|
|
|
];
|
|
|
|
|
const DAYS_AR = ["ح", "ن", "ث", "ر", "خ", "ج", "س"];
|
2026-05-26 16:38:16 +03:00
|
|
|
|
|
|
|
|
const availableDatesSet = useMemo(() => {
|
|
|
|
|
const dates = new Set();
|
|
|
|
|
if (!Array.isArray(availableRanges)) return dates;
|
2026-05-30 13:24:22 +03:00
|
|
|
availableRanges.forEach((r) => {
|
2026-05-26 16:38:16 +03:00
|
|
|
const start = new Date(r.startDate || r.start);
|
|
|
|
|
const end = new Date(r.endDate || r.end);
|
|
|
|
|
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
|
2026-05-30 13:24:22 +03:00
|
|
|
dates.add(d.toISOString().split("T")[0]);
|
2026-05-26 16:38:16 +03:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return dates;
|
|
|
|
|
}, [availableRanges]);
|
|
|
|
|
|
|
|
|
|
const isDateAvailable = (dateStr) => availableDatesSet.has(dateStr);
|
|
|
|
|
const isPastDate = (dateStr) => {
|
|
|
|
|
const today = new Date();
|
|
|
|
|
today.setHours(0, 0, 0, 0);
|
|
|
|
|
return new Date(dateStr) < today;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDayClick = (dateStr) => {
|
2026-05-30 13:24:22 +03:00
|
|
|
if (bookingStep === "entry") {
|
2026-05-26 16:38:16 +03:00
|
|
|
setSelectedStart(dateStr);
|
|
|
|
|
setSelectedEnd(null);
|
2026-05-30 13:24:22 +03:00
|
|
|
setBookingStep("exit");
|
2026-05-26 16:38:16 +03:00
|
|
|
} else {
|
|
|
|
|
if (new Date(dateStr) <= new Date(selectedStart)) {
|
|
|
|
|
setSelectedStart(dateStr);
|
|
|
|
|
setSelectedEnd(null);
|
2026-05-30 13:24:22 +03:00
|
|
|
setBookingStep("exit");
|
2026-05-26 16:38:16 +03:00
|
|
|
} else {
|
|
|
|
|
setSelectedEnd(dateStr);
|
2026-05-30 13:24:22 +03:00
|
|
|
setBookingStep("entry");
|
2026-05-26 16:38:16 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleBookingConfirm = async () => {
|
2026-05-30 13:24:22 +03:00
|
|
|
if (!AuthService.isAuthenticated()) {
|
|
|
|
|
setShowLoginDialog(true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-05-26 16:38:16 +03:00
|
|
|
if (!selectedStart || !selectedEnd) {
|
2026-05-30 13:24:22 +03:00
|
|
|
setBookingError("يرجى تحديد تاريخ البداية والنهاية");
|
2026-05-26 16:38:16 +03:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setBookingLoading(true);
|
|
|
|
|
setBookingError(null);
|
|
|
|
|
try {
|
|
|
|
|
const propInfoId = property._raw?.propertyInformationId || property.id;
|
2026-05-30 13:24:22 +03:00
|
|
|
const startDate = new Date(selectedStart + "T00:00:00.000").toISOString();
|
|
|
|
|
const endDate = new Date(selectedEnd + "T00:00:00.000").toISOString();
|
2026-05-26 16:38:16 +03:00
|
|
|
await bookReservation(propInfoId, startDate, endDate);
|
|
|
|
|
setBookingSuccess(true);
|
2026-05-30 13:24:22 +03:00
|
|
|
toast.success("تم إرسال طلب الحجز بنجاح");
|
2026-05-26 16:38:16 +03:00
|
|
|
} catch (err) {
|
2026-05-30 13:24:22 +03:00
|
|
|
setBookingError(err.message || "فشل الحجز");
|
2026-05-26 16:38:16 +03:00
|
|
|
} finally {
|
|
|
|
|
setBookingLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const navigateMonth = (delta) => {
|
|
|
|
|
let month = calendarMonth + delta;
|
|
|
|
|
let year = calendarYear;
|
2026-05-30 13:24:22 +03:00
|
|
|
if (month < 0) {
|
|
|
|
|
month = 11;
|
|
|
|
|
year--;
|
|
|
|
|
}
|
|
|
|
|
if (month > 11) {
|
|
|
|
|
month = 0;
|
|
|
|
|
year++;
|
|
|
|
|
}
|
2026-05-26 16:38:16 +03:00
|
|
|
setCalendarMonth(month);
|
|
|
|
|
setCalendarYear(year);
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-25 21:27:39 +03:00
|
|
|
const handleRatingSuccess = () => {
|
|
|
|
|
setShowRatingForm(false);
|
|
|
|
|
if (property) fetchAvgRating(property.id);
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-30 13:44:52 +00:00
|
|
|
if (loading) {
|
|
|
|
|
return (
|
2026-05-25 21:27:39 +03:00
|
|
|
<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>
|
2026-03-30 13:44:52 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!property) {
|
|
|
|
|
return (
|
2026-05-25 21:27:39 +03:00
|
|
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
|
|
|
|
<div className="text-center max-w-md">
|
|
|
|
|
<div className="w-24 h-24 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
|
|
|
<Home className="w-12 h-12 text-amber-600" />
|
|
|
|
|
</div>
|
2026-05-30 13:24:22 +03:00
|
|
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">
|
|
|
|
|
العقار غير موجود
|
|
|
|
|
</h2>
|
2026-05-25 21:27:39 +03:00
|
|
|
<p className="text-gray-600 mb-6">لم يتم العثور على العقار المطلوب</p>
|
2026-05-30 13:24:22 +03:00
|
|
|
<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"
|
|
|
|
|
>
|
2026-05-25 21:27:39 +03:00
|
|
|
<ArrowLeft className="w-5 h-5" />
|
|
|
|
|
العودة إلى العقارات
|
|
|
|
|
</Link>
|
|
|
|
|
</div>
|
2026-03-30 13:44:52 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 21:27:39 +03:00
|
|
|
const isFav = isFavorite(property.id);
|
2026-05-30 13:24:22 +03:00
|
|
|
const isRoomType = property.type === "room";
|
2026-05-25 21:53:57 +03:00
|
|
|
const isMostRequested = avgRating !== null && avgRating >= 4.5;
|
2026-05-30 13:24:22 +03:00
|
|
|
const showPricingToggle =
|
|
|
|
|
property.isRent &&
|
|
|
|
|
property.priceDisplay?.daily > 0 &&
|
|
|
|
|
property.priceDisplay?.monthly > 0;
|
|
|
|
|
const effectivePricingMode = showPricingToggle
|
|
|
|
|
? pricingMode
|
|
|
|
|
: property.isRent && property.priceDisplay?.monthly > 0
|
|
|
|
|
? "monthly"
|
|
|
|
|
: "daily";
|
2026-04-28 13:17:57 -07:00
|
|
|
|
2026-03-30 13:44:52 +00:00
|
|
|
return (
|
2026-05-25 21:27:39 +03:00
|
|
|
<div className="min-h-screen bg-gray-50" dir="rtl">
|
|
|
|
|
<Toaster position="top-center" reverseOrder={false} />
|
|
|
|
|
|
|
|
|
|
<div className="container mx-auto px-4 py-6">
|
2026-05-30 13:24:22 +03:00
|
|
|
<motion.div
|
|
|
|
|
initial={{ opacity: 0, y: -10 }}
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
className="mb-6"
|
|
|
|
|
>
|
|
|
|
|
<Link
|
|
|
|
|
href="/properties"
|
|
|
|
|
className="inline-flex items-center gap-2 text-gray-600 hover:text-amber-600 transition-colors"
|
|
|
|
|
>
|
2026-05-25 21:27:39 +03:00
|
|
|
<ArrowLeft className="w-5 h-5" />
|
|
|
|
|
العودة إلى العقارات
|
|
|
|
|
</Link>
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
|
|
|
<div className="lg:col-span-2 space-y-5">
|
2026-05-25 21:27:39 +03:00
|
|
|
{/* Image Gallery */}
|
2026-05-30 13:24:22 +03:00
|
|
|
<motion.div
|
|
|
|
|
initial={{ opacity: 0, y: 20 }}
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
className="bg-white rounded-2xl overflow-hidden shadow-sm border border-gray-200"
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
className="relative bg-gray-900"
|
|
|
|
|
style={{ minHeight: "380px", maxHeight: "460px" }}
|
|
|
|
|
>
|
2026-05-25 21:27:39 +03:00
|
|
|
{property.images.length > 0 ? (
|
2026-05-30 13:24:22 +03:00
|
|
|
<img
|
|
|
|
|
src={property.images[currentImage]}
|
|
|
|
|
alt={property.title}
|
2026-05-25 21:53:57 +03:00
|
|
|
className="w-full h-full object-contain mx-auto"
|
2026-05-30 13:24:22 +03:00
|
|
|
style={{ minHeight: "380px", maxHeight: "460px" }}
|
|
|
|
|
/>
|
2026-05-25 21:27:39 +03:00
|
|
|
) : (
|
2026-05-30 13:24:22 +03:00
|
|
|
<div
|
|
|
|
|
className="w-full h-full flex items-center justify-center"
|
|
|
|
|
style={{ minHeight: "420px" }}
|
|
|
|
|
>
|
2026-05-25 21:53:57 +03:00
|
|
|
<div className="text-center">
|
|
|
|
|
<ImageIcon className="w-20 h-20 text-gray-500 mx-auto mb-2" />
|
|
|
|
|
<p className="text-gray-400 text-sm">لا توجد صور</p>
|
|
|
|
|
</div>
|
2026-05-25 21:27:39 +03:00
|
|
|
</div>
|
|
|
|
|
)}
|
2026-05-25 21:53:57 +03:00
|
|
|
|
|
|
|
|
{isMostRequested && (
|
|
|
|
|
<div className="absolute top-4 right-4 z-10">
|
|
|
|
|
<span className="bg-gradient-to-l from-amber-500 to-amber-600 text-white px-3 py-1.5 rounded-full text-xs font-bold shadow-lg flex items-center gap-1">
|
|
|
|
|
<Star className="w-3 h-3 fill-white" /> الأكثر طلباً
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-05-25 21:27:39 +03:00
|
|
|
{property.images.length > 1 && (
|
2026-03-30 16:08:35 +00:00
|
|
|
<>
|
2026-05-30 13:24:22 +03:00
|
|
|
<button
|
|
|
|
|
onClick={() =>
|
|
|
|
|
setCurrentImage(
|
|
|
|
|
(prev) =>
|
|
|
|
|
(prev - 1 + property.images.length) %
|
|
|
|
|
property.images.length,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
className="absolute right-4 top-1/2 -translate-y-1/2 bg-black/60 text-white p-3 rounded-full hover:bg-black/80 transition-all shadow-lg"
|
|
|
|
|
>
|
2026-05-25 21:27:39 +03:00
|
|
|
<ChevronRight className="w-5 h-5" />
|
|
|
|
|
</button>
|
2026-05-30 13:24:22 +03:00
|
|
|
<button
|
|
|
|
|
onClick={() =>
|
|
|
|
|
setCurrentImage(
|
|
|
|
|
(prev) => (prev + 1) % property.images.length,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
className="absolute left-4 top-1/2 -translate-y-1/2 bg-black/60 text-white p-3 rounded-full hover:bg-black/80 transition-all shadow-lg"
|
|
|
|
|
>
|
2026-05-25 21:27:39 +03:00
|
|
|
<ChevronLeft className="w-5 h-5" />
|
|
|
|
|
</button>
|
2026-03-30 16:08:35 +00:00
|
|
|
</>
|
|
|
|
|
)}
|
2026-05-25 21:53:57 +03:00
|
|
|
|
|
|
|
|
<div className="absolute bottom-4 right-4 flex gap-2 z-10">
|
2026-05-30 13:24:22 +03:00
|
|
|
<span className="bg-black/70 text-white px-3 py-1 rounded-full text-sm font-medium backdrop-blur-sm">
|
|
|
|
|
{property.statusLabel}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="bg-black/70 text-white px-3 py-1 rounded-full text-sm font-medium backdrop-blur-sm">
|
|
|
|
|
{property.typeLabel}
|
|
|
|
|
</span>
|
2026-05-25 21:53:57 +03:00
|
|
|
</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">
|
|
|
|
|
{currentImage + 1} / {property.images.length || 1}
|
2026-03-30 13:44:52 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-05-25 21:53:57 +03:00
|
|
|
|
2026-05-25 21:27:39 +03:00
|
|
|
{property.images.length > 1 && (
|
2026-05-30 13:24:22 +03:00
|
|
|
<div
|
|
|
|
|
className="flex gap-2 p-3 bg-gray-50 overflow-x-auto"
|
|
|
|
|
style={{ scrollBehavior: "smooth" }}
|
|
|
|
|
>
|
2026-05-25 21:27:39 +03:00
|
|
|
{property.images.map((img, idx) => (
|
2026-05-30 13:24:22 +03:00
|
|
|
<button
|
|
|
|
|
key={idx}
|
|
|
|
|
onClick={() => setCurrentImage(idx)}
|
|
|
|
|
className={`flex-shrink-0 w-24 h-20 rounded-xl overflow-hidden border-2 transition-all duration-200 ${idx === currentImage ? "border-amber-500 ring-2 ring-amber-200 shadow-md" : "border-gray-200 opacity-60 hover:opacity-100"}`}
|
|
|
|
|
>
|
|
|
|
|
<img
|
|
|
|
|
src={img}
|
|
|
|
|
alt=""
|
|
|
|
|
className="w-full h-full object-cover"
|
|
|
|
|
/>
|
2026-05-25 21:27:39 +03:00
|
|
|
</button>
|
|
|
|
|
))}
|
2026-03-30 13:44:52 +00:00
|
|
|
</div>
|
2026-05-25 21:27:39 +03:00
|
|
|
)}
|
|
|
|
|
</motion.div>
|
2026-04-28 13:17:57 -07:00
|
|
|
|
2026-05-25 21:27:39 +03:00
|
|
|
{/* Property Info */}
|
2026-05-30 13:24:22 +03:00
|
|
|
<motion.div
|
|
|
|
|
initial={{ opacity: 0, y: 20 }}
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
className="bg-white rounded-2xl p-5 shadow-sm border border-gray-200"
|
|
|
|
|
>
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="flex justify-between items-start mb-3">
|
2026-04-28 13:17:57 -07:00
|
|
|
<div>
|
2026-05-26 00:20:20 +03:00
|
|
|
<div className="flex items-center gap-1.5 mb-1 flex-wrap">
|
2026-05-30 13:24:22 +03:00
|
|
|
<span className="px-2 py-0.5 bg-amber-100 text-amber-800 rounded-full text-xs">
|
|
|
|
|
{property.typeLabel}
|
|
|
|
|
</span>
|
|
|
|
|
<span
|
|
|
|
|
className={`px-2 py-0.5 rounded-full text-xs ${property.status === "available" ? "bg-green-100 text-green-800" : "bg-yellow-100 text-yellow-800"}`}
|
|
|
|
|
>
|
|
|
|
|
{property.statusLabel}
|
|
|
|
|
</span>
|
2026-05-26 00:20:20 +03:00
|
|
|
{property.isRent && property.displayType && (
|
|
|
|
|
<span className="px-2 py-0.5 bg-blue-100 text-blue-800 rounded-full text-xs">
|
|
|
|
|
{(() => {
|
|
|
|
|
const dt = property.displayType.toLowerCase();
|
2026-05-30 13:24:22 +03:00
|
|
|
if (dt === "both" || dt.includes("both"))
|
|
|
|
|
return "يومي وشهري";
|
|
|
|
|
if (dt.includes("daily")) return "يومي";
|
|
|
|
|
if (dt.includes("monthly")) return "شهري";
|
2026-05-26 00:20:20 +03:00
|
|
|
return dt;
|
|
|
|
|
})()}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
2026-05-30 13:24:22 +03:00
|
|
|
<span
|
|
|
|
|
className={`px-2 py-0.5 rounded-full text-xs ${property.furnished ? "bg-purple-100 text-purple-800" : "bg-gray-100 text-gray-600"}`}
|
|
|
|
|
>
|
|
|
|
|
{property.furnished ? "مفروش" : "غير مفروش"}
|
2026-05-26 00:20:20 +03:00
|
|
|
</span>
|
2026-05-25 21:27:39 +03:00
|
|
|
</div>
|
2026-05-30 13:24:22 +03:00
|
|
|
<h1 className="text-xl font-bold text-gray-900">
|
|
|
|
|
{property.title}
|
|
|
|
|
</h1>
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="flex items-center gap-1 text-gray-500 text-xs mt-0.5">
|
|
|
|
|
<MapPin className="w-3 h-3" />
|
2026-05-30 13:24:22 +03:00
|
|
|
<span>
|
|
|
|
|
{property.location.address || property.location.city}
|
|
|
|
|
</span>
|
2026-05-25 21:27:39 +03:00
|
|
|
</div>
|
2026-03-30 13:44:52 +00:00
|
|
|
</div>
|
2026-05-30 13:24:22 +03:00
|
|
|
<button
|
|
|
|
|
onClick={handleFavorite}
|
|
|
|
|
disabled={favLoading}
|
|
|
|
|
className="p-1.5 rounded-full hover:bg-gray-100 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<Heart
|
|
|
|
|
className={`w-5 h-5 ${isFav ? "fill-red-500 text-red-500" : "text-gray-400"}`}
|
|
|
|
|
/>
|
2026-05-25 22:42:53 +03:00
|
|
|
</button>
|
2026-05-25 21:27:39 +03:00
|
|
|
</div>
|
2026-04-28 13:17:57 -07:00
|
|
|
|
2026-05-25 21:27:39 +03:00
|
|
|
{/* Price */}
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="bg-amber-50 rounded-xl p-3 mb-4">
|
2026-05-25 21:27:39 +03:00
|
|
|
{property.isRent ? (
|
2026-05-25 21:53:57 +03:00
|
|
|
<div className="flex flex-wrap gap-6 items-end">
|
2026-05-25 21:27:39 +03:00
|
|
|
{property.priceDisplay.monthly > 0 && (
|
|
|
|
|
<div>
|
2026-05-30 13:24:22 +03:00
|
|
|
<span className="text-2xl font-bold text-amber-600">
|
|
|
|
|
{formatCurrency(property.priceDisplay.monthly)}
|
|
|
|
|
</span>
|
2026-05-25 21:27:39 +03:00
|
|
|
<span className="text-gray-500 mr-1">ل.س / شهرياً</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{property.priceDisplay.daily > 0 && (
|
|
|
|
|
<div>
|
2026-05-30 13:24:22 +03:00
|
|
|
<span className="text-2xl font-bold text-amber-600">
|
|
|
|
|
{formatCurrency(property.priceDisplay.daily)}
|
|
|
|
|
</span>
|
2026-05-25 21:27:39 +03:00
|
|
|
<span className="text-gray-500 mr-1">ل.س / يومياً</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{property.deposit > 0 && (
|
2026-05-25 21:53:57 +03:00
|
|
|
<div className="text-sm text-gray-500">
|
2026-05-30 13:24:22 +03:00
|
|
|
<span className="font-medium">تأمين:</span>{" "}
|
|
|
|
|
{formatCurrency(property.deposit)} ل.س
|
2026-05-25 21:53:57 +03:00
|
|
|
</div>
|
2026-05-25 21:27:39 +03:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
2026-04-28 13:17:57 -07:00
|
|
|
<div>
|
2026-05-30 13:24:22 +03:00
|
|
|
<span className="text-2xl font-bold text-blue-600">
|
|
|
|
|
{formatCurrency(property.price)}
|
|
|
|
|
</span>
|
2026-05-25 21:27:39 +03:00
|
|
|
<span className="text-gray-500 mr-1">ل.س</span>
|
|
|
|
|
<span className="text-sm text-gray-400 mr-2">للبيع</span>
|
2026-03-30 13:44:52 +00:00
|
|
|
</div>
|
2026-05-25 21:27:39 +03:00
|
|
|
)}
|
2026-03-30 13:44:52 +00:00
|
|
|
</div>
|
2026-04-28 13:17:57 -07:00
|
|
|
|
2026-05-25 21:53:57 +03:00
|
|
|
{/* Specs Tiles */}
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="grid grid-cols-3 md:grid-cols-6 gap-1.5 mb-4">
|
2026-05-25 21:27:39 +03:00
|
|
|
{property.bedrooms > 0 && (
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="bg-gray-50 rounded-lg p-2 text-center">
|
|
|
|
|
<Bed className="w-4 h-4 text-amber-500 mx-auto mb-0.5" />
|
2026-05-30 13:24:22 +03:00
|
|
|
<div className="font-bold text-gray-900 text-sm">
|
|
|
|
|
{property.bedrooms}
|
|
|
|
|
</div>
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="text-[10px] text-gray-500">غرف نوم</div>
|
2026-05-25 21:27:39 +03:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{property.bathrooms > 0 && (
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="bg-gray-50 rounded-lg p-2 text-center">
|
|
|
|
|
<Bath className="w-4 h-4 text-amber-500 mx-auto mb-0.5" />
|
2026-05-30 13:24:22 +03:00
|
|
|
<div className="font-bold text-gray-900 text-sm">
|
|
|
|
|
{property.bathrooms}
|
|
|
|
|
</div>
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="text-[10px] text-gray-500">حمامات</div>
|
2026-03-30 13:44:52 +00:00
|
|
|
</div>
|
2026-05-25 21:27:39 +03:00
|
|
|
)}
|
|
|
|
|
{property.area > 0 && (
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="bg-gray-50 rounded-lg p-2 text-center">
|
|
|
|
|
<Square className="w-4 h-4 text-amber-500 mx-auto mb-0.5" />
|
2026-05-30 13:24:22 +03:00
|
|
|
<div className="font-bold text-gray-900 text-sm">
|
|
|
|
|
{property.area}
|
|
|
|
|
</div>
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="text-[10px] text-gray-500">م²</div>
|
2026-05-25 21:27:39 +03:00
|
|
|
</div>
|
|
|
|
|
)}
|
2026-05-25 21:53:57 +03:00
|
|
|
{property.floor > 0 && (
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="bg-gray-50 rounded-lg p-2 text-center">
|
|
|
|
|
<Layers className="w-4 h-4 text-amber-500 mx-auto mb-0.5" />
|
2026-05-30 13:24:22 +03:00
|
|
|
<div className="font-bold text-gray-900 text-sm">
|
|
|
|
|
{property.floor}
|
|
|
|
|
</div>
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="text-[10px] text-gray-500">طابق</div>
|
2026-05-25 21:53:57 +03:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{property.salons > 0 && (
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="bg-gray-50 rounded-lg p-2 text-center">
|
|
|
|
|
<Sofa className="w-4 h-4 text-amber-500 mx-auto mb-0.5" />
|
2026-05-30 13:24:22 +03:00
|
|
|
<div className="font-bold text-gray-900 text-sm">
|
|
|
|
|
{property.salons}
|
|
|
|
|
</div>
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="text-[10px] text-gray-500">صالونات</div>
|
2026-05-25 21:53:57 +03:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{property.balconies > 0 && (
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="bg-gray-50 rounded-lg p-2 text-center">
|
|
|
|
|
<DoorOpen className="w-4 h-4 text-amber-500 mx-auto mb-0.5" />
|
2026-05-30 13:24:22 +03:00
|
|
|
<div className="font-bold text-gray-900 text-sm">
|
|
|
|
|
{property.balconies}
|
|
|
|
|
</div>
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="text-[10px] text-gray-500">بلكونات</div>
|
2026-05-25 21:53:57 +03:00
|
|
|
</div>
|
|
|
|
|
)}
|
2026-05-30 13:24:22 +03:00
|
|
|
{avgRating !== null && avgRating > 0 && (
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="bg-gray-50 rounded-lg p-2 text-center">
|
|
|
|
|
<Star className="w-4 h-4 text-amber-500 mx-auto mb-0.5 fill-amber-500" />
|
2026-05-30 13:24:22 +03:00
|
|
|
<div className="font-bold text-gray-900 text-sm">
|
|
|
|
|
{avgRating.toFixed(1)}
|
|
|
|
|
</div>
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="text-[10px] text-gray-500">التقييم</div>
|
2026-05-25 21:27:39 +03:00
|
|
|
</div>
|
|
|
|
|
)}
|
2026-05-25 23:07:29 +03:00
|
|
|
{property.bookedCount > 0 && (
|
|
|
|
|
<div className="bg-gray-50 rounded-lg p-2 text-center">
|
|
|
|
|
<Calendar className="w-4 h-4 text-amber-500 mx-auto mb-0.5" />
|
2026-05-30 13:24:22 +03:00
|
|
|
<div className="font-bold text-gray-900 text-sm">
|
|
|
|
|
{property.bookedCount}
|
|
|
|
|
</div>
|
2026-05-25 23:07:29 +03:00
|
|
|
<div className="text-[10px] text-gray-500">حجوزات</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-03-30 13:44:52 +00:00
|
|
|
</div>
|
|
|
|
|
|
2026-05-25 21:27:39 +03:00
|
|
|
{/* Description */}
|
|
|
|
|
{property.description && (
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="mb-4">
|
2026-05-30 13:24:22 +03:00
|
|
|
<h3 className="font-bold text-gray-900 mb-1 text-sm">
|
|
|
|
|
الوصف
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="text-gray-600 text-sm leading-relaxed">
|
|
|
|
|
{property.description}
|
|
|
|
|
</p>
|
2026-05-25 21:27:39 +03:00
|
|
|
</div>
|
|
|
|
|
)}
|
2026-03-30 13:44:52 +00:00
|
|
|
|
2026-05-25 21:27:39 +03:00
|
|
|
{/* Features */}
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="flex flex-wrap gap-1.5 mb-4">
|
2026-05-30 13:24:22 +03:00
|
|
|
{property.isSmokeAllow && (
|
|
|
|
|
<span className="px-2 py-0.5 bg-gray-100 text-gray-700 rounded-full text-xs border flex items-center gap-1">
|
|
|
|
|
<Wind className="w-3 h-3" /> يسمح بالتدخين
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
{!property.isSmokeAllow && (
|
|
|
|
|
<span className="px-2 py-0.5 bg-gray-100 text-gray-700 rounded-full text-xs border flex items-center gap-1">
|
|
|
|
|
<Ban className="w-3 h-3" /> ممنوع التدخين
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
{property.isVisitorAllow && (
|
|
|
|
|
<span className="px-2 py-0.5 bg-gray-100 text-gray-700 rounded-full text-xs border flex items-center gap-1">
|
|
|
|
|
<Users className="w-3 h-3" /> يسمح بالزوار
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
{property.specializedFor && (
|
|
|
|
|
<span className="px-2 py-0.5 bg-amber-50 text-amber-700 rounded-full text-xs border border-amber-200 flex items-center gap-1">
|
|
|
|
|
<Users className="w-3 h-3" /> {property.specializedFor}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
2026-04-26 13:46:30 +03:00
|
|
|
</div>
|
2026-05-25 21:27:39 +03:00
|
|
|
|
2026-05-25 21:53:57 +03:00
|
|
|
{/* Services with detail text */}
|
2026-05-30 13:24:22 +03:00
|
|
|
{property.services &&
|
|
|
|
|
(Array.isArray(property.services)
|
|
|
|
|
? property.services.length > 0
|
|
|
|
|
: Object.keys(property.services).length > 0) && (
|
|
|
|
|
<div className="mb-4">
|
|
|
|
|
<h3 className="font-bold text-gray-900 mb-2 text-sm">
|
|
|
|
|
الخدمات
|
|
|
|
|
</h3>
|
|
|
|
|
<div className="flex flex-wrap gap-1.5">
|
|
|
|
|
{Array.isArray(property.services)
|
|
|
|
|
? property.services.map((svc, i) => (
|
|
|
|
|
<span
|
|
|
|
|
key={i}
|
|
|
|
|
className="px-3 py-1 bg-green-50 text-green-700 rounded-full text-sm border border-green-200 flex items-center gap-1"
|
|
|
|
|
>
|
|
|
|
|
{serviceLabels[svc] || svc}
|
|
|
|
|
</span>
|
|
|
|
|
))
|
|
|
|
|
: Object.entries(property.services).map(
|
|
|
|
|
([key, val]) => {
|
|
|
|
|
if (!val) return null;
|
|
|
|
|
const detail =
|
|
|
|
|
typeof val === "object" && val.detail
|
|
|
|
|
? val.detail
|
|
|
|
|
: typeof val === "string"
|
|
|
|
|
? val
|
|
|
|
|
: null;
|
|
|
|
|
return (
|
|
|
|
|
<span
|
|
|
|
|
key={key}
|
|
|
|
|
className="px-3 py-1 bg-green-50 text-green-700 rounded-full text-sm border border-green-200 flex items-center gap-1"
|
|
|
|
|
>
|
|
|
|
|
{serviceLabels[key] || key}
|
|
|
|
|
{detail && (
|
|
|
|
|
<span className="text-green-400">
|
|
|
|
|
· {detail}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-05-25 21:53:57 +03:00
|
|
|
</div>
|
2026-05-30 13:24:22 +03:00
|
|
|
)}
|
2026-05-25 21:53:57 +03:00
|
|
|
|
|
|
|
|
{/* Room Details (only for room type) */}
|
|
|
|
|
{isRoomType && Object.keys(property.roomDetails).length > 0 && (
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="mb-4 bg-blue-50 rounded-xl p-3">
|
|
|
|
|
<h3 className="font-bold text-gray-900 mb-2 text-sm flex items-center gap-2">
|
|
|
|
|
<Info className="w-4 h-4 text-blue-500" />
|
2026-05-25 21:53:57 +03:00
|
|
|
تفاصيل الغرفة
|
|
|
|
|
</h3>
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
2026-05-25 22:14:37 +03:00
|
|
|
{(() => {
|
|
|
|
|
const rd = property.roomDetails;
|
|
|
|
|
const items = [];
|
2026-05-30 13:24:22 +03:00
|
|
|
if (rd.areaType)
|
|
|
|
|
items.push({
|
|
|
|
|
label: "نوع المساحة",
|
|
|
|
|
value:
|
|
|
|
|
rd.areaType === "private room"
|
|
|
|
|
? "غرفة خاصة"
|
|
|
|
|
: rd.areaType === "shared room"
|
|
|
|
|
? "غرفة مشتركة"
|
|
|
|
|
: rd.areaType,
|
|
|
|
|
});
|
|
|
|
|
if (rd.peopleAllowed)
|
|
|
|
|
items.push({
|
|
|
|
|
label: "عدد الأشخاص",
|
|
|
|
|
value: rd.peopleAllowed,
|
|
|
|
|
});
|
|
|
|
|
if (rd.furnitureDetails || rd.furniture)
|
|
|
|
|
items.push({
|
|
|
|
|
label: "الأثاث",
|
|
|
|
|
value: rd.furnitureDetails || rd.furniture,
|
|
|
|
|
});
|
|
|
|
|
if (rd.entranceType)
|
|
|
|
|
items.push({
|
|
|
|
|
label: "نوع المدخل",
|
|
|
|
|
value:
|
|
|
|
|
rd.entranceType === "shared entrance"
|
|
|
|
|
? "مدخل مشترك"
|
|
|
|
|
: rd.entranceType === "independent entrance"
|
|
|
|
|
? "مدخل مستقل"
|
|
|
|
|
: rd.entranceType,
|
|
|
|
|
});
|
|
|
|
|
if (rd.bathroomType)
|
|
|
|
|
items.push({
|
|
|
|
|
label: "الحمام",
|
|
|
|
|
value:
|
|
|
|
|
rd.bathroomType === "room specific"
|
|
|
|
|
? "خاص بالغرفة"
|
|
|
|
|
: rd.bathroomType === "shared"
|
|
|
|
|
? "مشترك"
|
|
|
|
|
: rd.bathroomType,
|
|
|
|
|
});
|
|
|
|
|
if (rd.kitchenType)
|
|
|
|
|
items.push({
|
|
|
|
|
label: "المطبخ",
|
|
|
|
|
value:
|
|
|
|
|
rd.kitchenType === "shared"
|
|
|
|
|
? "مشترك"
|
|
|
|
|
: rd.kitchenType === "not available"
|
|
|
|
|
? "غير متوفر"
|
|
|
|
|
: rd.kitchenType,
|
|
|
|
|
});
|
|
|
|
|
if (rd.homeResidentsCount ?? rd.residents)
|
|
|
|
|
items.push({
|
|
|
|
|
label: "عدد السكان",
|
|
|
|
|
value: rd.homeResidentsCount ?? rd.residents,
|
|
|
|
|
});
|
|
|
|
|
if (rd.currentPopulationGender)
|
|
|
|
|
items.push({
|
|
|
|
|
label: "جنس السكان",
|
|
|
|
|
value:
|
|
|
|
|
rd.currentPopulationGender === "men"
|
|
|
|
|
? "رجال"
|
|
|
|
|
: rd.currentPopulationGender === "women"
|
|
|
|
|
? "نساء"
|
|
|
|
|
: rd.currentPopulationGender === "family"
|
|
|
|
|
? "عائلة"
|
|
|
|
|
: rd.currentPopulationGender,
|
|
|
|
|
});
|
|
|
|
|
if (rd.dedicatedTo)
|
|
|
|
|
items.push({
|
|
|
|
|
label: "مخصص لـ",
|
|
|
|
|
value:
|
|
|
|
|
rd.dedicatedTo === "men only"
|
|
|
|
|
? "رجال فقط"
|
|
|
|
|
: rd.dedicatedTo === "women only"
|
|
|
|
|
? "نساء فقط"
|
|
|
|
|
: rd.dedicatedTo === "families only"
|
|
|
|
|
? "عائلات فقط"
|
|
|
|
|
: rd.dedicatedTo === "everyone"
|
|
|
|
|
? "الجميع"
|
|
|
|
|
: rd.dedicatedTo,
|
|
|
|
|
});
|
|
|
|
|
if (rd.hasRestrictedOwnerAreas !== undefined)
|
|
|
|
|
items.push({
|
|
|
|
|
label: "مناطق ممنوعة",
|
|
|
|
|
value: rd.hasRestrictedOwnerAreas ? "نعم" : "لا",
|
|
|
|
|
});
|
|
|
|
|
if (rd.hasChildren !== undefined)
|
|
|
|
|
items.push({
|
|
|
|
|
label: "أطفال",
|
|
|
|
|
value: rd.hasChildren ? "مسموح" : "غير مسموح",
|
|
|
|
|
});
|
|
|
|
|
if (rd.hasPets !== undefined)
|
|
|
|
|
items.push({
|
|
|
|
|
label: "حيوانات أليفة",
|
|
|
|
|
value: rd.hasPets ? "مسموح" : "غير مسموح",
|
|
|
|
|
});
|
|
|
|
|
if (rd.languageDialect)
|
|
|
|
|
items.push({
|
|
|
|
|
label: "اللغة",
|
|
|
|
|
value: rd.languageDialect,
|
|
|
|
|
});
|
|
|
|
|
if (rd.visitorsAllowed !== undefined)
|
|
|
|
|
items.push({
|
|
|
|
|
label: "الزوار",
|
|
|
|
|
value: rd.visitorsAllowed ? "مسموح" : "ممنوع",
|
|
|
|
|
});
|
|
|
|
|
if (rd.quietTimesEnabled ?? rd.quietTimes)
|
|
|
|
|
items.push({
|
|
|
|
|
label: "أوقات الهدوء",
|
|
|
|
|
value:
|
|
|
|
|
rd.quietTimesDetails ||
|
|
|
|
|
rd.quietTimes ||
|
|
|
|
|
(rd.quietTimesEnabled ? "مفعلة" : ""),
|
|
|
|
|
});
|
2026-05-25 22:14:37 +03:00
|
|
|
return items.map((item, i) => (
|
2026-05-30 13:24:22 +03:00
|
|
|
<div
|
|
|
|
|
key={i}
|
|
|
|
|
className="bg-white rounded-lg p-2 text-center"
|
|
|
|
|
>
|
|
|
|
|
<div className="text-xs text-gray-500">
|
|
|
|
|
{item.label}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="font-medium text-sm">
|
|
|
|
|
{item.value}
|
|
|
|
|
</div>
|
2026-05-25 22:14:37 +03:00
|
|
|
</div>
|
|
|
|
|
));
|
|
|
|
|
})()}
|
2026-05-25 21:53:57 +03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Proximity */}
|
|
|
|
|
{Object.keys(property.proximity).length > 0 && (
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="mb-4">
|
2026-05-30 13:24:22 +03:00
|
|
|
<h3 className="font-bold text-gray-900 mb-2 text-sm">
|
|
|
|
|
القرب من الخدمات
|
|
|
|
|
</h3>
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-1.5">
|
2026-05-25 21:53:57 +03:00
|
|
|
{Object.entries(property.proximity).map(([key, val]) => {
|
|
|
|
|
if (!val) return null;
|
2026-05-30 13:24:22 +03:00
|
|
|
const dist = typeof val === "object" ? val.distance : val;
|
2026-05-25 21:53:57 +03:00
|
|
|
return (
|
2026-05-30 13:24:22 +03:00
|
|
|
<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 === "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 === "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 === "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" />
|
|
|
|
|
)}
|
2026-05-25 21:53:57 +03:00
|
|
|
<div>
|
2026-05-30 13:24:22 +03:00
|
|
|
<div className="text-xs font-medium text-gray-900">
|
|
|
|
|
{proximityLabels[key] || key}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-[10px] text-gray-500">
|
|
|
|
|
{dist} {typeof dist === "number" ? "كم" : ""}
|
|
|
|
|
</div>
|
2026-05-25 21:53:57 +03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2026-05-25 21:27:39 +03:00
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-05-25 21:53:57 +03:00
|
|
|
{/* Terms as checklist */}
|
2026-05-25 21:27:39 +03:00
|
|
|
{Object.keys(property.terms).length > 0 && (
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="mb-4">
|
2026-05-30 13:24:22 +03:00
|
|
|
<h3 className="font-bold text-gray-900 mb-2 text-sm">
|
|
|
|
|
الشروط
|
|
|
|
|
</h3>
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-1.5">
|
2026-05-25 21:27:39 +03:00
|
|
|
{Object.entries(property.terms).map(([key, val]) => {
|
|
|
|
|
if (!val) return null;
|
2026-05-25 21:53:57 +03:00
|
|
|
return (
|
2026-05-30 13:24:22 +03:00
|
|
|
<div
|
|
|
|
|
key={key}
|
|
|
|
|
className="flex items-center gap-1.5 p-1.5 bg-gray-50 rounded-lg"
|
|
|
|
|
>
|
|
|
|
|
{key.startsWith("No") || key.startsWith("Only") ? (
|
2026-05-25 22:42:53 +03:00
|
|
|
<Ban className="w-3.5 h-3.5 text-red-500 flex-shrink-0" />
|
2026-05-25 21:53:57 +03:00
|
|
|
) : (
|
2026-05-25 22:42:53 +03:00
|
|
|
<Check className="w-3.5 h-3.5 text-green-500 flex-shrink-0" />
|
2026-05-25 21:53:57 +03:00
|
|
|
)}
|
2026-05-30 13:24:22 +03:00
|
|
|
<span className="text-xs text-gray-700">
|
|
|
|
|
{termLabels[key] || key}
|
|
|
|
|
</span>
|
2026-05-25 21:53:57 +03:00
|
|
|
</div>
|
|
|
|
|
);
|
2026-05-25 21:27:39 +03:00
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
|
|
|
|
{/* Map */}
|
|
|
|
|
{property.location.lat && property.location.lng && (
|
2026-05-30 13:24:22 +03:00
|
|
|
<motion.div
|
|
|
|
|
initial={{ opacity: 0, y: 20 }}
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
className="bg-white rounded-2xl overflow-hidden shadow-sm border border-gray-200"
|
|
|
|
|
>
|
2026-05-25 21:53:57 +03:00
|
|
|
<div className="h-64">
|
2026-05-30 13:24:22 +03:00
|
|
|
<MapContainer
|
|
|
|
|
center={[property.location.lat, property.location.lng]}
|
|
|
|
|
zoom={14}
|
|
|
|
|
className="h-full w-full"
|
|
|
|
|
scrollWheelZoom={false}
|
|
|
|
|
>
|
2026-05-25 21:53:57 +03:00
|
|
|
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
|
2026-05-30 13:24:22 +03:00
|
|
|
<Marker
|
|
|
|
|
position={[property.location.lat, property.location.lng]}
|
|
|
|
|
>
|
2026-05-25 21:53:57 +03:00
|
|
|
<Popup>{property.title}</Popup>
|
|
|
|
|
</Marker>
|
|
|
|
|
</MapContainer>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="p-3 bg-amber-50 text-center text-sm text-amber-700 flex items-center justify-center gap-2">
|
|
|
|
|
<Info className="w-4 h-4" />
|
|
|
|
|
<span>موقع تقريبي — يظهر الموقع الدقيق بعد تأكيد الحجز</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="p-3 border-t border-gray-100">
|
2026-05-30 13:24:22 +03:00
|
|
|
<a
|
|
|
|
|
href={`https://www.google.com/maps?q=${property.location.lat},${property.location.lng}`}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
className="flex items-center justify-center gap-2 text-blue-600 hover:text-blue-700 font-medium text-sm"
|
|
|
|
|
>
|
2026-05-25 21:53:57 +03:00
|
|
|
<ExternalLink className="w-4 h-4" />
|
|
|
|
|
فتح في Google Maps
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
2026-05-25 21:27:39 +03:00
|
|
|
</motion.div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Ratings Section */}
|
2026-05-30 13:24:22 +03:00
|
|
|
<motion.div
|
|
|
|
|
initial={{ opacity: 0, y: 20 }}
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
>
|
|
|
|
|
<PropertyRatingList
|
|
|
|
|
propertyId={property._raw?.propertyInformationId || property.id}
|
|
|
|
|
/>
|
2026-05-25 21:27:39 +03:00
|
|
|
</motion.div>
|
2026-03-30 13:44:52 +00:00
|
|
|
</div>
|
|
|
|
|
|
2026-05-25 21:27:39 +03:00
|
|
|
{/* Sidebar */}
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="space-y-4">
|
2026-05-25 21:27:39 +03:00
|
|
|
{/* Booking Card */}
|
2026-05-30 13:45:32 +03:00
|
|
|
{property.isRent && !AuthService.isAdmin() && (
|
2026-05-30 13:24:22 +03:00
|
|
|
<motion.div
|
|
|
|
|
initial={{ opacity: 0, x: 20 }}
|
|
|
|
|
animate={{ opacity: 1, x: 0 }}
|
|
|
|
|
className="bg-white rounded-2xl p-5 shadow-sm border border-gray-200 sticky top-6"
|
|
|
|
|
>
|
2026-05-26 17:31:01 +03:00
|
|
|
{isOwnProperty ? (
|
|
|
|
|
<div className="text-center py-3">
|
|
|
|
|
<div className="w-14 h-14 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-2">
|
|
|
|
|
<Home className="w-7 h-7 text-gray-400" />
|
|
|
|
|
</div>
|
2026-05-30 13:24:22 +03:00
|
|
|
<h4 className="font-bold text-gray-700 text-sm mb-1">
|
|
|
|
|
هذا عقارك
|
|
|
|
|
</h4>
|
|
|
|
|
<p className="text-xs text-gray-500">
|
|
|
|
|
لا يمكنك حجز عقارك الخاص
|
|
|
|
|
</p>
|
2026-05-26 17:31:01 +03:00
|
|
|
</div>
|
|
|
|
|
) : bookingSuccess ? (
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="text-center py-3">
|
|
|
|
|
<div className="w-14 h-14 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-2">
|
|
|
|
|
<Check className="w-7 h-7 text-green-600" />
|
2026-05-25 21:27:39 +03:00
|
|
|
</div>
|
2026-05-30 13:24:22 +03:00
|
|
|
<h4 className="font-bold text-green-700 text-sm mb-1">
|
|
|
|
|
تم إرسال طلب الحجز
|
|
|
|
|
</h4>
|
|
|
|
|
<p className="text-xs text-gray-500">
|
|
|
|
|
سيتم مراجعة طلبك من قبل المالك
|
|
|
|
|
</p>
|
2026-03-30 13:44:52 +00:00
|
|
|
</div>
|
2026-05-25 21:27:39 +03:00
|
|
|
) : (
|
|
|
|
|
<>
|
2026-05-26 16:38:16 +03:00
|
|
|
{/* Pricing Mode Toggle */}
|
|
|
|
|
{showPricingToggle && (
|
|
|
|
|
<div className="grid grid-cols-2 gap-2 mb-3">
|
2026-05-30 13:24:22 +03:00
|
|
|
<button
|
|
|
|
|
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"}`}
|
|
|
|
|
>
|
|
|
|
|
<div className="text-xs font-bold text-gray-900">
|
|
|
|
|
إيجار يومي
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-sm font-bold text-amber-600">
|
|
|
|
|
{formatCurrency(property.priceDisplay.daily)} ل.س
|
|
|
|
|
</div>
|
2026-05-26 16:38:16 +03:00
|
|
|
</button>
|
2026-05-30 13:24:22 +03:00
|
|
|
<button
|
|
|
|
|
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"}`}
|
|
|
|
|
>
|
|
|
|
|
<div className="text-xs font-bold text-gray-900">
|
|
|
|
|
إيجار شهري
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-sm font-bold text-amber-600">
|
|
|
|
|
{formatCurrency(property.priceDisplay.monthly)} ل.س
|
|
|
|
|
</div>
|
2026-05-26 16:38:16 +03:00
|
|
|
</button>
|
2026-05-25 21:27:39 +03:00
|
|
|
</div>
|
2026-05-26 16:38:16 +03:00
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Step Indicator */}
|
|
|
|
|
<div className="flex items-center justify-center gap-2 mb-3">
|
2026-05-30 13:24:22 +03:00
|
|
|
<div
|
|
|
|
|
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${bookingStep === "entry" ? "bg-amber-100 text-amber-800" : "bg-gray-100 text-gray-500"}`}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
className={`w-1.5 h-1.5 rounded-full ${bookingStep === "entry" ? "bg-amber-500" : "bg-gray-400"}`}
|
|
|
|
|
/>
|
2026-05-26 16:38:16 +03:00
|
|
|
تحديد تاريخ البداية
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-gray-300 text-xs">←</div>
|
2026-05-30 13:24:22 +03:00
|
|
|
<div
|
|
|
|
|
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${bookingStep === "exit" ? "bg-amber-100 text-amber-800" : "bg-gray-100 text-gray-500"}`}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
className={`w-1.5 h-1.5 rounded-full ${bookingStep === "exit" ? "bg-amber-500" : "bg-gray-400"}`}
|
|
|
|
|
/>
|
2026-05-26 16:38:16 +03:00
|
|
|
تحديد تاريخ النهاية
|
2026-04-28 13:17:57 -07:00
|
|
|
</div>
|
2026-03-30 13:44:52 +00:00
|
|
|
</div>
|
2026-04-28 13:17:57 -07:00
|
|
|
|
2026-05-26 16:38:16 +03:00
|
|
|
{/* Calendar */}
|
2026-05-30 13:24:22 +03:00
|
|
|
{effectivePricingMode === "daily" ? (
|
2026-05-26 16:38:16 +03:00
|
|
|
<div className="mb-3">
|
|
|
|
|
<div className="flex items-center justify-between mb-2">
|
2026-05-30 13:24:22 +03:00
|
|
|
<button
|
|
|
|
|
onClick={() => navigateMonth(-1)}
|
|
|
|
|
className="p-1 rounded-lg hover:bg-gray-100 transition-colors"
|
|
|
|
|
>
|
2026-05-26 16:38:16 +03:00
|
|
|
<ChevronRight className="w-4 h-4 text-gray-600" />
|
|
|
|
|
</button>
|
2026-05-30 13:24:22 +03:00
|
|
|
<span className="text-sm font-bold text-gray-900">
|
|
|
|
|
{MONTHS_AR[calendarMonth]} {calendarYear}
|
|
|
|
|
</span>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => navigateMonth(1)}
|
|
|
|
|
className="p-1 rounded-lg hover:bg-gray-100 transition-colors"
|
|
|
|
|
>
|
2026-05-26 16:38:16 +03:00
|
|
|
<ChevronLeft className="w-4 h-4 text-gray-600" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-7 mb-1">
|
|
|
|
|
{DAYS_AR.map((d, i) => (
|
2026-05-30 13:24:22 +03:00
|
|
|
<div
|
|
|
|
|
key={i}
|
|
|
|
|
className="text-center text-[10px] text-gray-400 font-medium py-1"
|
|
|
|
|
>
|
|
|
|
|
{d}
|
|
|
|
|
</div>
|
2026-05-26 16:38:16 +03:00
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{(() => {
|
2026-05-30 13:24:22 +03:00
|
|
|
const firstDay = new Date(
|
|
|
|
|
calendarYear,
|
|
|
|
|
calendarMonth,
|
|
|
|
|
1,
|
|
|
|
|
).getDay();
|
|
|
|
|
const daysInMonth = new Date(
|
|
|
|
|
calendarYear,
|
|
|
|
|
calendarMonth + 1,
|
|
|
|
|
0,
|
|
|
|
|
).getDate();
|
2026-05-26 16:38:16 +03:00
|
|
|
const adjustedFirstDay = (firstDay + 1) % 7;
|
|
|
|
|
const cells = [];
|
|
|
|
|
for (let i = 0; i < adjustedFirstDay; i++) {
|
|
|
|
|
cells.push(<div key={`e-${i}`} />);
|
|
|
|
|
}
|
|
|
|
|
for (let day = 1; day <= daysInMonth; day++) {
|
2026-05-30 13:24:22 +03:00
|
|
|
const dateStr = `${calendarYear}-${String(calendarMonth + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
2026-05-26 16:38:16 +03:00
|
|
|
const past = isPastDate(dateStr);
|
|
|
|
|
const available = isDateAvailable(dateStr);
|
|
|
|
|
const isSelStart = dateStr === selectedStart;
|
|
|
|
|
const isSelEnd = dateStr === selectedEnd;
|
2026-05-30 13:24:22 +03:00
|
|
|
const inRange =
|
|
|
|
|
selectedStart &&
|
|
|
|
|
selectedEnd &&
|
|
|
|
|
new Date(dateStr) > new Date(selectedStart) &&
|
|
|
|
|
new Date(dateStr) < new Date(selectedEnd);
|
2026-05-26 16:38:16 +03:00
|
|
|
const disabled = past || !available;
|
|
|
|
|
cells.push(
|
2026-05-30 13:24:22 +03:00
|
|
|
<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" : ""}`}
|
|
|
|
|
>
|
2026-05-26 16:38:16 +03:00
|
|
|
{day}
|
2026-05-30 13:24:22 +03:00
|
|
|
</button>,
|
2026-05-26 16:38:16 +03:00
|
|
|
);
|
|
|
|
|
}
|
2026-05-30 13:24:22 +03:00
|
|
|
return (
|
|
|
|
|
<div className="grid grid-cols-7 gap-0.5">
|
|
|
|
|
{cells}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2026-05-26 16:38:16 +03:00
|
|
|
})()}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="mb-3">
|
|
|
|
|
<div className="flex items-center justify-between mb-2">
|
2026-05-30 13:24:22 +03:00
|
|
|
<button
|
|
|
|
|
onClick={() => setCalendarYear((prev) => prev - 1)}
|
|
|
|
|
className="p-1 rounded-lg hover:bg-gray-100 transition-colors"
|
|
|
|
|
>
|
2026-05-26 16:38:16 +03:00
|
|
|
<ChevronRight className="w-4 h-4 text-gray-600" />
|
|
|
|
|
</button>
|
2026-05-30 13:24:22 +03:00
|
|
|
<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"
|
|
|
|
|
>
|
2026-05-26 16:38:16 +03:00
|
|
|
<ChevronLeft className="w-4 h-4 text-gray-600" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="grid grid-cols-3 gap-2">
|
|
|
|
|
{MONTHS_AR.map((name, idx) => {
|
2026-05-30 13:24:22 +03:00
|
|
|
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);
|
2026-05-26 16:38:16 +03:00
|
|
|
return (
|
2026-05-30 13:24:22 +03:00
|
|
|
<button
|
|
|
|
|
key={idx}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
const firstDay = `${monthStr}-01`;
|
|
|
|
|
if (
|
|
|
|
|
bookingStep === "entry" ||
|
|
|
|
|
(selectedStart &&
|
|
|
|
|
monthStr <= selectedStart.substring(0, 7))
|
|
|
|
|
) {
|
|
|
|
|
setSelectedStart(firstDay);
|
|
|
|
|
setSelectedEnd(null);
|
|
|
|
|
setBookingStep("exit");
|
|
|
|
|
} else {
|
|
|
|
|
const lastDay = new Date(
|
|
|
|
|
calendarYear,
|
|
|
|
|
idx + 1,
|
|
|
|
|
0,
|
|
|
|
|
).getDate();
|
|
|
|
|
setSelectedEnd(
|
|
|
|
|
`${monthStr}-${String(lastDay).padStart(2, "0")}`,
|
|
|
|
|
);
|
|
|
|
|
setBookingStep("entry");
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
className={`p-2.5 rounded-xl text-center text-xs font-medium transition-all border ${isSelStart || isSelEnd ? "bg-amber-500 text-white border-amber-500 shadow-sm" : inRange ? "bg-amber-100 text-amber-800 border-amber-200" : "bg-white text-gray-700 border-gray-200 hover:border-amber-300"}`}
|
|
|
|
|
>
|
2026-05-26 16:38:16 +03:00
|
|
|
{name}
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Summary */}
|
|
|
|
|
{selectedStart && (
|
|
|
|
|
<div className="bg-gray-50 rounded-xl p-3 mb-3 space-y-1.5">
|
|
|
|
|
<div className="flex justify-between text-xs">
|
|
|
|
|
<span className="text-gray-500">تاريخ البداية</span>
|
2026-05-30 13:24:22 +03:00
|
|
|
<span className="font-medium text-gray-900">
|
|
|
|
|
{selectedStart}
|
|
|
|
|
</span>
|
2026-05-26 16:38:16 +03:00
|
|
|
</div>
|
|
|
|
|
{selectedEnd && (
|
|
|
|
|
<>
|
|
|
|
|
<div className="flex justify-between text-xs">
|
2026-05-30 13:24:22 +03:00
|
|
|
<span className="text-gray-500">
|
|
|
|
|
تاريخ النهاية
|
|
|
|
|
</span>
|
|
|
|
|
<span className="font-medium text-gray-900">
|
|
|
|
|
{selectedEnd}
|
|
|
|
|
</span>
|
2026-05-26 16:38:16 +03:00
|
|
|
</div>
|
|
|
|
|
<div className="border-t border-gray-200 my-1" />
|
|
|
|
|
<div className="flex justify-between text-xs">
|
2026-05-30 13:24:22 +03:00
|
|
|
<span className="text-gray-500">
|
|
|
|
|
{effectivePricingMode === "daily"
|
|
|
|
|
? "عدد الأيام"
|
|
|
|
|
: "عدد الأشهر"}
|
|
|
|
|
</span>
|
2026-05-26 16:38:16 +03:00
|
|
|
<span className="font-medium text-gray-900">
|
2026-05-30 13:24:22 +03:00
|
|
|
{effectivePricingMode === "daily"
|
|
|
|
|
? Math.max(
|
|
|
|
|
1,
|
|
|
|
|
Math.round(
|
|
|
|
|
(new Date(selectedEnd) -
|
|
|
|
|
new Date(selectedStart)) /
|
|
|
|
|
(1000 * 60 * 60 * 24),
|
|
|
|
|
) + 1,
|
|
|
|
|
)
|
|
|
|
|
: new Date(selectedEnd).getMonth() -
|
|
|
|
|
new Date(selectedStart).getMonth() +
|
|
|
|
|
(new Date(selectedEnd).getFullYear() -
|
|
|
|
|
new Date(selectedStart).getFullYear()) *
|
|
|
|
|
12 +
|
|
|
|
|
1}
|
2026-05-26 16:38:16 +03:00
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex justify-between text-xs font-bold">
|
|
|
|
|
<span className="text-gray-700">المجموع</span>
|
|
|
|
|
<span className="text-amber-600">
|
2026-05-30 13:24:22 +03:00
|
|
|
{formatCurrency(
|
|
|
|
|
effectivePricingMode === "daily"
|
|
|
|
|
? Math.max(
|
|
|
|
|
1,
|
|
|
|
|
Math.round(
|
|
|
|
|
(new Date(selectedEnd) -
|
|
|
|
|
new Date(selectedStart)) /
|
|
|
|
|
(1000 * 60 * 60 * 24),
|
|
|
|
|
) + 1,
|
|
|
|
|
) * property.priceDisplay.daily
|
|
|
|
|
: (new Date(selectedEnd).getMonth() -
|
|
|
|
|
new Date(selectedStart).getMonth() +
|
|
|
|
|
(new Date(selectedEnd).getFullYear() -
|
|
|
|
|
new Date(
|
|
|
|
|
selectedStart,
|
|
|
|
|
).getFullYear()) *
|
|
|
|
|
12 +
|
|
|
|
|
1) *
|
|
|
|
|
property.priceDisplay.monthly,
|
|
|
|
|
)}{" "}
|
|
|
|
|
ل.س
|
2026-05-26 16:38:16 +03:00
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
{property.deposit > 0 && (
|
|
|
|
|
<div className="flex justify-between text-xs">
|
|
|
|
|
<span className="text-gray-500">تأمين</span>
|
2026-05-30 13:24:22 +03:00
|
|
|
<span className="font-medium text-gray-900">
|
|
|
|
|
{formatCurrency(property.deposit)} ل.س
|
|
|
|
|
</span>
|
2026-05-26 16:38:16 +03:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-05-25 21:27:39 +03:00
|
|
|
{bookingError && (
|
2026-05-30 13:24:22 +03:00
|
|
|
<div className="bg-red-50 text-red-600 p-2.5 rounded-xl text-xs mb-3">
|
|
|
|
|
{bookingError}
|
|
|
|
|
</div>
|
2026-05-25 21:27:39 +03:00
|
|
|
)}
|
2026-03-30 13:44:52 +00:00
|
|
|
|
2026-05-30 13:24:22 +03:00
|
|
|
<button
|
|
|
|
|
onClick={handleBookingConfirm}
|
|
|
|
|
disabled={
|
|
|
|
|
bookingLoading || !selectedStart || !selectedEnd
|
|
|
|
|
}
|
|
|
|
|
className="w-full bg-amber-500 hover:bg-amber-600 text-white py-2.5 rounded-xl font-bold text-sm transition-all disabled:opacity-50 flex items-center justify-center gap-2"
|
|
|
|
|
>
|
|
|
|
|
{bookingLoading ? (
|
|
|
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
|
|
|
) : (
|
|
|
|
|
<Calendar className="w-4 h-4" />
|
|
|
|
|
)}
|
|
|
|
|
{bookingLoading ? "جاري الحجز..." : "تأكيد الحجز"}
|
2026-05-25 21:27:39 +03:00
|
|
|
</button>
|
|
|
|
|
</>
|
2026-03-30 13:44:52 +00:00
|
|
|
)}
|
|
|
|
|
</motion.div>
|
2026-05-25 21:27:39 +03:00
|
|
|
)}
|
2026-03-30 13:44:52 +00:00
|
|
|
|
2026-05-25 21:27:39 +03:00
|
|
|
{/* Contact Card */}
|
2026-05-30 13:24:22 +03:00
|
|
|
{/* {!isOwnProperty && (
|
2026-05-25 22:42:53 +03:00
|
|
|
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} className="bg-white rounded-2xl p-5 shadow-sm border border-gray-200">
|
|
|
|
|
<div className="flex items-center gap-2 mb-3">
|
|
|
|
|
<Phone className="w-4 h-4 text-amber-500" />
|
|
|
|
|
<h3 className="font-bold text-gray-900">معلومات المالك</h3>
|
2026-05-25 21:53:57 +03:00
|
|
|
</div>
|
2026-04-28 13:17:57 -07:00
|
|
|
|
2026-05-25 21:27:39 +03:00
|
|
|
{showContact && contactInfo ? (
|
2026-05-25 22:42:53 +03:00
|
|
|
<div className="space-y-2">
|
|
|
|
|
<div className="flex items-center gap-2.5 p-2.5 bg-gray-50 rounded-xl">
|
|
|
|
|
<Phone className="w-4 h-4 text-gray-600 flex-shrink-0" />
|
|
|
|
|
<span className="font-medium text-gray-900 text-sm" dir="ltr">{contactInfo.phone || contactInfo.phoneNumber || '—'}</span>
|
2026-03-30 13:44:52 +00:00
|
|
|
</div>
|
2026-05-25 21:27:39 +03:00
|
|
|
{contactInfo.whatsAppNumber && (
|
2026-05-25 21:53:57 +03:00
|
|
|
<a href={`https://wa.me/${contactInfo.whatsAppNumber.replace(/[^0-9]/g, '')}`} target="_blank" rel="noopener noreferrer"
|
2026-05-25 22:42:53 +03:00
|
|
|
className="flex items-center gap-2.5 p-2.5 bg-green-50 rounded-xl hover:bg-green-100 transition-colors">
|
|
|
|
|
<MessageCircle className="w-4 h-4 text-green-600 flex-shrink-0" />
|
|
|
|
|
<span className="font-medium text-gray-900 text-sm" dir="ltr">{contactInfo.whatsAppNumber}</span>
|
2026-05-25 21:53:57 +03:00
|
|
|
</a>
|
2026-05-25 21:27:39 +03:00
|
|
|
)}
|
2026-03-30 13:44:52 +00:00
|
|
|
</div>
|
2026-05-25 21:27:39 +03:00
|
|
|
) : (
|
|
|
|
|
<button onClick={fetchContactInfo}
|
2026-05-25 22:42:53 +03:00
|
|
|
className="w-full bg-gray-800 hover:bg-gray-900 text-white py-2.5 rounded-xl font-medium text-sm transition-colors flex items-center justify-center gap-2">
|
|
|
|
|
<Phone className="w-4 h-4" />
|
2026-05-25 21:27:39 +03:00
|
|
|
عرض معلومات الاتصال
|
2026-03-30 13:44:52 +00:00
|
|
|
</button>
|
2026-05-25 21:27:39 +03:00
|
|
|
)}
|
|
|
|
|
</motion.div>
|
2026-05-30 13:24:22 +03:00
|
|
|
)} */}
|
2026-03-30 13:44:52 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-05-25 21:27:39 +03:00
|
|
|
{/* Login Dialog */}
|
2026-03-30 13:44:52 +00:00
|
|
|
{showLoginDialog && (
|
2026-05-30 13:24:22 +03:00
|
|
|
<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()}
|
|
|
|
|
>
|
2026-03-30 13:44:52 +00:00
|
|
|
<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" />
|
|
|
|
|
</div>
|
2026-05-25 21:27:39 +03:00
|
|
|
<h3 className="text-xl font-bold mb-2">تسجيل الدخول مطلوب</h3>
|
2026-05-30 13:24:22 +03:00
|
|
|
<p className="text-gray-500 mb-4">
|
|
|
|
|
للحجز أو إضافة المفضلة، سجل دخولك.
|
|
|
|
|
</p>
|
|
|
|
|
<Link
|
|
|
|
|
href="/login"
|
|
|
|
|
className="block w-full bg-amber-500 text-white py-3 rounded-xl font-medium mb-2 hover:bg-amber-600"
|
|
|
|
|
>
|
|
|
|
|
تسجيل الدخول
|
|
|
|
|
</Link>
|
|
|
|
|
<Link
|
|
|
|
|
href="/auth/choose-role"
|
|
|
|
|
className="block w-full bg-gray-100 py-3 rounded-xl font-medium hover:bg-gray-200"
|
|
|
|
|
>
|
|
|
|
|
إنشاء حساب
|
|
|
|
|
</Link>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setShowLoginDialog(false)}
|
|
|
|
|
className="mt-3 text-gray-400 hover:text-gray-600"
|
|
|
|
|
>
|
|
|
|
|
إلغاء
|
|
|
|
|
</button>
|
2026-04-26 13:46:30 +03:00
|
|
|
</div>
|
2026-03-30 13:44:52 +00:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2026-05-25 21:27:39 +03:00
|
|
|
}
|
2026-06-04 23:09:48 +03:00
|
|
|
//reset
|