Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 1m18s

This commit is contained in:
Rahaf
2026-07-15 19:25:49 +03:00

View File

@ -8,26 +8,16 @@ import {
Bed, Bed,
Bath, Bath,
Square, Square,
DollarSign,
Filter, Filter,
Grid3x3, Grid3x3,
List, List,
Heart, Heart,
Share2,
ChevronDown, ChevronDown,
Star, Star,
Camera,
Home, Home,
Building2, Building2,
Trees,
Waves,
Warehouse,
Sparkles,
Shield,
Calendar,
Phone, Phone,
Mail, FileText
MessageCircle
} from 'lucide-react'; } from 'lucide-react';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
@ -37,28 +27,51 @@ import AuthService from '@/app/services/AuthService';
import toast, { Toaster } from 'react-hot-toast'; import toast, { Toaster } from 'react-hot-toast';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
// Map API data to UI format
function mapApiProperty(t, item, index) { function mapApiProperty(t, item, index) {
const info = item.propertyInformation || {}; const info = item.propertyInformation || {};
const dailyPrice = item.dailyRent ?? item.monthlyRent ?? item.price ?? 0; const dailyPrice = item.dailyRent ?? item.monthlyRent ?? item.price ?? 0;
const monthlyPrice = item.monthlyRent ?? 0;
const buildingTypeMap = { 0: 'apartment', 1: 'villa', 2: 'sweet', 3: 'room', 4: 'studio', 5: 'office', 6: 'farms', 7: 'shop', 8: 'warehouse' }; const buildingTypeMap = {
0: 'apartment',
1: 'villa',
2: 'sweet',
3: 'room',
4: 'studio',
5: 'office',
6: 'farms',
7: 'shop',
8: 'warehouse'
};
const propType = buildingTypeMap[info.buildingType] ?? buildingTypeMap[item.type] ?? 'apartment'; const propType = buildingTypeMap[info.buildingType] ?? buildingTypeMap[item.type] ?? 'apartment';
const statusMap = { 0: 'available', 1: 'notAvailable', 2: 'booked' }; const statusMap = { 0: 'available', 1: 'notAvailable', 2: 'booked' };
const status = statusMap[info.status] ?? statusMap[item.status] ?? 'available'; const status = statusMap[info.status] ?? statusMap[item.status] ?? 'available';
const features = []; const cityMap = {
if (item.isSmokeAllow) features.push(t('smoke-allowed')); 0: 'damascus',
if (item.isVisitorAllow) features.push(t('visitors-allowed')); 1: 'aleppo',
if (item.specializedFor) features.push(t('specialized')); 2: 'homs',
if (info.numberOfBedRooms) features.push(`${info.numberOfBedRooms} ${t('bedrooms')}`); 3: 'latakia',
if (info.numberOfBathRooms) features.push(`${info.numberOfBathRooms} ${t('bathrooms')}`); 4: 'daraa',
5: 'tartous',
6: 'suweida',
7: 'deirEzzor',
8: 'raqqa',
9: 'idlib',
10: 'hasakah',
11: 'qamishli',
12: 'ruralDamascus'
};
// Extract images from API and build full URLs const features = [];
const apiBase = typeof window !== 'undefined' ? (process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api') : ''; 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') : '';
const rawImages = Array.isArray(info.images) ? info.images : []; const rawImages = Array.isArray(info.images) ? info.images : [];
const images = rawImages.length > 0 const images = rawImages.length > 0
? rawImages.map(img => img.startsWith('http') ? img : `${apiBase}${img.startsWith('/') ? '' : '/Pictures/'}${img}`) ? rawImages.map(img => img.startsWith('http') ? img : `${apiBase}${img.startsWith('/') ? '' : '/Pictures/'}${img}`)
@ -66,13 +79,13 @@ function mapApiProperty(t, item, index) {
return { return {
id: item.id ?? index + 1, id: item.id ?? index + 1,
title: info.address || `${t('property')} #${item.id || index + 1}`, title: info.address || `${t('property', 'عقار')} #${item.id || index + 1}`,
description: info.description || '', description: info.description || '',
type: propType, type: propType,
price: dailyPrice, price: dailyPrice,
priceUnit: 'daily', priceUnit: 'daily',
location: { location: {
city: extractCity(info.address) || 'damascus', city: cityMap[info.city] || extractCity(info.address) || 'damascus',
district: info.address || '', district: info.address || '',
}, },
bedrooms: info.numberOfBedRooms || 0, bedrooms: info.numberOfBedRooms || 0,
@ -110,8 +123,6 @@ function extractCity(address) {
return ''; return '';
} }
// API-only — no fallback data
const PropertyCard = ({ property, viewMode = 'grid', onLoginRequired }) => { const PropertyCard = ({ property, viewMode = 'grid', onLoginRequired }) => {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const { isFavorite: checkFavorite, addFavorite, removeFavorite } = useFavorites(); const { isFavorite: checkFavorite, addFavorite, removeFavorite } = useFavorites();
@ -134,7 +145,7 @@ const PropertyCard = ({ property, viewMode = 'grid', onLoginRequired }) => {
}; };
const formatCurrency = (amount) => { const formatCurrency = (amount) => {
return amount?.toLocaleString() + ' ' + t('syp-symbol'); return amount?.toLocaleString() + ' ' + t('syp-symbol', 'ل.س');
}; };
const getPropertyTypeIcon = (type) => { const getPropertyTypeIcon = (type) => {
@ -149,10 +160,16 @@ const PropertyCard = ({ property, viewMode = 'grid', onLoginRequired }) => {
const getPropertyTypeLabel = (type) => { const getPropertyTypeLabel = (type) => {
switch (type) { switch (type) {
case 'villa': return t('buildingType.villa'); case 'villa': return t('buildingType.villa', 'فيلا');
case 'apartment': return t('buildingType.apartment'); case 'apartment': return t('buildingType.apartment', 'شقة');
case 'house': return t('house'); case 'house': return t('house', 'منزل');
case 'studio': return t('buildingType.studio'); 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; default: return type;
} }
}; };
@ -203,33 +220,33 @@ const PropertyCard = ({ property, viewMode = 'grid', onLoginRequired }) => {
{getPropertyTypeLabel(property.type)} {getPropertyTypeLabel(property.type)}
</span> </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'}`}> <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')} {property.status === 'available' ? t('available', 'متاح') : t('booked', 'محجوز')}
</span> </span>
</div> </div>
<h3 className="text-xl font-bold text-gray-900 mb-1">{property.title}</h3> <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"> <div className="flex items-center gap-1 text-gray-500 text-sm mb-3">
<MapPin className="w-4 h-4" /> <MapPin className="w-4 h-4" />
{t('city.' + property.location.city)}، {property.location.district} {t('city.' + property.location.city, property.location.city)}، {property.location.district}
</div> </div>
</div> </div>
<div className="text-left"> <div className="text-left">
<div className="text-2xl font-bold text-gray-900">{formatCurrency(property.price)}</div> <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 className="text-xs text-gray-500">/{property.priceUnit === 'daily' ? t('day', 'يوم') : t('month', 'شهر')}</div>
</div> </div>
</div> </div>
<div className="flex flex-wrap gap-4 mb-4"> <div className="flex flex-wrap gap-4 mb-4">
<div className="flex items-center gap-1 text-gray-600"> <div className="flex items-center gap-1 text-gray-600">
<Bed className="w-4 h-4" /> <Bed className="w-4 h-4" />
<span>{property.bedrooms} {t('rooms')}</span> <span>{property.bedrooms} {t('rooms', 'غرف')}</span>
</div> </div>
<div className="flex items-center gap-1 text-gray-600"> <div className="flex items-center gap-1 text-gray-600">
<Bath className="w-4 h-4" /> <Bath className="w-4 h-4" />
<span>{property.bathrooms} {t('bathrooms')}</span> <span>{property.bathrooms} {t('bathrooms', 'حمامات')}</span>
</div> </div>
<div className="flex items-center gap-1 text-gray-600"> <div className="flex items-center gap-1 text-gray-600">
<Square className="w-4 h-4" /> <Square className="w-4 h-4" />
<span>{property.area} {t('sqm')}</span> <span>{property.area} {t('sqm', 'م²')}</span>
</div> </div>
</div> </div>
@ -240,7 +257,7 @@ const PropertyCard = ({ property, viewMode = 'grid', onLoginRequired }) => {
href={`/property/${property.id}`} 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" className="flex-1 bg-gray-800 text-white py-3 rounded-xl font-medium hover:bg-gray-900 transition-colors text-center"
> >
{t('viewDetails')} {t('viewDetails', 'عرض التفاصيل')}
</Link> </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"> <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" /> <Phone className="w-4 h-4" />
@ -285,18 +302,18 @@ const PropertyCard = ({ property, viewMode = 'grid', onLoginRequired }) => {
{getPropertyTypeLabel(property.type)} {getPropertyTypeLabel(property.type)}
</span> </span>
{property.status === 'available' && ( {property.status === 'available' && (
<span className="px-2 py-1 bg-gray-800 text-white rounded-lg text-xs font-medium">{t('available')}</span> <span className="px-2 py-1 bg-gray-800 text-white rounded-lg text-xs font-medium">{t('available', 'متاح')}</span>
)} )}
</div> </div>
<h3 className="font-bold text-gray-900 mb-1 line-clamp-1">{property.title}</h3> <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"> <div className="flex items-center gap-1 text-gray-500 text-xs mb-2">
<MapPin className="w-3 h-3" /> <MapPin className="w-3 h-3" />
<span className="line-clamp-1">{t('city.' + property.location.city)}، {property.location.district}</span> <span className="line-clamp-1">{t('city.' + property.location.city, property.location.city)}، {property.location.district}</span>
</div> </div>
</div> </div>
<div className="text-left"> <div className="text-left">
<div className="text-xl font-bold text-gray-900">{formatCurrency(property.price)}</div> <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 className="text-xs text-gray-500">/{property.priceUnit === 'daily' ? t('day', 'يوم') : t('month', 'شهر')}</div>
</div> </div>
</div> </div>
@ -312,14 +329,14 @@ const PropertyCard = ({ property, viewMode = 'grid', onLoginRequired }) => {
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Square className="w-4 h-4" /> <Square className="w-4 h-4" />
<span>{property.area}{t('sqm')}</span> <span>{property.area}{t('sqm', 'م²')}</span>
</div> </div>
</div> </div>
{property.rating > 0 && ( {property.rating > 0 && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Star className="w-4 h-4 fill-amber-500 text-amber-500" /> <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> <span className="text-sm font-medium text-gray-700">{property.rating.toFixed(1)}</span>
</div> </div>
)} )}
</div> </div>
@ -327,7 +344,7 @@ const PropertyCard = ({ property, viewMode = 'grid', onLoginRequired }) => {
href={`/property/${property.id}`} 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" 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')} {t('viewDetails', 'عرض التفاصيل')}
</Link> </Link>
</div> </div>
</motion.div> </motion.div>
@ -335,40 +352,61 @@ const PropertyCard = ({ property, viewMode = 'grid', onLoginRequired }) => {
}; };
const FilterBar = ({ filters, onFilterChange }) => { const FilterBar = ({ filters, onFilterChange }) => {
const { t, i18n } = useTranslation(); const { t } = useTranslation();
const [showFilters, setShowFilters] = useState(false); const [showFilters, setShowFilters] = useState(false);
const propertyTypes = [ const propertyTypes = [
{ id: 'all', label: t('all') }, { id: 'all', label: t('all', 'الكل') },
{ id: 'apartment', label: t('buildingType.apartment'), icon: Building2 }, { id: 'residential', label: t('propertyType.residential', 'سكني') },
{ id: 'villa', label: t('buildingType.villa'), icon: Home }, { id: 'commercial', label: t('propertyType.commercial', 'تجاري') },
{ id: 'house', label: t('house'), icon: Home }, { 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 }
]; ];
const priceRanges = [ const priceRanges = [
{ id: 'all', label: t('allPrices') }, { id: 'all', label: t('allPrices', 'جميع الأسعار') },
{ id: '0-500000', label: t('price-range-less-than-500k') }, { id: '0-500000', label: t('price-range-less-than-500k', 'أقل من 500 ألف') },
{ id: '500000-1000000', label: t('price-range-500k-to-1m') }, { id: '500000-1000000', label: t('price-range-500k-to-1m', '500 ألف - 1 مليون') },
{ id: '1000000-2000000', label: t('price-range-1m-to-2m') }, { id: '1000000-2000000', label: t('price-range-1m-to-2m', '1 مليون - 2 مليون') },
{ id: '2000000-5000000', label: t('price-range-2m-to-5m') }, { id: '2000000-5000000', label: t('price-range-2m-to-5m', '2 مليون - 5 مليون') },
{ id: '5000000+', label: t('price-range-more-than-5m') } { id: '5000000+', label: t('price-range-more-than-5m', 'أكثر من 5 مليون') }
]; ];
const cities = [ const cities = [
{ id: 'all', label: t('allCities') }, { id: 'all', label: t('allCities', 'جميع المدن') },
{ id: 'damascus', label: t('city.damascus') }, { id: 'damascus', label: t('city.damascus', 'دمشق') },
{ id: 'aleppo', label: t('city.aleppo') }, { id: 'aleppo', label: t('city.aleppo', 'حلب') },
{ id: 'homs', label: t('city.homs') }, { id: 'homs', label: t('city.homs', 'حمص') },
{ id: 'latakia', label: t('city.latakia') }, { id: 'latakia', label: t('city.latakia', 'اللاذقية') },
{ id: 'daraa', label: t('city.daraa') }, { id: 'daraa', label: t('city.daraa', 'درعا') },
{ id: 'tartous', label: t('city.tartous') }, { id: 'tartous', label: t('city.tartous', 'طرطوس') },
{ id: 'suweida', label: t('city.suweida') }, { id: 'suweida', label: t('city.suweida', 'السويداء') },
{ id: 'deirEzzor', label: t('city.deirEzzor') }, { id: 'deirEzzor', label: t('city.deirEzzor', 'دير الزور') },
{ id: 'raqqa', label: t('city.raqqa') }, { id: 'raqqa', label: t('city.raqqa', 'الرقة') },
{ id: 'idlib', label: t('city.idlib') }, { id: 'idlib', label: t('city.idlib', 'إدلب') },
{ id: 'hasakah', label: t('city.hasakah') }, { id: 'hasakah', label: t('city.hasakah', 'الحسكة') },
{ id: 'qamishli', label: t('city.qamishli') }, { id: 'qamishli', label: t('city.qamishli', 'القامشلي') },
{ id: 'ruralDamascus', label: t('city.ruralDamascus') } { 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', 'أسهم عقارية') }
]; ];
return ( return (
@ -378,7 +416,7 @@ const FilterBar = ({ filters, onFilterChange }) => {
<Search className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" /> <Search className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
<input <input
type="text" type="text"
placeholder={t('search-property-placeholder')} 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" 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} value={filters.search}
onChange={(e) => onFilterChange({ ...filters, search: e.target.value })} onChange={(e) => onFilterChange({ ...filters, search: e.target.value })}
@ -389,7 +427,7 @@ const FilterBar = ({ filters, onFilterChange }) => {
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" 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" /> <Filter className="w-5 h-5" />
{t('advanced-filters')} {t('advanced-filters', 'فلاتر متقدمة')}
<ChevronDown className={`w-4 h-4 transition-transform ${showFilters ? 'rotate-180' : ''}`} /> <ChevronDown className={`w-4 h-4 transition-transform ${showFilters ? 'rotate-180' : ''}`} />
</button> </button>
</div> </div>
@ -402,32 +440,39 @@ const FilterBar = ({ filters, onFilterChange }) => {
exit={{ height: 0, opacity: 0 }} exit={{ height: 0, opacity: 0 }}
className="overflow-hidden" className="overflow-hidden"
> >
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 pt-4 border-t border-gray-100"> <div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 pt-4 border-t border-gray-100">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('property-type')}</label> <label className="block text-sm font-medium text-gray-700 mb-2">{t('property-type', 'نوع الاستثمار')}</label>
<div className="flex flex-wrap gap-2"> <select
{propertyTypes.map((type) => { value={filters.propertyType}
const Icon = type.icon; onChange={(e) => onFilterChange({ ...filters, propertyType: e.target.value })}
return ( 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"
<button >
key={type.id} {propertyTypes.map((type) => (
onClick={() => onFilterChange({ ...filters, propertyType: type.id })} <option key={type.id} value={type.id}>{type.label}</option>
className={`px-3 py-2 rounded-xl text-sm font-medium transition-all flex items-center gap-1 ${filters.propertyType === type.id ? 'bg-gray-800 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}`} ))}
> </select>
{Icon && <Icon className="w-4 h-4" />}
{type.label}
</button>
);
})}
</div>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('city')}</label> <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>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('city', 'المدينة')}</label>
<select <select
value={filters.city} value={filters.city}
onChange={(e) => onFilterChange({ ...filters, city: e.target.value })} onChange={(e) => onFilterChange({ ...filters, city: 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" 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"
> >
{cities.map((city) => ( {cities.map((city) => (
<option key={city.id} value={city.id}>{city.label}</option> <option key={city.id} value={city.id}>{city.label}</option>
@ -436,11 +481,11 @@ const FilterBar = ({ filters, onFilterChange }) => {
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('price-range')}</label> <label className="block text-sm font-medium text-gray-700 mb-2">{t('price-range', 'مجال السعر')}</label>
<select <select
value={filters.priceRange} value={filters.priceRange}
onChange={(e) => onFilterChange({ ...filters, priceRange: e.target.value })} onChange={(e) => onFilterChange({ ...filters, priceRange: 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" 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"
> >
{priceRanges.map((range) => ( {priceRanges.map((range) => (
<option key={range.id} value={range.id}>{range.label}</option> <option key={range.id} value={range.id}>{range.label}</option>
@ -449,13 +494,13 @@ const FilterBar = ({ filters, onFilterChange }) => {
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('bedrooms')}</label> <label className="block text-sm font-medium text-gray-700 mb-2">{t('bedrooms', 'عدد الغرف')}</label>
<select <select
value={filters.bedrooms} value={filters.bedrooms}
onChange={(e) => onFilterChange({ ...filters, bedrooms: e.target.value })} onChange={(e) => onFilterChange({ ...filters, bedrooms: 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" 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="all">{t('all-numbers', 'الكل')}</option>
<option value="1">1+</option> <option value="1">1+</option>
<option value="2">2+</option> <option value="2">2+</option>
<option value="3">3+</option> <option value="3">3+</option>
@ -465,19 +510,47 @@ const FilterBar = ({ filters, onFilterChange }) => {
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2">{t('area-sqm')}</label> <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>
<div className="flex gap-2"> <div className="flex gap-2">
<input <input
type="number" type="number"
placeholder={t('from')} 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" 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} value={filters.minArea}
onChange={(e) => onFilterChange({ ...filters, minArea: e.target.value })} onChange={(e) => onFilterChange({ ...filters, minArea: e.target.value })}
/> />
<input <input
type="number" type="number"
placeholder={t('to')} 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" 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} value={filters.maxArea}
onChange={(e) => onFilterChange({ ...filters, maxArea: e.target.value })} onChange={(e) => onFilterChange({ ...filters, maxArea: e.target.value })}
/> />
@ -490,22 +563,25 @@ const FilterBar = ({ filters, onFilterChange }) => {
onClick={() => onFilterChange({ onClick={() => onFilterChange({
search: '', search: '',
propertyType: 'all', propertyType: 'all',
buildingType: 'all',
city: 'all', city: 'all',
priceRange: 'all', priceRange: 'all',
bedrooms: 'all', bedrooms: 'all',
bathrooms: 'all',
certificate: 'all',
minArea: '', minArea: '',
maxArea: '', maxArea: '',
features: [] features: []
})} })}
className="px-6 py-2 bg-gray-100 rounded-xl font-medium hover:bg-gray-200 transition-colors text-gray-700" className="px-6 py-2 bg-gray-100 rounded-xl font-medium hover:bg-gray-200 transition-colors text-gray-700 text-sm"
> >
{t('reset')} {t('reset', 'إعادة تعيين')}
</button> </button>
<button <button
onClick={() => setShowFilters(false)} onClick={() => setShowFilters(false)}
className="px-6 py-2 bg-gray-800 text-white rounded-xl font-medium hover:bg-gray-900 transition-colors" 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')} {t('apply-filters', 'تطبيق')}
</button> </button>
</div> </div>
</motion.div> </motion.div>
@ -526,9 +602,12 @@ export default function PropertiesPage() {
const [filters, setFilters] = useState({ const [filters, setFilters] = useState({
search: '', search: '',
propertyType: 'all', propertyType: 'all',
buildingType: 'all',
city: 'all', city: 'all',
priceRange: 'all', priceRange: 'all',
bedrooms: 'all', bedrooms: 'all',
bathrooms: 'all',
certificate: 'all',
minArea: '', minArea: '',
maxArea: '', maxArea: '',
features: [] features: []
@ -536,13 +615,74 @@ export default function PropertiesPage() {
useEffect(() => { useEffect(() => {
async function fetchProperties() { async function fetchProperties() {
setLoading(true);
try { try {
const [rentData, saleData] = await Promise.all([ const queryParams = new URLSearchParams();
getRentProperties().catch(() => []),
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 })),
getSaleProperties().catch(() => []), getSaleProperties().catch(() => []),
]); ]);
const rentList = Array.isArray(rentData) ? rentData : []; const rentList = rentRes.isSuccess && Array.isArray(rentRes.data) ? rentRes.data : [];
const saleList = Array.isArray(saleData) ? saleData : []; const saleList = Array.isArray(saleData) ? saleData : [];
const mapped = [ const mapped = [
@ -550,9 +690,7 @@ export default function PropertiesPage() {
...saleList.map((p, i) => ({ ...mapApiProperty(t, p, rentList.length + i), purpose: 'sale' })), ...saleList.map((p, i) => ({ ...mapApiProperty(t, p, rentList.length + i), purpose: 'sale' })),
]; ];
if (mapped.length > 0) { setProperties(mapped);
setProperties(mapped);
}
} catch (err) { } catch (err) {
console.error('[Properties] Failed to fetch properties:', err); console.error('[Properties] Failed to fetch properties:', err);
} finally { } finally {
@ -561,7 +699,7 @@ export default function PropertiesPage() {
} }
fetchProperties(); fetchProperties();
}, []); }, [filters, t]);
const filteredProperties = properties const filteredProperties = properties
.filter(p => p.purpose === purposeTab) .filter(p => p.purpose === purposeTab)
@ -569,12 +707,6 @@ export default function PropertiesPage() {
if (filters.search && !property.title.includes(filters.search) && !property.description.includes(filters.search)) { if (filters.search && !property.title.includes(filters.search) && !property.description.includes(filters.search)) {
return false; return false;
} }
if (filters.propertyType !== 'all' && property.type !== filters.propertyType) {
return false;
}
if (filters.city !== 'all' && property.location.city !== filters.city) {
return false;
}
if (filters.priceRange !== 'all') { if (filters.priceRange !== 'all') {
const [min, max] = filters.priceRange.split('-'); const [min, max] = filters.priceRange.split('-');
if (max) { if (max) {
@ -584,11 +716,6 @@ export default function PropertiesPage() {
if (property.price < minVal) return false; if (property.price < minVal) return false;
} }
} }
if (filters.bedrooms !== 'all' && property.bedrooms < parseInt(filters.bedrooms)) {
return false;
}
if (filters.minArea && property.area < parseInt(filters.minArea)) return false;
if (filters.maxArea && property.area > parseInt(filters.maxArea)) return false;
return true; return true;
}) })
.sort((a, b) => { .sort((a, b) => {
@ -608,19 +735,18 @@ export default function PropertiesPage() {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
className="text-center mb-8" 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> <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> <p className="text-gray-500">{t('best-properties-in-syria', 'أفضل العقارات المتاحة في سوريا مباشرة وبسهولة')}</p>
{/* Purpose Toggle */}
<div className="flex justify-center mt-4"> <div className="flex justify-center mt-4">
<div className="inline-flex bg-gray-100 rounded-xl p-1"> <div className="inline-flex bg-gray-100 rounded-xl p-1">
<button onClick={() => setPurposeTab('rent')} <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'}`}> 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')} {t('for-rent', 'للإيجار')}
</button> </button>
<button onClick={() => setPurposeTab('sale')} <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'}`}> 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')} {t('for-sale', 'للبيع')}
</button> </button>
</div> </div>
</div> </div>
@ -635,18 +761,18 @@ export default function PropertiesPage() {
<div className="flex justify-between items-center my-6"> <div className="flex justify-between items-center my-6">
<div className="text-gray-600"> <div className="text-gray-600">
<span className="font-bold text-gray-900">{filteredProperties.length}</span> {t('property-available')} <span className="font-bold text-gray-900">{filteredProperties.length}</span> {t('property-available', 'عقار متاح')}
</div> </div>
<div className="flex gap-3"> <div className="flex gap-3">
<select <select
value={sortBy} value={sortBy}
onChange={(e) => setSortBy(e.target.value)} onChange={(e) => setSortBy(e.target.value)}
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" 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"
> >
<option value="newest">{t('newest')}</option> <option value="newest">{t('newest', 'الأحدث')}</option>
<option value="price_asc">{t('price-low-to-high')}</option> <option value="price_asc">{t('price-low-to-high', 'السعر من الأقل للأعلى')}</option>
<option value="price_desc">{t('price-high-to-low')}</option> <option value="price_desc">{t('price-high-to-low', 'السعر من الأعلى للأقل')}</option>
<option value="rating">{t('rating')}</option> <option value="rating">{t('rating', 'التقييم')}</option>
</select> </select>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
@ -674,7 +800,7 @@ export default function PropertiesPage() {
))} ))}
</div> </div>
{filteredProperties.length === 0 && ( {!loading && filteredProperties.length === 0 && (
<motion.div <motion.div
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
@ -683,8 +809,8 @@ export default function PropertiesPage() {
<div className="w-24 h-24 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4"> <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" /> <Home className="w-12 h-12 text-gray-400" />
</div> </div>
<h3 className="text-xl font-bold text-gray-700 mb-2">{t('no-properties')}</h3> <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> <p className="text-gray-500">{t('try-changing-search-criteria', 'يرجى تجربة تعديل خيارات الفلترة والبحث')}</p>
</motion.div> </motion.div>
)} )}
</div> </div>
@ -700,20 +826,20 @@ export default function PropertiesPage() {
<div className="w-14 h-14 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-4"> <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" /> <Heart className="w-7 h-7 text-amber-600" />
</div> </div>
<h3 className="text-xl font-bold text-gray-900 mb-2">{t('login-required-title')}</h3> <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> <p className="text-gray-500 mb-6">{t('login-required-favorites-desc', 'يجب تسجيل الدخول لتتمكن من إضافة العقار للمفضلة')}</p>
<div className="flex gap-3"> <div className="flex gap-3">
<button <button
onClick={() => setShowLoginDialog(false)} 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" className="flex-1 py-3 border border-gray-200 rounded-xl font-medium text-gray-600 hover:bg-gray-50 transition-colors"
> >
{t('cancel')} {t('cancel', 'إلغاء')}
</button> </button>
<Link <Link
href="/login" href="/login"
className="flex-1 py-3 bg-amber-500 text-white rounded-xl font-medium hover:bg-amber-600 transition-colors text-center" className="flex-1 py-3 bg-amber-500 text-white rounded-xl font-medium hover:bg-amber-600 transition-colors text-center"
> >
{t('login')} {t('login', 'دخول')}
</Link> </Link>
</div> </div>
</motion.div> </motion.div>
@ -721,4 +847,4 @@ export default function PropertiesPage() {
)} )}
</div> </div>
); );
} }