Files
SweetHome/app/properties/page.js
Claw AI da0c36727f
All checks were successful
Build frontend / build (push) Successful in 38s
Remove all fallback dummy data - API-only
- Removed FALLBACK_PROPERTIES from main page, properties listing, and property detail
- Pages now start empty and populate only from API responses
- Show empty state / error on API failure instead of dummy data
2026-03-28 17:48:00 +00:00

621 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Search,
MapPin,
Bed,
Bath,
Square,
DollarSign,
Filter,
Grid3x3,
List,
Heart,
Share2,
ChevronDown,
Star,
Camera,
Home,
Building2,
Trees,
Waves,
Warehouse,
Sparkles,
Shield,
Calendar,
Phone,
Mail,
MessageCircle
} from 'lucide-react';
import Image from 'next/image';
import Link from 'next/link';
import { getRentProperties, getSaleProperties } from '../utils/api';
// Map API data to UI format
function mapApiProperty(item, index) {
const info = item.propertyInformation || {};
const dailyPrice = item.dailyRent ?? item.monthlyRent ?? item.price ?? 0;
const monthlyPrice = item.monthlyRent ?? 0;
const buildingTypeMap = { 0: 'apartment', 1: 'villa', 2: 'house' };
const propType = buildingTypeMap[info.buildingType] ?? buildingTypeMap[item.type] ?? 'apartment';
const statusMap = { 0: 'available', 1: 'booked', 2: 'maintenance' };
const status = statusMap[info.status] ?? statusMap[item.status] ?? 'available';
const features = [];
if (item.isSmokeAllow) features.push('يسمح بالتدخين');
if (item.isVisitorAllow) features.push('يسمح بالزوار');
if (item.specializedFor) features.push('متخصص');
if (info.numberOfBedRooms) features.push(`${info.numberOfBedRooms} غرف نوم`);
if (info.numberOfBathRooms) features.push(`${info.numberOfBathRooms} حمامات`);
return {
id: item.id ?? index + 1,
title: info.address || `عقار #${item.id || index + 1}`,
description: info.description || '',
type: propType,
price: dailyPrice,
priceUnit: 'daily',
location: {
city: extractCity(info.address) || 'دمشق',
district: info.address || '',
},
bedrooms: info.numberOfBedRooms || 0,
bathrooms: info.numberOfBathRooms || 0,
area: info.space || 0,
features,
images: ['/property-placeholder.jpg'],
status,
rating: item.rating || 4.5,
isNew: false,
_raw: item,
};
}
function extractCity(address) {
if (!address) return '';
const cities = ['دمشق', 'حلب', 'حمص', 'اللاذقية', 'درعا', 'طرطوس', 'السويداء', 'دير الزور', 'الرقة', 'إدلب', 'الحسكة', 'القامشلي', 'ريف دمشق'];
for (const city of cities) {
if (address.includes(city)) return city;
}
return '';
}
// API-only — no fallback data
const PropertyCard = ({ property, viewMode = 'grid' }) => {
const [isFavorite, setIsFavorite] = useState(false);
const [currentImage, setCurrentImage] = useState(0);
const formatCurrency = (amount) => {
return amount?.toLocaleString() + ' ل.س';
};
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 'فيلا';
case 'apartment': return 'شقة';
case 'house': return 'بيت';
case 'studio': return 'استوديو';
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
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={() => setIsFavorite(!isFavorite)}
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 ${isFavorite ? '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' ? 'متاح' : 'محجوز'}
</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" />
{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' ? 'يوم' : 'شهر'}</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} غرف</span>
</div>
<div className="flex items-center gap-1 text-gray-600">
<Bath className="w-4 h-4" />
<span>{property.bathrooms} حمامات</span>
</div>
<div className="flex items-center gap-1 text-gray-600">
<Square className="w-4 h-4" />
<span>{property.area} م²</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}`}
className="flex-1 bg-gray-800 text-white py-3 rounded-xl font-medium hover:bg-gray-900 transition-colors text-center"
>
عرض التفاصيل
</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={() => setIsFavorite(!isFavorite)}
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 ${isFavorite ? '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">متاح</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">{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' ? 'يوم' : 'شهر'}</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}م²</span>
</div>
</div>
<div className="flex items-center gap-1">
<Star className="w-4 h-4 fill-gray-400 text-gray-400" />
<span className="text-sm font-medium text-gray-700">{property.rating || 4.5}</span>
</div>
</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"
>
عرض التفاصيل
</Link>
</div>
</motion.div>
);
};
const FilterBar = ({ filters, onFilterChange }) => {
const [showFilters, setShowFilters] = useState(false);
const propertyTypes = [
{ id: 'all', label: 'الكل' },
{ id: 'apartment', label: 'شقة', icon: Building2 },
{ id: 'villa', label: 'فيلا', icon: Home },
{ id: 'house', label: 'بيت', icon: Home },
];
const priceRanges = [
{ id: 'all', label: 'جميع الأسعار' },
{ id: '0-500000', label: 'أقل من 500,000' },
{ id: '500000-1000000', label: '500,000 - 1,000,000' },
{ id: '1000000-2000000', label: '1,000,000 - 2,000,000' },
{ id: '2000000-5000000', label: '2,000,000 - 5,000,000' },
{ id: '5000000+', label: 'أكثر من 5,000,000' }
];
const cities = [
{ id: 'all', label: 'جميع المدن' },
{ id: 'دمشق', label: 'دمشق' },
{ id: 'حلب', label: 'حلب' },
{ id: 'حمص', label: 'حمص' },
{ id: 'اللاذقية', label: 'اللاذقية' },
{ id: 'درعا', label: 'درعا' }
];
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="ابحث عن عقار..."
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" />
فلاتر متقدمة
<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"
>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 pt-4 border-t border-gray-100">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">نوع العقار</label>
<div className="flex flex-wrap gap-2">
{propertyTypes.map((type) => {
const Icon = type.icon;
return (
<button
key={type.id}
onClick={() => onFilterChange({ ...filters, propertyType: type.id })}
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'}`}
>
{Icon && <Icon className="w-4 h-4" />}
{type.label}
</button>
);
})}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">المدينة</label>
<select
value={filters.city}
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"
>
{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">نطاق السعر</label>
<select
value={filters.priceRange}
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"
>
{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">غرف النوم</label>
<select
value={filters.bedrooms}
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"
>
<option value="all">جميع الأعداد</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">المساحة (م²)</label>
<div className="flex gap-2">
<input
type="number"
placeholder="من"
className="w-full px-4 py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300"
value={filters.minArea}
onChange={(e) => onFilterChange({ ...filters, minArea: e.target.value })}
/>
<input
type="number"
placeholder="إلى"
className="w-full px-4 py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300"
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',
city: 'all',
priceRange: 'all',
bedrooms: 'all',
minArea: '',
maxArea: '',
features: []
})}
className="px-6 py-2 bg-gray-100 rounded-xl font-medium hover:bg-gray-200 transition-colors text-gray-700"
>
إعادة تعيين
</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"
>
تطبيق الفلاتر
</button>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};
export default function PropertiesPage() {
const [viewMode, setViewMode] = useState('grid');
const [sortBy, setSortBy] = useState('newest');
const [properties, setProperties] = useState([]);
const [loading, setLoading] = useState(true);
const [filters, setFilters] = useState({
search: '',
propertyType: 'all',
city: 'all',
priceRange: 'all',
bedrooms: 'all',
minArea: '',
maxArea: '',
features: []
});
useEffect(() => {
async function fetchProperties() {
try {
const [rentData, saleData] = await Promise.all([
getRentProperties().catch(() => []),
getSaleProperties().catch(() => []),
]);
const rentList = Array.isArray(rentData) ? rentData : [];
const saleList = Array.isArray(saleData) ? saleData : [];
const mapped = [
...rentList.map((p, i) => mapApiProperty(p, i)),
...saleList.map((p, i) => mapApiProperty(p, rentList.length + i)),
];
if (mapped.length > 0) {
setProperties(mapped);
}
} catch (err) {
console.error('[Properties] Failed to fetch properties:', err);
} finally {
setLoading(false);
}
}
fetchProperties();
}, []);
const filteredProperties = properties
.filter(property => {
if (filters.search && !property.title.includes(filters.search) && !property.description.includes(filters.search)) {
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') {
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;
}
}
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;
})
.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 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">عقارات للإيجار</h1>
<p className="text-gray-500">أفضل العقارات في سوريا</p>
{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> عقار متاح
</div>
<div className="flex gap-3">
<select
value={sortBy}
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"
>
<option value="newest">الأحدث</option>
<option value="price_asc">السعر: من الأقل</option>
<option value="price_desc">السعر: من الأعلى</option>
<option value="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} />
))}
</div>
{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">لا توجد عقارات</h3>
<p className="text-gray-500">جرب تغيير معايير البحث</p>
</motion.div>
)}
</div>
</div>
);
}