Files
SweetHome/app/properties/page.js

831 lines
36 KiB
JavaScript
Raw Normal View History

"use client";
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Search, MapPin, Bed, Bath, Square, Filter, Grid3x3, List, Heart, ChevronDown, Star, Home, Building2, Phone, FileText } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { getRentProperties, getSaleProperties } from "../utils/api";
import { useFavorites } from "@/app/contexts/FavoritesContext";
import AuthService from "@/app/services/AuthService";
import toast, { Toaster } from "react-hot-toast";
import { useTranslation } from "react-i18next";
function mapApiProperty(t, item, index) {
const info = item.propertyInformation || {};
const dailyPrice = item.dailyRent ?? item.monthlyRent ?? item.price ?? 0;
const buildingTypeMap = {
0: "apartment",
1: "villa",
2: "sweet",
3: "room",
4: "studio",
5: "office",
6: "farms",
7: "shop",
8: "warehouse",
2026-07-15 04:38:32 -07:00
};
const propType = buildingTypeMap[info.buildingType] ?? buildingTypeMap[item.type] ?? "apartment";
const statusMap = { 0: "available", 1: "notAvailable", 2: "booked" };
const status = statusMap[info.status] ?? statusMap[item.status] ?? "available";
2026-07-15 04:38:32 -07:00
const cityMap = {
0: "damascus",
1: "aleppo",
2: "homs",
3: "latakia",
4: "daraa",
5: "tartous",
6: "suweida",
7: "deirEzzor",
8: "raqqa",
9: "idlib",
10: "hasakah",
11: "qamishli",
12: "ruralDamascus",
2026-07-15 04:38:32 -07:00
};
const features = [];
if (item.isSmokeAllow) features.push(t("smoke-allowed", "مسموح التدخين"));
if (item.isVisitorAllow) features.push(t("visitors-allowed", "مسموح الزوار"));
if (item.specializedFor) features.push(t("specialized", "مخصص"));
if (info.numberOfBedRooms) features.push(`${info.numberOfBedRooms} ${t("bedrooms", "غرف نوم")}`);
if (info.numberOfBathRooms) features.push(`${info.numberOfBathRooms} ${t("bathrooms", "حمامات")}`);
2026-07-15 04:38:32 -07:00
const apiBase = typeof window !== "undefined" ? process.env.NEXT_PUBLIC_API_URL || "http://45.93.137.91/api" : "";
const rawImages = Array.isArray(info.images) ? info.images : [];
const images = rawImages.length > 0 ? rawImages.map((img) => (img.startsWith("http") ? img : `${apiBase}${img.startsWith("/") ? "" : "/Pictures/"}${img}`)) : ["/property-placeholder.jpg"];
return {
id: item.id ?? index + 1,
title: info.address || `${t("property", "عقار")} #${item.id || index + 1}`,
description: info.description || "",
type: propType,
price: dailyPrice,
priceUnit: "daily",
location: {
city: cityMap[info.city] || extractCity(info.address) || "damascus",
district: info.address || "",
},
bedrooms: info.numberOfBedRooms || 0,
bathrooms: info.numberOfBathRooms || 0,
area: info.space || 0,
features,
images,
status,
2026-05-26 20:24:37 +03:00
rating: item.rating || 0,
isNew: false,
_raw: item,
};
}
function extractCity(address) {
if (!address) return "";
const cityMap = {
دمشق: "damascus",
حلب: "aleppo",
حمص: "homs",
اللاذقية: "latakia",
درعا: "daraa",
طرطوس: "tartous",
السويداء: "suweida",
"دير الزور": "deirEzzor",
الرقة: "raqqa",
إدلب: "idlib",
الحسكة: "hasakah",
القامشلي: "qamishli",
"ريف دمشق": "ruralDamascus",
};
for (const [arabic, key] of Object.entries(cityMap)) {
if (address.includes(arabic)) return key;
}
return "";
}
const PropertyCard = ({ property, viewMode = "grid", onLoginRequired, type = "rent" }) => {
const { t, i18n } = useTranslation();
const { isFavorite: checkFavorite, addFavorite, removeFavorite } = useFavorites();
const [favLoading, setFavLoading] = useState(false);
const [currentImage, setCurrentImage] = useState(0);
const isFav = checkFavorite(property.id);
const toggleFavorite = async (e) => {
e.preventDefault();
e.stopPropagation();
if (!AuthService.isAuthenticated()) {
onLoginRequired?.();
return;
}
setFavLoading(true);
if (isFav) {
await removeFavorite(property.id);
} else {
await addFavorite(property.id);
}
setFavLoading(false);
};
const formatCurrency = (amount) => {
return amount?.toLocaleString() + " " + t("syp-symbol", "ل.س");
};
const getPropertyTypeIcon = (type) => {
switch (type) {
case "villa":
return <Home className="w-4 h-4" />;
case "apartment":
return <Building2 className="w-4 h-4" />;
case "house":
return <Home className="w-4 h-4" />;
case "studio":
return <Building2 className="w-4 h-4" />;
default:
return <Home className="w-4 h-4" />;
}
};
const getPropertyTypeLabel = (type) => {
switch (type) {
case "villa":
return t("buildingType.villa", "فيلا");
case "apartment":
return t("buildingType.apartment", "شقة");
case "house":
return t("house", "منزل");
case "studio":
return t("buildingType.studio", "ستوديو");
case "sweet":
return t("buildingType.sweet", "جناح");
case "room":
return t("buildingType.room", "غرفة");
case "office":
return t("buildingType.office", "مكتب");
case "farms":
return t("buildingType.farms", "مزرعة");
case "shop":
return t("buildingType.shop", "محل تجاري");
case "warehouse":
return t("buildingType.warehouse", "مستودع");
default:
return type;
}
};
if (viewMode === "list") {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-2xl shadow-sm hover:shadow-md transition-all duration-300 overflow-hidden border border-gray-100"
>
<div className="flex flex-col md:flex-row">
<div className="md:w-1/3 relative h-64 md:h-auto bg-gray-100">
<Image src={property.images[currentImage] || "/property-placeholder.jpg"} alt={property.title} fill loading="lazy" className="object-cover" />
{property.images.length > 1 && (
<div className="absolute bottom-2 left-2 right-2 flex justify-center gap-1">
{property.images.map((_, idx) => (
<button key={idx} onClick={() => setCurrentImage(idx)} className={`w-1.5 h-1.5 rounded-full transition-all ${idx === currentImage ? "bg-gray-800 w-3" : "bg-white/70"}`} />
))}
</div>
)}
<div className="absolute top-2 right-2 flex gap-2">
<button
onClick={toggleFavorite}
disabled={favLoading}
className="w-8 h-8 bg-white/90 backdrop-blur-sm rounded-full flex items-center justify-center hover:bg-white transition-colors shadow-sm"
>
<Heart className={`w-4 h-4 ${isFav ? "fill-red-500 text-red-500" : "text-gray-600"}`} />
</button>
</div>
</div>
<div className="md:w-2/3 p-6">
<div className="flex justify-between items-start mb-3">
<div>
<div className="flex items-center gap-2 mb-2">
<span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-lg text-xs font-medium flex items-center gap-1">
{getPropertyTypeIcon(property.type)}
{getPropertyTypeLabel(property.type)}
</span>
<span className={`px-2 py-1 rounded-lg text-xs font-medium ${property.status === "available" ? "bg-gray-800 text-white" : "bg-gray-200 text-gray-600"}`}>
{property.status === "available" ? t("available", "متاح") : t("booked", "محجوز")}
</span>
</div>
<h3 className="text-xl font-bold text-gray-900 mb-1">{property.title}</h3>
<div className="flex items-center gap-1 text-gray-500 text-sm mb-3">
<MapPin className="w-4 h-4" />
{t("city." + property.location.city, property.location.city)}، {property.location.district}
</div>
</div>
<div className="text-left">
<div className="text-2xl font-bold text-gray-900">{formatCurrency(property.price)}</div>
<div className="text-xs text-gray-500">/{property.priceUnit === "daily" ? t("day", "يوم") : t("month", "شهر")}</div>
</div>
</div>
<div className="flex flex-wrap gap-4 mb-4">
<div className="flex items-center gap-1 text-gray-600">
<Bed className="w-4 h-4" />
<span>
{property.bedrooms} {t("rooms", "غرف")}
</span>
</div>
<div className="flex items-center gap-1 text-gray-600">
<Bath className="w-4 h-4" />
<span>
{property.bathrooms} {t("bathrooms", "حمامات")}
</span>
</div>
<div className="flex items-center gap-1 text-gray-600">
<Square className="w-4 h-4" />
<span>
{property.area} {t("sqm", "م²")}
</span>
</div>
</div>
<p className="text-gray-600 text-sm mb-4 line-clamp-2">{property.description}</p>
<div className="flex gap-3">
//
<Link href={`/property/${property.id}?type=${type}`} className="flex-1 bg-gray-800 text-white py-3 rounded-xl font-medium hover:bg-gray-900 transition-colors text-center">
{t("viewDetails", "عرض التفاصيل")}
</Link>
<button className="px-4 bg-gray-100 text-gray-700 py-3 rounded-xl font-medium hover:bg-gray-200 transition-colors flex items-center gap-2">
<Phone className="w-4 h-4" />
</button>
</div>
</div>
</div>
</motion.div>
);
}
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-2xl shadow-sm hover:shadow-md transition-all duration-300 overflow-hidden border border-gray-100"
>
<div className="relative h-56 bg-gray-100">
<Image src={property.images[currentImage] || "/property-placeholder.jpg"} alt={property.title} fill className="object-cover" />
<div className="absolute top-2 right-2 flex gap-2">
<button
onClick={toggleFavorite}
disabled={favLoading}
className="w-8 h-8 bg-white/90 backdrop-blur-sm rounded-full flex items-center justify-center hover:bg-white transition-colors shadow-sm"
>
<Heart className={`w-4 h-4 ${isFav ? "fill-red-500 text-red-500" : "text-gray-600"}`} />
</button>
</div>
</div>
<div className="p-5">
<div className="flex justify-between items-start mb-3">
<div>
<div className="flex items-center gap-2 mb-2">
<span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-lg text-xs font-medium flex items-center gap-1">
{getPropertyTypeIcon(property.type)}
{getPropertyTypeLabel(property.type)}
</span>
{property.status === "available" && <span className="px-2 py-1 bg-gray-800 text-white rounded-lg text-xs font-medium">{t("available", "متاح")}</span>}
</div>
<h3 className="font-bold text-gray-900 mb-1 line-clamp-1">{property.title}</h3>
<div className="flex items-center gap-1 text-gray-500 text-xs mb-2">
<MapPin className="w-3 h-3" />
<span className="line-clamp-1">
{t("city." + property.location.city, property.location.city)}، {property.location.district}
</span>
</div>
</div>
<div className="text-left">
<div className="text-xl font-bold text-gray-900">{formatCurrency(property.price)}</div>
<div className="text-xs text-gray-500">/{property.priceUnit === "daily" ? t("day", "يوم") : t("month", "شهر")}</div>
</div>
</div>
<div className="flex justify-between items-center mb-4">
<div className="flex items-center gap-3 text-gray-600 text-sm">
<div className="flex items-center gap-1">
<Bed className="w-4 h-4" />
<span>{property.bedrooms}</span>
</div>
<div className="flex items-center gap-1">
<Bath className="w-4 h-4" />
<span>{property.bathrooms}</span>
</div>
<div className="flex items-center gap-1">
<Square className="w-4 h-4" />
<span>
{property.area}
{t("sqm", "م²")}
</span>
</div>
</div>
2026-05-26 20:24:37 +03:00
{property.rating > 0 && (
2026-07-15 04:38:32 -07:00
<div className="flex items-center gap-1">
<Star className="w-4 h-4 fill-amber-500 text-amber-500" />
<span className="text-sm font-medium text-gray-700">{property.rating.toFixed(1)}</span>
</div>
2026-05-26 20:24:37 +03:00
)}
</div>
<Link href={`/property/${property.id}?type=${type}`} className="block w-full bg-gray-800 text-white py-3 rounded-xl font-medium hover:bg-gray-900 transition-colors text-center">
{t("viewDetails", "عرض التفاصيل")}
</Link>
</div>
</motion.div>
);
};
const FilterBar = ({ filters, onFilterChange }) => {
2026-07-15 04:38:32 -07:00
const { t } = useTranslation();
const [showFilters, setShowFilters] = useState(false);
const propertyTypes = [
{ id: "all", label: t("all", "الكل") },
{ id: "residential", label: t("propertyType.residential", "سكني") },
{ id: "commercial", label: t("propertyType.commercial", "تجاري") },
{ id: "agricultural", label: t("propertyType.agricultural", "زراعي") },
2026-07-15 04:38:32 -07:00
];
const buildingTypes = [
{ id: "all", label: t("all", "الكل") },
{ id: "apartment", label: t("buildingType.apartment", "شقة"), icon: Building2 },
{ id: "villa", label: t("buildingType.villa", "فيلا"), icon: Home },
{ id: "sweet", label: t("buildingType.sweet", "جناح"), icon: Home },
{ id: "room", label: t("buildingType.room", "غرفة"), icon: Home },
{ id: "studio", label: t("buildingType.studio", "ستوديو"), icon: Building2 },
{ id: "office", label: t("buildingType.office", "مكتب"), icon: Building2 },
{ id: "farms", label: t("buildingType.farms", "مزرعة"), icon: Home },
{ id: "shop", label: t("buildingType.shop", "محل تجاري"), icon: Building2 },
{ id: "warehouse", label: t("buildingType.warehouse", "مستودع"), icon: Building2 },
];
const priceRanges = [
{ id: "all", label: t("allPrices", "جميع الأسعار") },
{ id: "0-500000", label: t("price-range-less-than-500k", "أقل من 500 ألف") },
{ id: "500000-1000000", label: t("price-range-500k-to-1m", "500 ألف - 1 مليون") },
{ id: "1000000-2000000", label: t("price-range-1m-to-2m", "1 مليون - 2 مليون") },
{ id: "2000000-5000000", label: t("price-range-2m-to-5m", "2 مليون - 5 مليون") },
{ id: "5000000+", label: t("price-range-more-than-5m", "أكثر من 5 مليون") },
];
const cities = [
{ id: "all", label: t("allCities", "جميع المدن") },
{ id: "damascus", label: t("city.damascus", "دمشق") },
{ id: "aleppo", label: t("city.aleppo", "حلب") },
{ id: "homs", label: t("city.homs", "حمص") },
{ id: "latakia", label: t("city.latakia", "اللاذقية") },
{ id: "daraa", label: t("city.daraa", "درعا") },
{ id: "tartous", label: t("city.tartous", "طرطوس") },
{ id: "suweida", label: t("city.suweida", "السويداء") },
{ id: "deirEzzor", label: t("city.deirEzzor", "دير الزور") },
{ id: "raqqa", label: t("city.raqqa", "الرقة") },
{ id: "idlib", label: t("city.idlib", "إدلب") },
{ id: "hasakah", label: t("city.hasakah", "الحسكة") },
{ id: "qamishli", label: t("city.qamishli", "القامشلي") },
{ id: "ruralDamascus", label: t("city.ruralDamascus", "ريف دمشق") },
2026-07-15 04:38:32 -07:00
];
const certificates = [
{ id: "all", label: t("allCertificates", "جميع السندات") },
{ id: "realEstateTitle", label: t("certificate.realEstateTitle", "طابو أخضر") },
{ id: "courtRuling", label: t("certificate.courtRuling", "حكم محكمة") },
{ id: "notary", label: t("certificate.notary", "كاتب عدل") },
{ id: "shares", label: t("certificate.shares", "أسهم عقارية") },
];
return (
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-4">
<div className="flex flex-col md:flex-row gap-3 mb-4">
<div className="flex-1 relative">
<Search className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
type="text"
placeholder={t("search-property-placeholder", "ابحث عن عقار...")}
className="w-full pr-12 px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300 transition-all"
value={filters.search}
onChange={(e) => onFilterChange({ ...filters, search: e.target.value })}
/>
</div>
<button onClick={() => setShowFilters(!showFilters)} className="px-6 py-3 bg-gray-100 rounded-xl font-medium hover:bg-gray-200 transition-colors flex items-center gap-2 text-gray-700">
<Filter className="w-5 h-5" />
{t("advanced-filters", "فلاتر متقدمة")}
<ChevronDown className={`w-4 h-4 transition-transform ${showFilters ? "rotate-180" : ""}`} />
</button>
</div>
<AnimatePresence>
{showFilters && (
<motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: "auto", opacity: 1 }} exit={{ height: 0, opacity: 0 }} className="overflow-hidden">
2026-07-15 04:38:32 -07:00
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 pt-4 border-t border-gray-100">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">{t("property-type", "نوع الاستثمار")}</label>
2026-07-15 04:38:32 -07:00
<select
value={filters.propertyType}
onChange={(e) => onFilterChange({ ...filters, propertyType: e.target.value })}
className="w-full px-4 py-2 border bg-[#1e293b] border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300 text-sm"
2026-07-15 04:38:32 -07:00
>
{propertyTypes.map((type) => (
<option key={type.id} value={type.id}>
{type.label}
</option>
2026-07-15 04:38:32 -07:00
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">{t("building-type", "نوع البناء")}</label>
2026-07-15 04:38:32 -07:00
<select
value={filters.buildingType}
onChange={(e) => onFilterChange({ ...filters, buildingType: e.target.value })}
className="w-full px-4 bg-[#1e293b] py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300 text-sm"
2026-07-15 04:38:32 -07:00
>
{buildingTypes.map((type) => (
<option key={type.id} value={type.id}>
{type.label}
</option>
2026-07-15 04:38:32 -07:00
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">{t("city", "المدينة")}</label>
<select
value={filters.city}
onChange={(e) => onFilterChange({ ...filters, city: e.target.value })}
className="w-full px-4 py-2 border bg-[#1e293b] border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300 text-sm"
>
{cities.map((city) => (
<option key={city.id} value={city.id}>
{city.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">{t("price-range", "مجال السعر")}</label>
<select
value={filters.priceRange}
onChange={(e) => onFilterChange({ ...filters, priceRange: e.target.value })}
className="w-full px-4 py-2 border bg-[#1e293b] border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300 text-sm"
>
{priceRanges.map((range) => (
<option key={range.id} value={range.id}>
{range.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">{t("bedrooms", "عدد الغرف")}</label>
<select
value={filters.bedrooms}
onChange={(e) => onFilterChange({ ...filters, bedrooms: e.target.value })}
className="w-full px-4 py-2 border bg-[#1e293b] border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300 text-sm"
>
<option value="all">{t("all-numbers", "الكل")}</option>
<option value="1">1+</option>
<option value="2">2+</option>
<option value="3">3+</option>
<option value="4">4+</option>
<option value="5">5+</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">{t("bathrooms-label", "عدد الحمامات")}</label>
2026-07-15 04:38:32 -07:00
<select
value={filters.bathrooms}
onChange={(e) => onFilterChange({ ...filters, bathrooms: e.target.value })}
className="w-full px-4 py-2 border bg-[#1e293b] border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300 text-sm"
2026-07-15 04:38:32 -07:00
>
<option value="all">{t("all-numbers", "الكل")}</option>
2026-07-15 04:38:32 -07:00
<option value="1">1+</option>
<option value="2">2+</option>
<option value="3">3+</option>
<option value="4">4+</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">{t("certificate-label", "الشهادة / السند العقاري")}</label>
2026-07-15 04:38:32 -07:00
<select
value={filters.certificate}
onChange={(e) => onFilterChange({ ...filters, certificate: e.target.value })}
className="w-full px-4 py-2 border bg-[#1e293b] border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300 text-sm"
2026-07-15 04:38:32 -07:00
>
{certificates.map((cert) => (
<option key={cert.id} value={cert.id}>
{cert.label}
</option>
2026-07-15 04:38:32 -07:00
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">{t("area-sqm", "المساحة (م²)")}</label>
<div className="flex gap-2">
<input
type="number"
placeholder={t("from", "من")}
2026-07-15 04:38:32 -07:00
className="w-full px-4 py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300 text-sm"
value={filters.minArea}
onChange={(e) => onFilterChange({ ...filters, minArea: e.target.value })}
/>
<input
type="number"
placeholder={t("to", "إلى")}
2026-07-15 04:38:32 -07:00
className="w-full px-4 py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300 text-sm"
value={filters.maxArea}
onChange={(e) => onFilterChange({ ...filters, maxArea: e.target.value })}
/>
</div>
</div>
</div>
<div className="flex gap-3 mt-4 pt-4 border-t border-gray-100">
<button
onClick={() =>
onFilterChange({
search: "",
propertyType: "all",
buildingType: "all",
city: "all",
priceRange: "all",
bedrooms: "all",
bathrooms: "all",
certificate: "all",
minArea: "",
maxArea: "",
features: [],
})
}
className="px-6 py-2 bg-gray-200 rounded-xl font-medium hover:bg-gray-200 transition-colors text-gray-700 text-sm"
>
{t("reset", "إعادة تعيين")}
</button>
<button onClick={() => setShowFilters(false)} className="px-6 py-2 bg-gray-800 text-white rounded-xl font-medium hover:bg-gray-900 transition-colors text-sm">
{t("apply-filters", "تطبيق")}
</button>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};
export default function PropertiesPage() {
const { t, i18n } = useTranslation();
const [purposeTab, setPurposeTab] = useState("rent");
const [viewMode, setViewMode] = useState("grid");
const [sortBy, setSortBy] = useState("newest");
const [properties, setProperties] = useState([]);
const [loading, setLoading] = useState(true);
const [showLoginDialog, setShowLoginDialog] = useState(false);
const [filters, setFilters] = useState({
search: "",
propertyType: "all",
buildingType: "all",
city: "all",
priceRange: "all",
bedrooms: "all",
bathrooms: "all",
certificate: "all",
minArea: "",
maxArea: "",
features: [],
});
useEffect(() => {
async function fetchProperties() {
2026-07-15 04:38:32 -07:00
setLoading(true);
try {
2026-07-15 04:38:32 -07:00
const queryParams = new URLSearchParams();
if (filters.bedrooms !== "all") {
queryParams.append("numberOfBedRooms", filters.bedrooms);
2026-07-15 04:38:32 -07:00
}
if (filters.bathrooms !== "all") {
queryParams.append("numberOfBathRooms", filters.bathrooms);
2026-07-15 04:38:32 -07:00
}
if (filters.minArea) {
queryParams.append("space", filters.minArea);
2026-07-15 04:38:32 -07:00
}
const buildingTypeReverseMap = {
apartment: "Apartment",
villa: "Villa",
sweet: "Sweet",
room: "Room",
studio: "Studio",
office: "Office",
farms: "Farms",
shop: "Shop",
warehouse: "Warehouse",
2026-07-15 04:38:32 -07:00
};
if (filters.buildingType !== "all" && buildingTypeReverseMap[filters.buildingType] !== undefined) {
queryParams.append("buildingType", buildingTypeReverseMap[filters.buildingType]);
2026-07-15 04:38:32 -07:00
}
const propertyTypeReverseMap = {
residential: "Residential",
commercial: "Commercial",
agricultural: "Agricultural",
2026-07-15 04:38:32 -07:00
};
if (filters.propertyType !== "all" && propertyTypeReverseMap[filters.propertyType] !== undefined) {
queryParams.append("propertyType", propertyTypeReverseMap[filters.propertyType]);
2026-07-15 04:38:32 -07:00
}
const certificateReverseMap = {
realEstateTitle: "RealEstateTitle",
courtRuling: "CourtRuling",
notary: "Notary",
shares: "Shares",
2026-07-15 04:38:32 -07:00
};
if (filters.certificate !== "all" && certificateReverseMap[filters.certificate] !== undefined) {
queryParams.append("certificate", certificateReverseMap[filters.certificate]);
2026-07-15 04:38:32 -07:00
}
if (filters.city !== "all") {
2026-07-15 04:38:32 -07:00
const cityFormatted = filters.city.charAt(0).toUpperCase() + filters.city.slice(1);
queryParams.append("city", cityFormatted);
2026-07-15 04:38:32 -07:00
}
const baseUrl = process.env.NEXT_PUBLIC_API_URL || "http://45.93.137.91/api";
2026-07-15 04:38:32 -07:00
const rentEndpoint = `${baseUrl}/RentProperties/FilterRentProperties?${queryParams.toString()}`;
const [rentRes, saleData] = await Promise.all([
fetch(rentEndpoint, {
headers: {
Accept: "application/json",
},
})
.then((res) => res.json())
.catch(() => ({ data: [], isSuccess: false })),
getSaleProperties().catch(() => []),
]);
2026-07-15 04:38:32 -07:00
const rentList = rentRes.isSuccess && Array.isArray(rentRes.data) ? rentRes.data : [];
const saleList = Array.isArray(saleData) ? saleData : [];
const mapped = [...rentList.map((p, i) => ({ ...mapApiProperty(t, p, i), purpose: "rent" })), ...saleList.map((p, i) => ({ ...mapApiProperty(t, p, rentList.length + i), purpose: "sale" }))];
2026-07-15 04:38:32 -07:00
setProperties(mapped);
} catch (err) {
console.error("[Properties] Failed to fetch properties:", err);
} finally {
setLoading(false);
}
}
fetchProperties();
2026-07-15 04:38:32 -07:00
}, [filters, t]);
const filteredProperties = properties
.filter((p) => p.purpose === purposeTab)
.filter((property) => {
if (filters.search && !property.title.includes(filters.search) && !property.description.includes(filters.search)) {
return false;
}
if (filters.priceRange !== "all") {
const [min, max] = filters.priceRange.split("-");
if (max) {
if (property.price < parseInt(min) || property.price > parseInt(max)) return false;
} else if (filters.priceRange.endsWith("+")) {
const minVal = parseInt(filters.priceRange.replace("+", ""));
if (property.price < minVal) return false;
}
}
return true;
})
.sort((a, b) => {
switch (sortBy) {
case "price_asc":
return a.price - b.price;
case "price_desc":
return b.price - a.price;
case "rating":
return b.rating - a.rating;
default:
return 0;
}
});
return (
<div dir={i18n.language === "ar" ? "rtl" : "ltr"} className="min-h-screen bg-gray-50 py-8">
<div className="container mx-auto px-4">
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="text-center mb-8">
<h1 className="text-4xl font-bold text-gray-900 mb-2">{purposeTab === "rent" ? t("properties-for-rent", "عقارات للإيجار") : t("properties-for-sale", "عقارات للبيع")}</h1>
<p className="text-gray-500">{t("best-properties-in-syria", "أفضل العقارات المتاحة في سوريا مباشرة وبسهولة")}</p>
2026-05-26 19:53:28 +03:00
<div className="flex justify-center mt-4">
<div className="inline-flex bg-gray-100 rounded-xl p-1">
<button
onClick={() => setPurposeTab("rent")}
className={`px-6 py-2 rounded-lg text-sm font-medium transition-all ${purposeTab === "rent" ? "bg-white text-amber-600 shadow-sm" : "text-gray-500 hover:text-gray-700"}`}
>
{t("for-rent", "للإيجار")}
2026-05-26 19:53:28 +03:00
</button>
<button
onClick={() => setPurposeTab("sale")}
className={`px-6 py-2 rounded-lg text-sm font-medium transition-all ${purposeTab === "sale" ? "bg-white text-blue-600 shadow-sm" : "text-gray-500 hover:text-gray-700"}`}
>
{t("for-sale", "للبيع")}
2026-05-26 19:53:28 +03:00
</button>
</div>
</div>
{loading && (
<div className="mt-4">
<div className="inline-block w-6 h-6 border-2 border-gray-200 border-t-gray-800 rounded-full animate-spin"></div>
</div>
)}
</motion.div>
<FilterBar filters={filters} onFilterChange={setFilters} />
<div className="flex justify-between items-center my-6">
<div className="text-gray-600">
<span className="font-bold text-gray-900">{filteredProperties.length}</span> {t("property-available", "عقار متاح")}
</div>
<div className="flex gap-3">
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
className="px-4 py-2 border bg-[#1e293b] border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300 text-gray-700 text-sm"
>
<option value="newest">{t("newest", "الأحدث")}</option>
<option value="price_asc">{t("price-low-to-high", "السعر من الأقل للأعلى")}</option>
<option value="price_desc">{t("price-high-to-low", "السعر من الأعلى للأقل")}</option>
<option value="rating">{t("rating", "التقييم")}</option>
</select>
<div className="flex gap-2">
<button
onClick={() => setViewMode("grid")}
className={`p-2 rounded-xl transition-colors ${viewMode === "grid" ? "bg-gray-800 text-white" : "bg-gray-100 text-gray-600 hover:bg-gray-200"}`}
>
<Grid3x3 className="w-5 h-5" />
</button>
<button
onClick={() => setViewMode("list")}
className={`p-2 rounded-xl transition-colors ${viewMode === "list" ? "bg-gray-800 text-white" : "bg-gray-100 text-gray-600 hover:bg-gray-200"}`}
>
<List className="w-5 h-5" />
</button>
</div>
</div>
</div>
<div className={viewMode === "grid" ? "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" : "space-y-4"}>
{filteredProperties.map((property) => (
<PropertyCard key={property.id} property={property} viewMode={viewMode} type={property.purpose} onLoginRequired={() => setShowLoginDialog(true)} />
))}
</div>
2026-07-15 04:38:32 -07:00
{!loading && filteredProperties.length === 0 && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-center py-16">
<div className="w-24 h-24 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Home className="w-12 h-12 text-gray-400" />
</div>
<h3 className="text-xl font-bold text-gray-700 mb-2">{t("no-properties", "لا يوجد نتائج تطابق بحثك")}</h3>
<p className="text-gray-500">{t("try-changing-search-criteria", "يرجى تجربة تعديل خيارات الفلترة والبحث")}</p>
</motion.div>
)}
</div>
<Toaster position="top-center" />
{showLoginDialog && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm" onClick={() => setShowLoginDialog(false)}>
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
onClick={(e) => e.stopPropagation()}
className="bg-white rounded-2xl p-6 max-w-sm w-full mx-4 shadow-xl text-center"
>
<div className="w-14 h-14 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Heart className="w-7 h-7 text-amber-600" />
</div>
<h3 className="text-xl font-bold text-gray-900 mb-2">{t("login-required-title", "تسجيل الدخول مطلوب")}</h3>
<p className="text-gray-500 mb-6">{t("login-required-favorites-desc", "يجب تسجيل الدخول لتتمكن من إضافة العقار للمفضلة")}</p>
<div className="flex gap-3">
<button onClick={() => setShowLoginDialog(false)} className="flex-1 py-3 border border-gray-200 rounded-xl font-medium text-gray-600 hover:bg-gray-50 transition-colors">
{t("cancel", "إلغاء")}
</button>
<Link href="/login" className="flex-1 py-3 bg-amber-500 text-white rounded-xl font-medium hover:bg-amber-600 transition-colors text-center">
{t("login", "دخول")}
</Link>
</div>
</motion.div>
</div>
)}
</div>
);
}