"use client"; import { useState, useEffect, useMemo, useRef } from "react"; import { useTranslation } from "react-i18next"; import { motion, AnimatePresence } from "framer-motion"; import toast, { Toaster } from "react-hot-toast"; import Link from "next/link"; import { useParams, useRouter, useSearchParams } from "next/navigation"; import { 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, Theater, } 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 DateSelectionCalendar from "@/app/components/property/DateSelectionCalendar"; import { getPropertyAverageRating } from "../../utils/ratings"; import "leaflet/dist/leaflet.css"; import Loading from "@/app/loading"; function PropertyDetailMap({ lat, lng, title }) { const mapRef = useRef(null); const mapInstanceRef = useRef(null); const markerRef = useRef(null); useEffect(() => { if (!mapRef.current || mapInstanceRef.current) return; if (mapRef.current._leaflet_id && !mapInstanceRef.current) { delete mapRef.current._leaflet_id; } const L = require("leaflet"); delete L.Icon.Default.prototype._getIconUrl; L.Icon.Default.mergeOptions({ iconRetinaUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png", iconUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png", shadowUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png", }); const map = L.map(mapRef.current, { center: [lat, lng], zoom: 14, scrollWheelZoom: false, }); L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: '© OpenStreetMap contributors', maxZoom: 19, }).addTo(map); const marker = L.marker([lat, lng]).addTo(map).bindPopup(title); mapInstanceRef.current = map; markerRef.current = marker; map.invalidateSize(); return () => { markerRef.current?.remove(); markerRef.current = null; if (mapInstanceRef.current) { mapInstanceRef.current.remove(); mapInstanceRef.current = null; } }; }, [lat, lng, title]); useEffect(() => { if (mapInstanceRef.current) { mapInstanceRef.current.setView([lat, lng], 14); markerRef.current?.setLatLng([lat, lng]); markerRef.current?.setPopupContent(title); mapInstanceRef.current.invalidateSize(); } }, [lat, lng, title]); return
; } function formatCurrency(amount) { if (!amount || isNaN(amount)) return "0"; return Number(amount).toLocaleString("ar-SA"); } function buildImageUrl(img) { 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}`; } const svcKey = (k) => "propertyService." + k.charAt(0).toLowerCase() + k.slice(1); const termKey = (k) => "propertyTerm." + k.charAt(0).toLowerCase() + k.slice(1); const proxKey = (k) => "proximity." + k.toLowerCase(); function mapApiDetail(item) { if (!item) return null; const info = item.propertyInformation || {}; const details = typeof info.detailsJSON === "object" && info.detailsJSON ? info.detailsJSON : (() => { try { return JSON.parse(info.detailsJSON || "{}"); } catch { return {}; } })(); const isRent = item.dailyRent != null || item.monthlyRent != null; const dailyPrice = item.dailyRent || 0; const monthlyPrice = item.monthlyRent || 0; const salePrice = item.price || 0; const commission = item.commission || 0; const price = isRent ? monthlyPrice || dailyPrice : salePrice; const priceUnit = isRent ? (monthlyPrice ? "monthly" : "daily") : "sale"; const propType = BuildingTypeKeys[info.buildingType] ?? BuildingTypeKeys[item.type] ?? "apartment"; const status = PropertyStatusKeys[info.status] ?? PropertyStatusKeys[item.status] ?? "available"; const rawImages = Array.isArray(info.images) ? info.images : []; const photosFallback = Array.isArray(details.photos) ? details.photos : []; const allRaw = rawImages.length > 0 ? rawImages : photosFallback; const images = allRaw.length > 0 ? allRaw.map(buildImageUrl).filter(Boolean) : []; // Normalize services: Flutter sends list of strings or object with boolean values const rawServices = details.services || {}; const serviceList = Array.isArray(rawServices) ? rawServices : Object.keys(rawServices).filter((k) => rawServices[k]); const serviceDetails = details.serviceDetails || {}; const services = {}; serviceList.forEach((s) => { services[s] = serviceDetails[s] || true; }); const rawTerms = details.terms || {}; const terms = Array.isArray(rawTerms) ? rawTerms.reduce((acc, t) => ({ ...acc, [t]: true }), {}) : rawTerms; // Try multiple key aliases like Flutter does const floor = details.floorNumber ?? details.floor ?? 0; const salons = details.numberOfSalons ?? details.salonsCount ?? details.salons ?? 0; const balconies = details.numberOfBalconies ?? details.balconiesCount ?? details.balconies ?? 0; 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; }); const roomDetails = details.room || details.roomDetails || {}; 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; return { id: item.id, propertyInformationId: info.id, title: details.description || info.address || "", description: details.description || info.description || "", type: propType, typeLabel: propType, price, priceUnit, priceDisplay: { daily: dailyPrice, monthly: monthlyPrice, sale: salePrice }, isRent, displayType, propertyCondition, furnished, location: { city: extractCity(info.address) || "damascus", address: info.address || "", lat: parseFloat(info.cordsX) || 0, lng: parseFloat(info.cordsY) || 0, }, bedrooms: info.numberOfBedRooms || 0, bathrooms: info.numberOfBathRooms || 0, area: info.space || 0, floor, salons, balconies, images, status, statusLabel: status, services, terms, proximity, roomDetails, details, bookedCount: details.bookedCount || 0, deposit: item.deposit || 0, currencyId: item.currencyId, isSmokeAllow: item.isSmokeAllow, isVisitorAllow: item.isVisitorAllow, specializedFor: item.specializedFor, ownerId: info.ownerId ?? info.userId ?? item.ownerId ?? item.userId ?? null, _raw: item, }; } export default function PropertyDetailsPage({ type }) { const { t, i18n } = useTranslation(); const params = useParams(); const router = useRouter(); const { isFavorite, addFavorite, removeFavorite } = useFavorites(); const [property, setProperty] = useState(null); const [loading, setLoading] = useState(true); const [currentImage, setCurrentImage] = useState(0); const [showLoginDialog, setShowLoginDialog] = useState(false); const [bookingDates, setBookingDates] = useState({ start: "", end: "" }); const [bookingLoading, setBookingLoading] = useState(false); const [bookingError, setBookingError] = useState(null); const [bookingSuccess, setBookingSuccess] = useState(false); const [availableRanges, setAvailableRanges] = useState([]); const [selectedStart, setSelectedStart] = useState(null); const [selectedEnd, setSelectedEnd] = useState(null); const [pricingMode, setPricingMode] = useState("daily"); const [isOwnProperty, setIsOwnProperty] = useState(false); 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); useEffect(() => { const id = params.id; if (!id) return; setLoading(true); async function fetchProperty() { try { let data = null; try { data = type === "rent" ? await getRentProperty(id) : (await getSalePropertyById(id)) || (await getSaleProperty(id)); } catch {} if (!data) { try { data = (await getSalePropertyById(id)) || (await getSaleProperty(id)); } catch {} } if (!data) { try { data = await getSaleProperty(id); } catch {} } if (data) { const mapped = mapApiDetail(data); setProperty(mapped); if (mapped) fetchAvgRating(mapped.id); if (mapped && mapped.isRent) { try { const propInfoId = mapped._raw?.propertyInformationId || mapped.id; const ranges = await getAvailableDateRanges(propInfoId); if (ranges && Array.isArray(ranges)) { setAvailableRanges(ranges); } } catch (e) { console.warn("Failed to fetch date ranges", e); } } // Check if current user owns this property via their own listings if (AuthService.isAuthenticated() && AuthService.isOwner()) { try { const [myRent, mySale] = await Promise.allSettled([getMyRentListings(), getMySaleListings()]); const myPropIds = new Set(); const collectIds = (result) => { if (result.status !== "fulfilled" || !result.value) return; const list = Array.isArray(result.value) ? result.value : [result.value]; list.filter(Boolean).forEach((p) => { const info = p.propertyInformation || {}; if (info.id) myPropIds.add(Number(info.id)); if (p.id) myPropIds.add(Number(p.id)); }); }; collectIds(myRent); collectIds(mySale); const propInfoId = mapped._raw?.propertyInformation?.id; if (myPropIds.has(Number(mapped.id)) || (propInfoId && myPropIds.has(Number(propInfoId)))) { setIsOwnProperty(true); } } catch (e) { console.warn("[OwnerCheck] failed:", e); } } } } catch (err) { console.error("[PropertyDetail] Failed:", err); } finally { setLoading(false); } } fetchProperty(); }, [params.id]); const fetchAvgRating = async (propId) => { try { const avg = await getPropertyAverageRating(propId); setAvgRating(avg); } catch {} }; const fetchContactInfo = async () => { if (!property) return; try { const info = await getOwnerContactInformation(property._raw?.propertyInformationId || property.id); setContactInfo(info); setShowContact(true); } catch (err) { toast.error(t("contactLoadFailed")); } }; const handleFavorite = async () => { if (!AuthService.isAuthenticated()) { setShowLoginDialog(true); return; } if (!property) return; setFavLoading(true); try { if (isFavorite(property.id)) { await removeFavorite(property.id); } else { await addFavorite(property.id); } } catch (err) { toast.error(t("errorOccurred")); } finally { setFavLoading(false); } }; const handleBookNow = async () => { if (!AuthService.isAuthenticated()) { setShowLoginDialog(true); return; } if (!bookingDates.start || !bookingDates.end) { setBookingError(t("validationDateRequired")); return; } setBookingLoading(true); setBookingError(null); try { await bookReservation(property._raw?.propertyInformationId || property.id, bookingDates.start, bookingDates.end); setBookingSuccess(true); toast.success(t("bookingSentSuccess")); } catch (err) { setBookingError(err.message || t("bookingFailed")); } finally { setBookingLoading(false); } }; const availableDatesSet = useMemo(() => { const dates = new Set(); if (!Array.isArray(availableRanges)) return dates; availableRanges.forEach((r) => { 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)) { dates.add(d.toISOString().split("T")[0]); } }); return dates; }, [availableRanges]); const handleBookingConfirm = async () => { if (!AuthService.isAuthenticated()) { setShowLoginDialog(true); return; } if (!selectedStart || !selectedEnd) { setBookingError(t("validationDateRequired")); return; } setBookingLoading(true); setBookingError(null); try { const propInfoId = property._raw?.propertyInformationId || property.id; const startDate = new Date(selectedStart + "T00:00:00.000").toISOString(); const endDate = new Date(selectedEnd + "T00:00:00.000").toISOString(); await bookReservation(propInfoId, startDate, endDate); setBookingSuccess(true); toast.success(t("bookingSentSuccess")); } catch (err) { setBookingError(err.message || t("bookingFailed")); } finally { setBookingLoading(false); } }; const handleRatingSuccess = () => { setShowRatingForm(false); if (property) fetchAvgRating(property.id); }; if (loading) { return{t("propertyNotFoundDesc")}
{t("noImages")}
{property.description}
{t("cannotBookOwnProperty")}
{t("bookingRequestWillBeReviewed")}
{t("loginRequiredDesc")}
{t("login")} {t("createAccount")}