Files
SweetHome/app/property/[id]/PropertyDetail.js
hamzaobed7 00dde588cb
All checks were successful
Build frontend / build (push) Successful in 1m46s
fixed getSaleProperiesById and make login type is email
2026-08-11 16:04:09 +03:00

1082 lines
50 KiB
JavaScript

"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: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> 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 <div ref={mapRef} className="h-full w-full" />;
}
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 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 <Loading />;
}
if (!property) {
return (
<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>
<h2 className="text-2xl font-bold text-gray-900 mb-2">{t("propertyNotFound")}</h2>
<p className="text-gray-600 mb-6">{t("propertyNotFoundDesc")}</p>
<Link href="/properties" className="inline-flex items-center gap-2 bg-amber-500 text-white px-6 py-3 rounded-xl font-medium hover:bg-amber-600">
<ArrowLeft className="w-5 h-5" />
{t("backToProperties")}
</Link>
</div>
</div>
);
}
const isFav = isFavorite(property.id);
const isRoomType = property.type === "room";
const isMostRequested = avgRating !== null && avgRating >= 4.5;
const showPricingToggle = property.isRent && property.priceDisplay?.daily > 0 && property.priceDisplay?.monthly > 0;
const effectivePricingMode = showPricingToggle ? pricingMode : property.isRent && property.priceDisplay?.monthly > 0 ? "monthly" : "daily";
return (
<div className="min-h-screen bg-gray-50" dir={i18n.language === "ar" ? "rtl" : "ltr"}>
<Toaster position="top-center" reverseOrder={false} />
<div className="container mx-auto px-4 py-6">
<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">
<ArrowLeft className="w-5 h-5" />
{t("backToProperties")}
</Link>
</motion.div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-5">
{/* Image Gallery */}
<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" }}>
{property.images.length > 0 ? (
<img src={property.images[currentImage]} alt={property.title} className="w-full h-full object-contain mx-auto" style={{ minHeight: "380px", maxHeight: "460px" }} />
) : (
<div className="w-full h-full flex items-center justify-center" style={{ minHeight: "420px" }}>
<div className="text-center">
<ImageIcon className="w-20 h-20 text-gray-500 mx-auto mb-2" />
<p className="text-gray-400 text-sm">{t("noImages")}</p>
</div>
</div>
)}
{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" /> {t("mostRequested")}
</span>
</div>
)}
{property.images.length > 1 && (
<>
<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"
>
<ChevronRight className="w-5 h-5" />
</button>
<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"
>
<ChevronLeft className="w-5 h-5" />
</button>
</>
)}
<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-black/70 text-white px-3 py-1 rounded-full text-sm font-medium backdrop-blur-sm">{t("buildingType." + property.typeLabel)}</span>
</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}
</div>
</div>
{property.images.length > 1 && (
<div className="flex gap-2 p-3 bg-gray-50 overflow-x-auto" style={{ scrollBehavior: "smooth" }}>
{property.images.map((img, idx) => (
<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" />
</button>
))}
</div>
)}
</motion.div>
{/* Property Info */}
<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">
<div className="flex justify-between items-start mb-3">
<div>
<div className="flex items-center gap-1.5 mb-1 flex-wrap">
<span className="px-2 py-0.5 bg-amber-100 text-amber-800 rounded-full text-xs">{t("buildingType." + 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"}`}>
{t("propertyStatus." + property.statusLabel)}
</span>
{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();
if (dt === "both" || dt.includes("both")) return t("dailyAndMonthly");
if (dt.includes("daily")) return t("rentType.daily");
if (dt.includes("monthly")) return t("rentType.monthly");
return dt;
})()}
</span>
)}
<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 ? t("rentCondition.furnished") : t("rentCondition.unfurnished")}
</span>
</div>
<h1 className="text-xl font-bold text-gray-900">{property.title || t("propertyWithId", { id: property.id })}</h1>
<div className="flex items-center gap-1 text-gray-500 text-xs mt-0.5">
<MapPin className="w-3 h-3" />
<span>{property.location.address || t("city." + property.location.city)}</span>
</div>
</div>
<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"}`} />
</button>
</div>
{/* Price */}
<div className="bg-[#1E293B] rounded-xl p-3 mb-4">
{property.isRent ? (
<div className="flex flex-wrap gap-6 items-end">
{property.priceDisplay.monthly > 0 && (
<div>
<span className="text-2xl font-bold text-amber-600">{formatCurrency(property.priceDisplay.monthly)}</span>
<span className="text-gray-500 mr-1">{t("sypPerMonth")}</span>
</div>
)}
{property.priceDisplay.daily > 0 && (
<div>
<span className="text-2xl font-bold text-amber-600">{formatCurrency(property.priceDisplay.daily)}</span>
<span className="text-gray-500 mr-1">{t("sypPerDay")}</span>
</div>
)}
{property.deposit > 0 && (
<div className="text-sm text-gray-500">
<span className="font-medium">{t("depositLabel")}</span> {formatCurrency(property.deposit)} {t("syp")}
</div>
)}
</div>
) : (
<div>
<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-sm text-gray-400 mr-2">{t("forSale")}</span>
</div>
)}
</div>
{/* Specs Tiles */}
<div className="grid grid-cols-3 md:grid-cols-6 gap-1.5 mb-4">
{property.bedrooms > 0 && (
<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" />
<div className="font-bold text-gray-900 text-sm">{property.bedrooms}</div>
<div className="text-[10px] text-gray-500">{t("bedrooms")}</div>
</div>
)}
{property.bathrooms > 0 && (
<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" />
<div className="font-bold text-gray-900 text-sm">{property.bathrooms}</div>
<div className="text-[10px] text-gray-500">{t("bathrooms")}</div>
</div>
)}
{property.area > 0 && (
<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" />
<div className="font-bold text-gray-900 text-sm">{property.area}</div>
<div className="text-[10px] text-gray-500">{t("sqm")}</div>
</div>
)}
{property.floor > 0 && (
<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" />
<div className="font-bold text-gray-900 text-sm">{property.floor}</div>
<div className="text-[10px] text-gray-500">{t("floor")}</div>
</div>
)}
{property.salons > 0 && (
<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" />
<div className="font-bold text-gray-900 text-sm">{property.salons}</div>
<div className="text-[10px] text-gray-500">{t("salons")}</div>
</div>
)}
{property.balconies > 0 && (
<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" />
<div className="font-bold text-gray-900 text-sm">{property.balconies}</div>
<div className="text-[10px] text-gray-500">{t("balconies")}</div>
</div>
)}
{avgRating !== null && avgRating > 0 && (
<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" />
<div className="font-bold text-gray-900 text-sm">{avgRating.toFixed(1)}</div>
<div className="text-[10px] text-gray-500">{t("rateAndReview")}</div>
</div>
)}
{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" />
<div className="font-bold text-gray-900 text-sm">{property.bookedCount}</div>
<div className="text-[10px] text-gray-500">{t("bookingCount")}</div>
</div>
)}
</div>
{/* Description */}
{property.description && (
<div className="mb-4">
<h3 className="font-bold text-gray-900 mb-1 text-sm">{t("description")}</h3>
<p className="text-gray-600 text-sm leading-relaxed">{property.description}</p>
</div>
)}
{/* Features */}
<div className="flex flex-wrap gap-1.5 mb-4">
{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" /> {t("smokingAllowed")}
</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" /> {t("propertyTerm.noSmoking")}
</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" /> {t("visitorsAllowed")}
</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>
)}
</div>
{/* Services with detail text */}
{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">{t("services")}</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">
{t(svcKey(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">
{t(svcKey(key))}
{detail && <span className="text-green-400">· {detail}</span>}
</span>
);
})}
</div>
</div>
)}
{/* Room Details (only for room type) */}
{isRoomType && Object.keys(property.roomDetails).length > 0 && (
<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" />
{t("roomDetails")}
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{(() => {
const rd = property.roomDetails;
const items = [];
if (rd.areaType)
items.push({
label: t("roomDetail.areaType"),
value: rd.areaType === "private room" ? t("roomDetail.privateRoom") : rd.areaType === "shared room" ? t("roomDetail.sharedRoom") : rd.areaType,
});
if (rd.peopleAllowed)
items.push({
label: t("roomDetail.numberOfPeople"),
value: rd.peopleAllowed,
});
if (rd.furnitureDetails || rd.furniture)
items.push({
label: t("roomDetail.furniture"),
value: rd.furnitureDetails || rd.furniture,
});
if (rd.entranceType)
items.push({
label: t("roomDetail.entranceType"),
value:
rd.entranceType === "shared entrance" ? t("roomDetail.sharedEntrance") : rd.entranceType === "independent entrance" ? t("roomDetail.independentEntrance") : rd.entranceType,
});
if (rd.bathroomType)
items.push({
label: t("roomDetail.bathroom"),
value: rd.bathroomType === "room specific" ? t("roomDetail.bathroomPrivate") : rd.bathroomType === "shared" ? t("roomDetail.bathroomShared") : rd.bathroomType,
});
if (rd.kitchenType)
items.push({
label: t("roomDetail.kitchen"),
value: rd.kitchenType === "shared" ? t("roomDetail.kitchenShared") : rd.kitchenType === "not available" ? t("roomDetail.kitchenNotAvailable") : rd.kitchenType,
});
if (rd.homeResidentsCount ?? rd.residents)
items.push({
label: t("roomDetail.residents"),
value: rd.homeResidentsCount ?? rd.residents,
});
if (rd.currentPopulationGender)
items.push({
label: t("roomDetail.gender"),
value:
rd.currentPopulationGender === "men"
? t("roomDetail.men")
: rd.currentPopulationGender === "women"
? t("roomDetail.women")
: rd.currentPopulationGender === "family"
? t("roomDetail.family")
: rd.currentPopulationGender,
});
if (rd.dedicatedTo)
items.push({
label: t("roomDetail.dedicatedTo"),
value:
rd.dedicatedTo === "men only"
? t("roomDetail.menOnly")
: rd.dedicatedTo === "women only"
? t("roomDetail.womenOnly")
: rd.dedicatedTo === "families only"
? t("roomDetail.familiesOnly")
: rd.dedicatedTo === "everyone"
? t("roomDetail.everyone")
: rd.dedicatedTo,
});
if (rd.hasRestrictedOwnerAreas !== undefined)
items.push({
label: t("roomDetail.restrictedAreas"),
value: rd.hasRestrictedOwnerAreas ? t("roomDetail.yes") : t("roomDetail.no"),
});
if (rd.hasChildren !== undefined)
items.push({
label: t("roomDetail.children"),
value: rd.hasChildren ? t("roomDetail.allowed") : t("roomDetail.notAllowed"),
});
if (rd.hasPets !== undefined)
items.push({
label: t("roomDetail.pets"),
value: rd.hasPets ? t("roomDetail.allowed") : t("roomDetail.notAllowed"),
});
if (rd.languageDialect)
items.push({
label: t("roomDetail.language"),
value: rd.languageDialect,
});
if (rd.visitorsAllowed !== undefined)
items.push({
label: t("roomDetail.visitors"),
value: rd.visitorsAllowed ? t("roomDetail.allowed") : t("roomDetail.forbidden"),
});
if (rd.quietTimesEnabled ?? rd.quietTimes)
items.push({
label: t("roomDetail.quietHours"),
value: rd.quietTimesDetails || rd.quietTimes || (rd.quietTimesEnabled ? t("roomDetail.quietHoursEnabled") : ""),
});
return items.map((item, i) => (
<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>
</div>
));
})()}
</div>
</div>
)}
{/* Proximity */}
{Object.keys(property.proximity).length > 0 && (
<div className="mb-4">
<h3 className="font-bold text-gray-900 mb-2 text-sm">{t("proximityToServices")}</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-1.5">
{Object.entries(property.proximity).map(([key, val]) => {
if (!val) return null;
const dist = typeof val === "object" ? val.distance : val;
return (
<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 === "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 === "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","Mosque"].includes(key) && <MapPin className="w-3.5 h-3.5 text-amber-500 flex-shrink-0" />}
<div>
<div className="text-xs font-medium text-gray-900">{t(proxKey(key))}</div>
<div className="text-[10px] text-gray-500">
{dist} {typeof dist === "number" ? t("km") : ""}
</div>
</div>
</div>
);
})}
</div>
</div>
)}
{/* Terms as checklist */}
{Object.keys(property.terms).length > 0 && (
<div className="mb-4">
<h3 className="font-bold text-gray-900 mb-2 text-sm">{t("terms")}</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-1.5">
{Object.entries(property.terms).map(([key, val]) => {
if (!val) return null;
return (
<div key={key} className="flex items-center gap-1.5 p-1.5 bg-gray-50 rounded-lg">
{key.startsWith("No") || key.startsWith("Only") ? <Ban className="w-3.5 h-3.5 text-red-500 flex-shrink-0" /> : <Check className="w-3.5 h-3.5 text-green-500 flex-shrink-0" />}
<span className="text-xs text-gray-700">{t(termKey(key))}</span>
</div>
);
})}
</div>
</div>
)}
</motion.div>
{/* Map */}
{property.location.lat && property.location.lng && (
<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="h-64">
<PropertyDetailMap lat={property.location.lat} lng={property.location.lng} title={property.title} />
</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>{t("approximateLocation")}</span>
</div>
<div className="p-3 border-t border-gray-100">
<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"
>
<ExternalLink className="w-4 h-4" />
{t("openInGoogleMaps")}
</a>
</div>
</motion.div>
)}
{/* Ratings Section */}
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }}>
<PropertyRatingList propertyId={property._raw?.propertyInformationId || property.id} />
</motion.div>
</div>
{/* Sidebar */}
<div className="space-y-4">
{/* Booking Card */}
{property.isRent && (
<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">
{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>
<h4 className="font-bold text-gray-700 text-sm mb-1">{t("thisIsYourProperty")}</h4>
<p className="text-xs text-gray-500">{t("cannotBookOwnProperty")}</p>
</div>
) : bookingSuccess ? (
<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" />
</div>
<h4 className="font-bold text-green-700 text-sm mb-1">{t("bookingRequestSent")}</h4>
<p className="text-xs text-gray-500">{t("bookingRequestWillBeReviewed")}</p>
</div>
) : (
<>
{/* Pricing Mode Toggle */}
{showPricingToggle && (
<div className="grid grid-cols-2 gap-2 mb-3">
<button
onClick={() => setPricingMode("daily")}
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-sm font-bold text-amber-600">
{formatCurrency(property.priceDisplay.daily)} {t("syp")}
</div>
</button>
<button
onClick={() => setPricingMode("monthly")}
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-sm font-bold text-amber-600">
{formatCurrency(property.priceDisplay.monthly)} {t("syp")}
</div>
</button>
</div>
)}
{/* Calendar */}
<DateSelectionCalendar
mode={effectivePricingMode}
availableDates={availableDatesSet}
selectedStart={selectedStart}
selectedEnd={selectedEnd}
onSelectStart={(d) => {
setSelectedStart(d);
setSelectedEnd(null);
}}
onSelectEnd={setSelectedEnd}
/>
{/* 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">{t("startDateLabel")}</span>
<span className="font-medium text-gray-900">{selectedStart}</span>
</div>
{selectedEnd && (
<>
<div className="flex justify-between text-xs">
<span className="text-gray-500">{t("endDateLabel")}</span>
<span className="font-medium text-gray-900">{selectedEnd}</span>
</div>
<div className="border-t border-gray-200 my-1 " />
<div className="flex justify-between text-xs">
<span className="text-gray-500">{effectivePricingMode === "daily" ? t("numberOfDays") : t("numberOfMonths")}</span>
<span className="font-medium text-gray-900">
{effectivePricingMode === "daily"
? Math.max(1, Math.round((new Date(selectedEnd) - new Date(selectedStart)) / (1000 * 60 * 60 * 24)))
: new Date(selectedEnd).getMonth() - new Date(selectedStart).getMonth() + (new Date(selectedEnd).getFullYear() - new Date(selectedStart).getFullYear()) * 12 + 1}
</span>
</div>
<div className="flex justify-between text-xs font-bold">
<span className="text-gray-700">{t("total")}</span>
<span className="text-amber-600">
{formatCurrency(
effectivePricingMode === "daily"
? Math.max(1, Math.round((new Date(selectedEnd) - new Date(selectedStart)) / (1000 * 60 * 60 * 24))) * property.priceDisplay.daily
: (new Date(selectedEnd).getMonth() - new Date(selectedStart).getMonth() + (new Date(selectedEnd).getFullYear() - new Date(selectedStart).getFullYear()) * 12 + 1) *
property.priceDisplay.monthly,
)}{" "}
{t("syp")}
</span>
</div>
{property.deposit > 0 && (
<div className="flex justify-between text-xs">
<span className="text-gray-500">{t("deposit")}</span>
<span className="font-medium text-gray-900">
{formatCurrency(property.deposit)} {t("syp")}
</span>
</div>
)}
</>
)}
</div>
)}
{bookingError && <div className="bg-red-50 text-red-600 p-2.5 rounded-xl text-xs mb-3">{bookingError}</div>}
<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 ? t("bookingInProgress") : t("confirmBooking")}
</button>
</>
)}
</motion.div>
)}
</div>
</div>
</div>
{/* Login Dialog */}
{showLoginDialog && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onClick={() => setShowLoginDialog(false)}>
<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">
<LogIn className="w-8 h-8 text-amber-600" />
</div>
<h3 className="text-xl font-bold mb-2">{t("loginRequiredTitle")}</h3>
<p className="text-gray-500 mb-4">{t("loginRequiredDesc")}</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">
{t("login")}
</Link>
<Link href="/auth/choose-role" className="block w-full bg-gray-100 py-3 rounded-xl font-medium hover:bg-gray-200">
{t("createAccount")}
</Link>
<button onClick={() => setShowLoginDialog(false)} className="mt-3 text-gray-400 hover:text-gray-600">
{t("cancel")}
</button>
</div>
</div>
)}
</div>
);
}
//reset