2026-02-15 01:53:37 +03:00
|
|
|
|
'use client';
|
|
|
|
|
|
|
2026-03-26 22:20:33 +00:00
|
|
|
|
import { useState, useEffect } from 'react';
|
2026-02-15 01:53:37 +03:00
|
|
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
|
|
|
|
import {
|
|
|
|
|
|
Search,
|
|
|
|
|
|
MapPin,
|
|
|
|
|
|
Bed,
|
|
|
|
|
|
Bath,
|
|
|
|
|
|
Square,
|
|
|
|
|
|
Filter,
|
|
|
|
|
|
Grid3x3,
|
|
|
|
|
|
List,
|
|
|
|
|
|
Heart,
|
|
|
|
|
|
ChevronDown,
|
|
|
|
|
|
Star,
|
|
|
|
|
|
Home,
|
|
|
|
|
|
Building2,
|
|
|
|
|
|
Phone,
|
2026-07-15 04:38:32 -07:00
|
|
|
|
FileText
|
2026-02-15 01:53:37 +03:00
|
|
|
|
} from 'lucide-react';
|
|
|
|
|
|
import Image from 'next/image';
|
|
|
|
|
|
import Link from 'next/link';
|
2026-03-26 22:20:33 +00:00
|
|
|
|
import { getRentProperties, getSaleProperties } from '../utils/api';
|
2026-03-30 17:54:42 +00:00
|
|
|
|
import { useFavorites } from '@/app/contexts/FavoritesContext';
|
|
|
|
|
|
import AuthService from '@/app/services/AuthService';
|
|
|
|
|
|
import toast, { Toaster } from 'react-hot-toast';
|
2026-07-01 15:43:58 +03:00
|
|
|
|
import { useTranslation } from 'react-i18next';
|
2026-03-26 22:20:33 +00:00
|
|
|
|
|
2026-07-01 15:43:58 +03:00
|
|
|
|
function mapApiProperty(t, item, index) {
|
2026-03-26 22:59:08 +00:00
|
|
|
|
const info = item.propertyInformation || {};
|
2026-03-26 22:20:33 +00:00
|
|
|
|
|
|
|
|
|
|
const dailyPrice = item.dailyRent ?? item.monthlyRent ?? item.price ?? 0;
|
|
|
|
|
|
|
2026-07-15 04:38:32 -07:00
|
|
|
|
const buildingTypeMap = {
|
|
|
|
|
|
0: 'apartment',
|
|
|
|
|
|
1: 'villa',
|
|
|
|
|
|
2: 'sweet',
|
|
|
|
|
|
3: 'room',
|
|
|
|
|
|
4: 'studio',
|
|
|
|
|
|
5: 'office',
|
|
|
|
|
|
6: 'farms',
|
|
|
|
|
|
7: 'shop',
|
|
|
|
|
|
8: 'warehouse'
|
|
|
|
|
|
};
|
2026-03-26 22:59:08 +00:00
|
|
|
|
const propType = buildingTypeMap[info.buildingType] ?? buildingTypeMap[item.type] ?? 'apartment';
|
2026-03-26 22:20:33 +00:00
|
|
|
|
|
2026-05-25 21:27:39 +03:00
|
|
|
|
const statusMap = { 0: 'available', 1: 'notAvailable', 2: 'booked' };
|
2026-03-26 22:59:08 +00:00
|
|
|
|
const status = statusMap[info.status] ?? statusMap[item.status] ?? 'available';
|
2026-03-26 22:20:33 +00:00
|
|
|
|
|
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-03-26 22:20:33 +00:00
|
|
|
|
const features = [];
|
2026-07-15 04:38:32 -07:00
|
|
|
|
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', 'حمامات')}`);
|
|
|
|
|
|
|
|
|
|
|
|
const apiBase = typeof window !== 'undefined' ? (process.env.NEXT_PUBLIC_API_URL || 'http://45.93.137.91/api') : '';
|
2026-03-30 01:01:42 +00:00
|
|
|
|
const rawImages = Array.isArray(info.images) ? info.images : [];
|
|
|
|
|
|
const images = rawImages.length > 0
|
2026-03-30 18:52:08 +00:00
|
|
|
|
? rawImages.map(img => img.startsWith('http') ? img : `${apiBase}${img.startsWith('/') ? '' : '/Pictures/'}${img}`)
|
2026-03-30 01:01:42 +00:00
|
|
|
|
: ['/property-placeholder.jpg'];
|
|
|
|
|
|
|
2026-03-26 22:20:33 +00:00
|
|
|
|
return {
|
|
|
|
|
|
id: item.id ?? index + 1,
|
2026-07-15 04:38:32 -07:00
|
|
|
|
title: info.address || `${t('property', 'عقار')} #${item.id || index + 1}`,
|
2026-03-26 22:20:33 +00:00
|
|
|
|
description: info.description || '',
|
|
|
|
|
|
type: propType,
|
|
|
|
|
|
price: dailyPrice,
|
|
|
|
|
|
priceUnit: 'daily',
|
|
|
|
|
|
location: {
|
2026-07-15 04:38:32 -07:00
|
|
|
|
city: cityMap[info.city] || extractCity(info.address) || 'damascus',
|
2026-03-26 22:20:33 +00:00
|
|
|
|
district: info.address || '',
|
|
|
|
|
|
},
|
2026-03-26 22:59:08 +00:00
|
|
|
|
bedrooms: info.numberOfBedRooms || 0,
|
2026-03-26 22:20:33 +00:00
|
|
|
|
bathrooms: info.numberOfBathRooms || 0,
|
|
|
|
|
|
area: info.space || 0,
|
|
|
|
|
|
features,
|
2026-03-30 01:01:42 +00:00
|
|
|
|
images,
|
2026-03-26 22:20:33 +00:00
|
|
|
|
status,
|
2026-05-26 20:24:37 +03:00
|
|
|
|
rating: item.rating || 0,
|
2026-03-26 22:20:33 +00:00
|
|
|
|
isNew: false,
|
|
|
|
|
|
_raw: item,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2026-02-15 01:53:37 +03:00
|
|
|
|
|
2026-03-26 22:20:33 +00:00
|
|
|
|
function extractCity(address) {
|
|
|
|
|
|
if (!address) return '';
|
2026-07-01 15:43:58 +03:00
|
|
|
|
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;
|
2026-03-26 22:20:33 +00:00
|
|
|
|
}
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-30 18:34:19 +00:00
|
|
|
|
const PropertyCard = ({ property, viewMode = 'grid', onLoginRequired }) => {
|
2026-07-01 15:43:58 +03:00
|
|
|
|
const { t, i18n } = useTranslation();
|
2026-03-30 17:54:42 +00:00
|
|
|
|
const { isFavorite: checkFavorite, addFavorite, removeFavorite } = useFavorites();
|
|
|
|
|
|
const [favLoading, setFavLoading] = useState(false);
|
2026-02-15 01:53:37 +03:00
|
|
|
|
const [currentImage, setCurrentImage] = useState(0);
|
2026-03-30 17:54:42 +00:00
|
|
|
|
const isFav = checkFavorite(property.id);
|
|
|
|
|
|
|
|
|
|
|
|
const toggleFavorite = async (e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation();
|
2026-03-30 18:34:19 +00:00
|
|
|
|
if (!AuthService.isAuthenticated()) { onLoginRequired?.(); return; }
|
2026-03-30 17:54:42 +00:00
|
|
|
|
setFavLoading(true);
|
|
|
|
|
|
if (isFav) {
|
|
|
|
|
|
await removeFavorite(property.id);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await addFavorite(property.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
setFavLoading(false);
|
2026-08-06 22:26:35 +03:00
|
|
|
|
|
2026-03-30 17:54:42 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-15 01:53:37 +03:00
|
|
|
|
const formatCurrency = (amount) => {
|
2026-07-15 04:38:32 -07:00
|
|
|
|
return amount?.toLocaleString() + ' ' + t('syp-symbol', 'ل.س');
|
2026-02-15 01:53:37 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const getPropertyTypeIcon = (type) => {
|
2026-03-26 22:20:33 +00:00
|
|
|
|
switch (type) {
|
2026-02-15 01:53:37 +03:00
|
|
|
|
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) => {
|
2026-03-26 22:20:33 +00:00
|
|
|
|
switch (type) {
|
2026-07-15 04:38:32 -07:00
|
|
|
|
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', 'مستودع');
|
2026-02-15 01:53:37 +03:00
|
|
|
|
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
|
2026-08-06 22:26:35 +03:00
|
|
|
|
loading='lazy'
|
2026-02-15 01:53:37 +03:00
|
|
|
|
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)}
|
2026-03-26 22:20:33 +00:00
|
|
|
|
className={`w-1.5 h-1.5 rounded-full transition-all ${idx === currentImage ? 'bg-gray-800 w-3' : 'bg-white/70'}`}
|
2026-02-15 01:53:37 +03:00
|
|
|
|
/>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<div className="absolute top-2 right-2 flex gap-2">
|
|
|
|
|
|
<button
|
2026-03-30 17:54:42 +00:00
|
|
|
|
onClick={toggleFavorite}
|
|
|
|
|
|
disabled={favLoading}
|
2026-02-15 01:53:37 +03:00
|
|
|
|
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"
|
|
|
|
|
|
>
|
2026-03-30 17:54:42 +00:00
|
|
|
|
<Heart className={`w-4 h-4 ${isFav ? 'fill-red-500 text-red-500' : 'text-gray-600'}`} />
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</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>
|
2026-03-26 22:20:33 +00:00
|
|
|
|
<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'}`}>
|
2026-07-15 04:38:32 -07:00
|
|
|
|
{property.status === 'available' ? t('available', 'متاح') : t('booked', 'محجوز')}
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</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" />
|
2026-07-15 04:38:32 -07:00
|
|
|
|
{t('city.' + property.location.city, property.location.city)}، {property.location.district}
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="text-left">
|
|
|
|
|
|
<div className="text-2xl font-bold text-gray-900">{formatCurrency(property.price)}</div>
|
2026-07-15 04:38:32 -07:00
|
|
|
|
<div className="text-xs text-gray-500">/{property.priceUnit === 'daily' ? t('day', 'يوم') : t('month', 'شهر')}</div>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</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" />
|
2026-07-15 04:38:32 -07:00
|
|
|
|
<span>{property.bedrooms} {t('rooms', 'غرف')}</span>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center gap-1 text-gray-600">
|
|
|
|
|
|
<Bath className="w-4 h-4" />
|
2026-07-15 04:38:32 -07:00
|
|
|
|
<span>{property.bathrooms} {t('bathrooms', 'حمامات')}</span>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center gap-1 text-gray-600">
|
|
|
|
|
|
<Square className="w-4 h-4" />
|
2026-07-15 04:38:32 -07:00
|
|
|
|
<span>{property.area} {t('sqm', 'م²')}</span>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-26 22:20:33 +00:00
|
|
|
|
<p className="text-gray-600 text-sm mb-4 line-clamp-2">{property.description}</p>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
|
|
|
|
|
|
<div className="flex gap-3">
|
|
|
|
|
|
<Link
|
|
|
|
|
|
href={`/property/${property.id}`}
|
|
|
|
|
|
className="flex-1 bg-gray-800 text-white py-3 rounded-xl font-medium hover:bg-gray-900 transition-colors text-center"
|
|
|
|
|
|
>
|
2026-07-15 04:38:32 -07:00
|
|
|
|
{t('viewDetails', 'عرض التفاصيل')}
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</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
|
2026-03-30 17:54:42 +00:00
|
|
|
|
onClick={toggleFavorite}
|
|
|
|
|
|
disabled={favLoading}
|
2026-02-15 01:53:37 +03:00
|
|
|
|
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"
|
|
|
|
|
|
>
|
2026-03-30 17:54:42 +00:00
|
|
|
|
<Heart className={`w-4 h-4 ${isFav ? 'fill-red-500 text-red-500' : 'text-gray-600'}`} />
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</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' && (
|
2026-07-15 04:38:32 -07:00
|
|
|
|
<span className="px-2 py-1 bg-gray-800 text-white rounded-lg text-xs font-medium">{t('available', 'متاح')}</span>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
)}
|
|
|
|
|
|
</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" />
|
2026-07-15 04:38:32 -07:00
|
|
|
|
<span className="line-clamp-1">{t('city.' + property.location.city, property.location.city)}، {property.location.district}</span>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="text-left">
|
|
|
|
|
|
<div className="text-xl font-bold text-gray-900">{formatCurrency(property.price)}</div>
|
2026-07-15 04:38:32 -07:00
|
|
|
|
<div className="text-xs text-gray-500">/{property.priceUnit === 'daily' ? t('day', 'يوم') : t('month', 'شهر')}</div>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</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" />
|
2026-07-15 04:38:32 -07:00
|
|
|
|
<span>{property.area}{t('sqm', 'م²')}</span>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</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
|
|
|
|
)}
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Link
|
|
|
|
|
|
href={`/property/${property.id}`}
|
|
|
|
|
|
className="block w-full bg-gray-800 text-white py-3 rounded-xl font-medium hover:bg-gray-900 transition-colors text-center"
|
|
|
|
|
|
>
|
2026-07-15 04:38:32 -07:00
|
|
|
|
{t('viewDetails', 'عرض التفاصيل')}
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</Link>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const FilterBar = ({ filters, onFilterChange }) => {
|
2026-07-15 04:38:32 -07:00
|
|
|
|
const { t } = useTranslation();
|
2026-02-15 01:53:37 +03:00
|
|
|
|
const [showFilters, setShowFilters] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
const propertyTypes = [
|
2026-07-15 04:38:32 -07:00
|
|
|
|
{ id: 'all', label: t('all', 'الكل') },
|
|
|
|
|
|
{ id: 'residential', label: t('propertyType.residential', 'سكني') },
|
|
|
|
|
|
{ id: 'commercial', label: t('propertyType.commercial', 'تجاري') },
|
|
|
|
|
|
{ id: 'agricultural', label: t('propertyType.agricultural', 'زراعي') },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
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 }
|
2026-02-15 01:53:37 +03:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const priceRanges = [
|
2026-07-15 04:38:32 -07:00
|
|
|
|
{ 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 مليون') }
|
2026-02-15 01:53:37 +03:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const cities = [
|
2026-07-15 04:38:32 -07:00
|
|
|
|
{ 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', 'ريف دمشق') }
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
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', 'أسهم عقارية') }
|
2026-02-15 01:53:37 +03:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
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"
|
2026-07-15 04:38:32 -07:00
|
|
|
|
placeholder={t('search-property-placeholder', 'ابحث عن عقار...')}
|
2026-02-15 01:53:37 +03:00
|
|
|
|
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" />
|
2026-07-15 04:38:32 -07:00
|
|
|
|
{t('advanced-filters', 'فلاتر متقدمة')}
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<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">
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<div>
|
2026-07-15 04:38:32 -07:00
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">{t('property-type', 'نوع الاستثمار')}</label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
value={filters.propertyType}
|
|
|
|
|
|
onChange={(e) => onFilterChange({ ...filters, propertyType: e.target.value })}
|
|
|
|
|
|
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"
|
|
|
|
|
|
>
|
|
|
|
|
|
{propertyTypes.map((type) => (
|
|
|
|
|
|
<option key={type.id} value={type.id}>{type.label}</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">{t('building-type', 'نوع البناء')}</label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
value={filters.buildingType}
|
|
|
|
|
|
onChange={(e) => onFilterChange({ ...filters, buildingType: e.target.value })}
|
|
|
|
|
|
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"
|
|
|
|
|
|
>
|
|
|
|
|
|
{buildingTypes.map((type) => (
|
|
|
|
|
|
<option key={type.id} value={type.id}>{type.label}</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
2026-07-15 04:38:32 -07:00
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">{t('city', 'المدينة')}</label>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<select
|
|
|
|
|
|
value={filters.city}
|
|
|
|
|
|
onChange={(e) => onFilterChange({ ...filters, city: e.target.value })}
|
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"
|
2026-02-15 01:53:37 +03:00
|
|
|
|
>
|
|
|
|
|
|
{cities.map((city) => (
|
|
|
|
|
|
<option key={city.id} value={city.id}>{city.label}</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
2026-07-15 04:38:32 -07:00
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">{t('price-range', 'مجال السعر')}</label>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<select
|
|
|
|
|
|
value={filters.priceRange}
|
|
|
|
|
|
onChange={(e) => onFilterChange({ ...filters, priceRange: e.target.value })}
|
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"
|
2026-02-15 01:53:37 +03:00
|
|
|
|
>
|
|
|
|
|
|
{priceRanges.map((range) => (
|
|
|
|
|
|
<option key={range.id} value={range.id}>{range.label}</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
2026-07-15 04:38:32 -07:00
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">{t('bedrooms', 'عدد الغرف')}</label>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<select
|
|
|
|
|
|
value={filters.bedrooms}
|
|
|
|
|
|
onChange={(e) => onFilterChange({ ...filters, bedrooms: e.target.value })}
|
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"
|
2026-02-15 01:53:37 +03:00
|
|
|
|
>
|
2026-07-15 04:38:32 -07:00
|
|
|
|
<option value="all">{t('all-numbers', 'الكل')}</option>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<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>
|
2026-07-15 04:38:32 -07:00
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">{t('bathrooms-label', 'عدد الحمامات')}</label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
value={filters.bathrooms}
|
|
|
|
|
|
onChange={(e) => onFilterChange({ ...filters, bathrooms: e.target.value })}
|
|
|
|
|
|
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"
|
|
|
|
|
|
>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">{t('certificate-label', 'الشهادة / السند العقاري')}</label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
value={filters.certificate}
|
|
|
|
|
|
onChange={(e) => onFilterChange({ ...filters, certificate: e.target.value })}
|
|
|
|
|
|
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"
|
|
|
|
|
|
>
|
|
|
|
|
|
{certificates.map((cert) => (
|
|
|
|
|
|
<option key={cert.id} value={cert.id}>{cert.label}</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">{t('area-sqm', 'المساحة (م²)')}</label>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="number"
|
2026-07-15 04:38:32 -07:00
|
|
|
|
placeholder={t('from', 'من')}
|
|
|
|
|
|
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"
|
2026-02-15 01:53:37 +03:00
|
|
|
|
value={filters.minArea}
|
|
|
|
|
|
onChange={(e) => onFilterChange({ ...filters, minArea: e.target.value })}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="number"
|
2026-07-15 04:38:32 -07:00
|
|
|
|
placeholder={t('to', 'إلى')}
|
|
|
|
|
|
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"
|
2026-02-15 01:53:37 +03:00
|
|
|
|
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',
|
2026-07-15 04:38:32 -07:00
|
|
|
|
buildingType: 'all',
|
2026-02-15 01:53:37 +03:00
|
|
|
|
city: 'all',
|
|
|
|
|
|
priceRange: 'all',
|
|
|
|
|
|
bedrooms: 'all',
|
2026-07-15 04:38:32 -07:00
|
|
|
|
bathrooms: 'all',
|
|
|
|
|
|
certificate: 'all',
|
2026-02-15 01:53:37 +03:00
|
|
|
|
minArea: '',
|
|
|
|
|
|
maxArea: '',
|
|
|
|
|
|
features: []
|
|
|
|
|
|
})}
|
2026-07-15 04:38:32 -07:00
|
|
|
|
className="px-6 py-2 bg-gray-100 rounded-xl font-medium hover:bg-gray-200 transition-colors text-gray-700 text-sm"
|
2026-02-15 01:53:37 +03:00
|
|
|
|
>
|
2026-07-15 04:38:32 -07:00
|
|
|
|
{t('reset', 'إعادة تعيين')}
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => setShowFilters(false)}
|
2026-07-15 04:38:32 -07:00
|
|
|
|
className="px-6 py-2 bg-gray-800 text-white rounded-xl font-medium hover:bg-gray-900 transition-colors text-sm"
|
2026-02-15 01:53:37 +03:00
|
|
|
|
>
|
2026-07-15 04:38:32 -07:00
|
|
|
|
{t('apply-filters', 'تطبيق')}
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</AnimatePresence>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default function PropertiesPage() {
|
2026-07-01 15:43:58 +03:00
|
|
|
|
const { t, i18n } = useTranslation();
|
2026-05-26 19:53:28 +03:00
|
|
|
|
const [purposeTab, setPurposeTab] = useState('rent');
|
2026-03-26 22:20:33 +00:00
|
|
|
|
const [viewMode, setViewMode] = useState('grid');
|
2026-02-15 01:53:37 +03:00
|
|
|
|
const [sortBy, setSortBy] = useState('newest');
|
2026-03-28 17:48:00 +00:00
|
|
|
|
const [properties, setProperties] = useState([]);
|
2026-03-26 22:20:33 +00:00
|
|
|
|
const [loading, setLoading] = useState(true);
|
2026-03-30 18:34:19 +00:00
|
|
|
|
const [showLoginDialog, setShowLoginDialog] = useState(false);
|
2026-02-15 01:53:37 +03:00
|
|
|
|
const [filters, setFilters] = useState({
|
|
|
|
|
|
search: '',
|
|
|
|
|
|
propertyType: 'all',
|
2026-07-15 04:38:32 -07:00
|
|
|
|
buildingType: 'all',
|
2026-02-15 01:53:37 +03:00
|
|
|
|
city: 'all',
|
|
|
|
|
|
priceRange: 'all',
|
|
|
|
|
|
bedrooms: 'all',
|
2026-07-15 04:38:32 -07:00
|
|
|
|
bathrooms: 'all',
|
|
|
|
|
|
certificate: 'all',
|
2026-02-15 01:53:37 +03:00
|
|
|
|
minArea: '',
|
|
|
|
|
|
maxArea: '',
|
|
|
|
|
|
features: []
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-26 22:20:33 +00:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
async function fetchProperties() {
|
2026-07-15 04:38:32 -07:00
|
|
|
|
setLoading(true);
|
2026-03-26 22:20:33 +00:00
|
|
|
|
try {
|
2026-07-15 04:38:32 -07:00
|
|
|
|
const queryParams = new URLSearchParams();
|
|
|
|
|
|
|
|
|
|
|
|
if (filters.bedrooms !== 'all') {
|
|
|
|
|
|
queryParams.append('numberOfBedRooms', filters.bedrooms);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (filters.bathrooms !== 'all') {
|
|
|
|
|
|
queryParams.append('numberOfBathRooms', filters.bathrooms);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (filters.minArea) {
|
|
|
|
|
|
queryParams.append('space', filters.minArea);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const buildingTypeReverseMap = {
|
|
|
|
|
|
'apartment': 'Apartment',
|
|
|
|
|
|
'villa': 'Villa',
|
|
|
|
|
|
'sweet': 'Sweet',
|
|
|
|
|
|
'room': 'Room',
|
|
|
|
|
|
'studio': 'Studio',
|
|
|
|
|
|
'office': 'Office',
|
|
|
|
|
|
'farms': 'Farms',
|
|
|
|
|
|
'shop': 'Shop',
|
|
|
|
|
|
'warehouse': 'Warehouse'
|
|
|
|
|
|
};
|
|
|
|
|
|
if (filters.buildingType !== 'all' && buildingTypeReverseMap[filters.buildingType] !== undefined) {
|
|
|
|
|
|
queryParams.append('buildingType', buildingTypeReverseMap[filters.buildingType]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const propertyTypeReverseMap = {
|
|
|
|
|
|
'residential': 'Residential',
|
|
|
|
|
|
'commercial': 'Commercial',
|
|
|
|
|
|
'agricultural': 'Agricultural'
|
|
|
|
|
|
};
|
|
|
|
|
|
if (filters.propertyType !== 'all' && propertyTypeReverseMap[filters.propertyType] !== undefined) {
|
|
|
|
|
|
queryParams.append('propertyType', propertyTypeReverseMap[filters.propertyType]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const certificateReverseMap = {
|
|
|
|
|
|
'realEstateTitle': 'RealEstateTitle',
|
|
|
|
|
|
'courtRuling': 'CourtRuling',
|
|
|
|
|
|
'notary': 'Notary',
|
|
|
|
|
|
'shares': 'Shares'
|
|
|
|
|
|
};
|
|
|
|
|
|
if (filters.certificate !== 'all' && certificateReverseMap[filters.certificate] !== undefined) {
|
|
|
|
|
|
queryParams.append('certificate', certificateReverseMap[filters.certificate]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (filters.city !== 'all') {
|
|
|
|
|
|
const cityFormatted = filters.city.charAt(0).toUpperCase() + filters.city.slice(1);
|
|
|
|
|
|
queryParams.append('city', cityFormatted);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://45.93.137.91/api';
|
|
|
|
|
|
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 })),
|
2026-03-26 22:20:33 +00:00
|
|
|
|
getSaleProperties().catch(() => []),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
2026-07-15 04:38:32 -07:00
|
|
|
|
const rentList = rentRes.isSuccess && Array.isArray(rentRes.data) ? rentRes.data : [];
|
2026-03-26 22:20:33 +00:00
|
|
|
|
const saleList = Array.isArray(saleData) ? saleData : [];
|
|
|
|
|
|
|
|
|
|
|
|
const mapped = [
|
2026-07-01 15:43:58 +03:00
|
|
|
|
...rentList.map((p, i) => ({ ...mapApiProperty(t, p, i), purpose: 'rent' })),
|
|
|
|
|
|
...saleList.map((p, i) => ({ ...mapApiProperty(t, p, rentList.length + i), purpose: 'sale' })),
|
2026-03-26 22:20:33 +00:00
|
|
|
|
];
|
|
|
|
|
|
|
2026-07-15 04:38:32 -07:00
|
|
|
|
setProperties(mapped);
|
2026-03-26 22:20:33 +00:00
|
|
|
|
} catch (err) {
|
2026-03-28 17:48:00 +00:00
|
|
|
|
console.error('[Properties] Failed to fetch properties:', err);
|
2026-03-26 22:20:33 +00:00
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
2026-02-15 01:53:37 +03:00
|
|
|
|
}
|
2026-03-26 22:20:33 +00:00
|
|
|
|
|
|
|
|
|
|
fetchProperties();
|
2026-07-15 04:38:32 -07:00
|
|
|
|
}, [filters, t]);
|
2026-02-15 01:53:37 +03:00
|
|
|
|
|
|
|
|
|
|
const filteredProperties = properties
|
2026-05-26 19:53:28 +03:00
|
|
|
|
.filter(p => p.purpose === purposeTab)
|
2026-02-15 01:53:37 +03:00
|
|
|
|
.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('+')) {
|
2026-03-26 22:20:33 +00:00
|
|
|
|
const minVal = parseInt(filters.priceRange.replace('+', ''));
|
|
|
|
|
|
if (property.price < minVal) return false;
|
2026-02-15 01:53:37 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
})
|
|
|
|
|
|
.sort((a, b) => {
|
2026-03-26 22:20:33 +00:00
|
|
|
|
switch (sortBy) {
|
2026-02-15 01:53:37 +03:00
|
|
|
|
case 'price_asc': return a.price - b.price;
|
|
|
|
|
|
case 'price_desc': return b.price - a.price;
|
|
|
|
|
|
case 'rating': return b.rating - a.rating;
|
2026-03-26 22:20:33 +00:00
|
|
|
|
default: return 0;
|
2026-02-15 01:53:37 +03:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-07-01 15:43:58 +03:00
|
|
|
|
<div dir={i18n.language === 'ar' ? 'rtl' : 'ltr'} className="min-h-screen bg-gray-50 py-8">
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<div className="container mx-auto px-4">
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
initial={{ opacity: 0, y: -20 }}
|
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
|
className="text-center mb-8"
|
|
|
|
|
|
>
|
2026-07-15 04:38:32 -07:00
|
|
|
|
<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'}`}>
|
2026-07-15 04:38:32 -07:00
|
|
|
|
{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'}`}>
|
2026-07-15 04:38:32 -07:00
|
|
|
|
{t('for-sale', 'للبيع')}
|
2026-05-26 19:53:28 +03:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-26 22:20:33 +00:00
|
|
|
|
{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>
|
|
|
|
|
|
)}
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</motion.div>
|
|
|
|
|
|
|
|
|
|
|
|
<FilterBar filters={filters} onFilterChange={setFilters} />
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex justify-between items-center my-6">
|
|
|
|
|
|
<div className="text-gray-600">
|
2026-07-15 04:38:32 -07:00
|
|
|
|
<span className="font-bold text-gray-900">{filteredProperties.length}</span> {t('property-available', 'عقار متاح')}
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex gap-3">
|
|
|
|
|
|
<select
|
|
|
|
|
|
value={sortBy}
|
|
|
|
|
|
onChange={(e) => setSortBy(e.target.value)}
|
2026-07-15 04:38:32 -07:00
|
|
|
|
className="px-4 py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300 text-gray-700 text-sm"
|
2026-02-15 01:53:37 +03:00
|
|
|
|
>
|
2026-07-15 04:38:32 -07:00
|
|
|
|
<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>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</select>
|
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => setViewMode('grid')}
|
2026-03-26 22:20:33 +00:00
|
|
|
|
className={`p-2 rounded-xl transition-colors ${viewMode === 'grid' ? 'bg-gray-800 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}`}
|
2026-02-15 01:53:37 +03:00
|
|
|
|
>
|
|
|
|
|
|
<Grid3x3 className="w-5 h-5" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => setViewMode('list')}
|
2026-03-26 22:20:33 +00:00
|
|
|
|
className={`p-2 rounded-xl transition-colors ${viewMode === 'list' ? 'bg-gray-800 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}`}
|
2026-02-15 01:53:37 +03:00
|
|
|
|
>
|
|
|
|
|
|
<List className="w-5 h-5" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-26 22:20:33 +00:00
|
|
|
|
<div className={viewMode === 'grid'
|
2026-02-15 01:53:37 +03:00
|
|
|
|
? 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6'
|
|
|
|
|
|
: 'space-y-4'
|
|
|
|
|
|
}>
|
|
|
|
|
|
{filteredProperties.map((property) => (
|
2026-03-30 18:34:19 +00:00
|
|
|
|
<PropertyCard key={property.id} property={property} viewMode={viewMode} onLoginRequired={() => setShowLoginDialog(true)} />
|
2026-02-15 01:53:37 +03:00
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-15 04:38:32 -07:00
|
|
|
|
{!loading && filteredProperties.length === 0 && (
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<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>
|
2026-07-15 04:38:32 -07:00
|
|
|
|
<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>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</motion.div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2026-03-30 17:54:42 +00:00
|
|
|
|
<Toaster position="top-center" />
|
2026-03-30 18:34:19 +00:00
|
|
|
|
{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>
|
2026-07-15 04:38:32 -07:00
|
|
|
|
<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>
|
2026-03-30 18:34:19 +00:00
|
|
|
|
<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"
|
|
|
|
|
|
>
|
2026-07-15 04:38:32 -07:00
|
|
|
|
{t('cancel', 'إلغاء')}
|
2026-03-30 18:34:19 +00:00
|
|
|
|
</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"
|
|
|
|
|
|
>
|
2026-07-15 04:38:32 -07:00
|
|
|
|
{t('login', 'دخول')}
|
2026-03-30 18:34:19 +00:00
|
|
|
|
</Link>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
2026-07-15 04:38:32 -07:00
|
|
|
|
}
|