Add API client and wire up live data fetching
All checks were successful
Build frontend / build (push) Successful in 43s

- Created app/utils/api.js with functions for all OpenAPI endpoints
- Updated main page to fetch RentProperties + SaleProperties from API
- Updated properties listing page with API integration
- Updated property detail page to fetch by ID from API
- Added mapApiProperty() adapter to transform API responses to UI format
- All pages gracefully fall back to dummy data if API is unavailable
This commit is contained in:
Claw AI
2026-03-26 22:20:33 +00:00
parent 082f20da40
commit cfb9c0058b
4 changed files with 673 additions and 631 deletions

View File

@ -1,8 +1,7 @@
'use client';
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useTranslation } from 'react-i18next';
import {
Search,
MapPin,
@ -32,7 +31,67 @@ import {
} 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 || item;
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] || 'apartment';
const statusMap = { 0: 'available', 1: 'booked', 2: 'maintenance' };
const status = statusMap[info.status] || 'available';
const features = [];
if (item.isSmokeAllow) features.push('يسمح بالتدخين');
if (item.isVisitorAllow) features.push('يسمح بالزوار');
if (info.numberOfRooms) features.push(`${info.numberOfRooms} غرف`);
if (info.numberOfBathRooms) features.push(`${info.numberOfBathRooms} حمامات`);
return {
id: item.id ?? index + 1,
title: info.address || info.description?.substring(0, 40) || 'عقار',
description: info.description || '',
type: propType,
price: dailyPrice,
priceUnit: 'daily',
location: {
city: extractCity(info.address),
district: info.address || '',
},
bedrooms: info.numberOfBedRooms || info.numberOfRooms || 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 '';
}
// Fallback data
const FALLBACK_PROPERTIES = [
{ id: 1, title: 'فيلا فاخرة في المزة', description: 'فيلا فاخرة مع حديقة خاصة ومسبح في أفضل أحياء دمشق.', type: 'villa', price: 500000, priceUnit: 'daily', location: { city: 'دمشق', district: 'المزة' }, bedrooms: 5, bathrooms: 4, area: 450, features: ['مسبح', 'حديقة خاصة', 'موقف سيارات', 'أمن'], images: ['/villa1.jpg'], status: 'available', rating: 4.8, isNew: true },
{ id: 2, title: 'شقة حديثة في الشهباء', description: 'شقة عصرية في حي الشهباء الراقي بحلب.', type: 'apartment', price: 250000, priceUnit: 'daily', location: { city: 'حلب', district: 'الشهباء' }, bedrooms: 3, bathrooms: 2, area: 180, features: ['مطبخ مجهز', 'بلكونة', 'موقف سيارات', 'مصعد'], images: ['/apartment1.jpg'], status: 'available', rating: 4.5, isNew: false },
{ id: 3, title: 'بيت عائلي في بابا عمرو', description: 'بيت واسع مناسب للعائلات في حمص.', type: 'house', price: 350000, priceUnit: 'daily', location: { city: 'حمص', district: 'بابا عمرو' }, bedrooms: 4, bathrooms: 3, area: 300, features: ['حديقة كبيرة', 'موقف سيارات', 'مدفأة'], images: ['/house1.jpg'], status: 'booked', rating: 4.3, isNew: false },
{ id: 4, title: 'شقة بجانب البحر', description: 'شقة رائعة مع إطلالة بحرية في اللاذقية.', type: 'apartment', price: 300000, priceUnit: 'daily', location: { city: 'اللاذقية', district: 'الشاطئ الأزرق' }, bedrooms: 3, bathrooms: 2, area: 200, features: ['إطلالة بحرية', 'شرفة', 'تكييف'], images: ['/seaside1.jpg'], status: 'available', rating: 4.9, isNew: true },
{ id: 5, title: 'فيلا في درعا', description: 'فيلا فاخرة في حي الأطباء بدرعا.', type: 'villa', price: 400000, priceUnit: 'daily', location: { city: 'درعا', district: 'حي الأطباء' }, bedrooms: 4, bathrooms: 3, area: 350, features: ['حديقة مثمرة', 'أنظمة أمن', 'مسبح'], images: ['/villa4.jpg'], status: 'available', rating: 4.6, isNew: false },
];
const PropertyCard = ({ property, viewMode = 'grid' }) => {
const [isFavorite, setIsFavorite] = useState(false);
@ -43,7 +102,7 @@ const PropertyCard = ({ property, viewMode = 'grid' }) => {
};
const getPropertyTypeIcon = (type) => {
switch(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" />;
@ -53,7 +112,7 @@ const PropertyCard = ({ property, viewMode = 'grid' }) => {
};
const getPropertyTypeLabel = (type) => {
switch(type) {
switch (type) {
case 'villa': return 'فيلا';
case 'apartment': return 'شقة';
case 'house': return 'بيت';
@ -83,9 +142,7 @@ const PropertyCard = ({ property, viewMode = 'grid' }) => {
<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'
}`}
className={`w-1.5 h-1.5 rounded-full transition-all ${idx === currentImage ? 'bg-gray-800 w-3' : 'bg-white/70'}`}
/>
))}
</div>
@ -98,11 +155,6 @@ const PropertyCard = ({ property, viewMode = 'grid' }) => {
<Heart className={`w-4 h-4 ${isFavorite ? 'fill-red-500 text-red-500' : 'text-gray-600'}`} />
</button>
</div>
{property.isNew && (
<div className="absolute top-2 left-2 bg-gray-800 text-white px-2 py-1 rounded-lg text-xs font-medium">
جديد
</div>
)}
</div>
<div className="md:w-2/3 p-6">
@ -113,11 +165,7 @@ const PropertyCard = ({ property, viewMode = 'grid' }) => {
{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'
}`}>
<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>
@ -148,22 +196,7 @@ const PropertyCard = ({ property, viewMode = 'grid' }) => {
</div>
</div>
<p className="text-gray-600 text-sm mb-4 line-clamp-2">
{property.description}
</p>
<div className="flex flex-wrap gap-2 mb-4">
{property.features.slice(0, 4).map((feature, idx) => (
<span key={idx} className="px-2 py-1 bg-gray-100 text-gray-600 rounded-lg text-xs">
{feature}
</span>
))}
{property.features.length > 4 && (
<span className="px-2 py-1 bg-gray-100 text-gray-600 rounded-lg text-xs">
+{property.features.length - 4}
</span>
)}
</div>
<p className="text-gray-600 text-sm mb-4 line-clamp-2">{property.description}</p>
<div className="flex gap-3">
<Link
@ -195,19 +228,6 @@ const PropertyCard = ({ property, viewMode = 'grid' }) => {
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)}
@ -216,11 +236,6 @@ const PropertyCard = ({ property, viewMode = 'grid' }) => {
<Heart className={`w-4 h-4 ${isFavorite ? 'fill-red-500 text-red-500' : 'text-gray-600'}`} />
</button>
</div>
{property.isNew && (
<div className="absolute top-2 left-2 bg-gray-800 text-white px-2 py-1 rounded-lg text-xs font-medium">
جديد
</div>
)}
</div>
<div className="p-5">
@ -232,9 +247,7 @@ const PropertyCard = ({ property, viewMode = 'grid' }) => {
{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>
<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>
@ -270,19 +283,6 @@ const PropertyCard = ({ property, viewMode = 'grid' }) => {
</div>
</div>
<div className="flex flex-wrap gap-2 mb-4">
{property.features.slice(0, 3).map((feature, idx) => (
<span key={idx} className="px-2 py-1 bg-gray-100 text-gray-600 rounded-lg text-xs">
{feature}
</span>
))}
{property.features.length > 3 && (
<span className="px-2 py-1 bg-gray-100 text-gray-600 rounded-lg text-xs">
+{property.features.length - 3}
</span>
)}
</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"
@ -302,7 +302,6 @@ const FilterBar = ({ filters, onFilterChange }) => {
{ id: 'apartment', label: 'شقة', icon: Building2 },
{ id: 'villa', label: 'فيلا', icon: Home },
{ id: 'house', label: 'بيت', icon: Home },
{ id: 'studio', label: 'استوديو', icon: Building2 }
];
const priceRanges = [
@ -364,11 +363,7 @@ const FilterBar = ({ filters, onFilterChange }) => {
<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'
}`}
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}
@ -439,30 +434,6 @@ const FilterBar = ({ filters, onFilterChange }) => {
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">المميزات</label>
<div className="flex flex-wrap gap-2">
{['مسبح', 'حديقة', 'موقف سيارات', 'أمن', 'مصعد', 'تكييف'].map((feature) => (
<button
key={feature}
onClick={() => {
const newFeatures = filters.features.includes(feature)
? filters.features.filter(f => f !== feature)
: [...filters.features, feature];
onFilterChange({ ...filters, features: newFeatures });
}}
className={`px-3 py-2 rounded-xl text-sm font-medium transition-all ${
filters.features.includes(feature)
? 'bg-gray-800 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
{feature}
</button>
))}
</div>
</div>
</div>
<div className="flex gap-3 mt-4 pt-4 border-t border-gray-100">
@ -496,8 +467,10 @@ const FilterBar = ({ filters, onFilterChange }) => {
};
export default function PropertiesPage() {
const [viewMode, setViewMode] = useState('grid');
const [viewMode, setViewMode] = useState('grid');
const [sortBy, setSortBy] = useState('newest');
const [properties, setProperties] = useState(FALLBACK_PROPERTIES);
const [loading, setLoading] = useState(true);
const [filters, setFilters] = useState({
search: '',
propertyType: 'all',
@ -509,93 +482,34 @@ export default function PropertiesPage() {
features: []
});
const [properties] = useState([
{
id: 1,
title: 'فيلا فاخرة في المزة',
description: 'فيلا فاخرة مع حديقة خاصة ومسبح في أفضل أحياء دمشق.',
type: 'villa',
price: 500000,
priceUnit: 'daily',
location: { city: 'دمشق', district: 'المزة' },
bedrooms: 5,
bathrooms: 4,
area: 450,
features: ['مسبح', 'حديقة خاصة', 'موقف سيارات', 'أمن'],
images: ['/villa1.jpg'],
status: 'available',
rating: 4.8,
isNew: true
},
{
id: 2,
title: 'شقة حديثة في الشهباء',
description: 'شقة عصرية في حي الشهباء الراقي بحلب.',
type: 'apartment',
price: 250000,
priceUnit: 'daily',
location: { city: 'حلب', district: 'الشهباء' },
bedrooms: 3,
bathrooms: 2,
area: 180,
features: ['مطبخ مجهز', 'بلكونة', 'موقف سيارات', 'مصعد'],
images: ['/apartment1.jpg'],
status: 'available',
rating: 4.5,
isNew: false
},
{
id: 3,
title: 'بيت عائلي في بابا عمرو',
description: 'بيت واسع مناسب للعائلات في حمص.',
type: 'house',
price: 350000,
priceUnit: 'daily',
location: { city: 'حمص', district: 'بابا عمرو' },
bedrooms: 4,
bathrooms: 3,
area: 300,
features: ['حديقة كبيرة', 'موقف سيارات', 'مدفأة'],
images: ['/house1.jpg'],
status: 'booked',
rating: 4.3,
isNew: false
},
{
id: 4,
title: 'شقة بجانب البحر',
description: 'شقة رائعة مع إطلالة بحرية في اللاذقية.',
type: 'apartment',
price: 300000,
priceUnit: 'daily',
location: { city: 'اللاذقية', district: 'الشاطئ الأزرق' },
bedrooms: 3,
bathrooms: 2,
area: 200,
features: ['إطلالة بحرية', 'شرفة', 'تكييف'],
images: ['/seaside1.jpg'],
status: 'available',
rating: 4.9,
isNew: true
},
{
id: 5,
title: 'فيلا في درعا',
description: 'فيلا فاخرة في حي الأطباء بدرعا.',
type: 'villa',
price: 400000,
priceUnit: 'daily',
location: { city: 'درعا', district: 'حي الأطباء' },
bedrooms: 4,
bathrooms: 3,
area: 350,
features: ['حديقة مثمرة', 'أنظمة أمن', 'مسبح'],
images: ['/villa4.jpg'],
status: 'available',
rating: 4.6,
isNew: false
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.warn('Failed to fetch properties:', err);
} finally {
setLoading(false);
}
}
]);
fetchProperties();
}, []);
const filteredProperties = properties
.filter(property => {
@ -613,8 +527,8 @@ export default function PropertiesPage() {
if (max) {
if (property.price < parseInt(min) || property.price > parseInt(max)) return false;
} else if (filters.priceRange.endsWith('+')) {
const min = parseInt(filters.priceRange.replace('+', ''));
if (property.price < min) return false;
const minVal = parseInt(filters.priceRange.replace('+', ''));
if (property.price < minVal) return false;
}
}
if (filters.bedrooms !== 'all' && property.bedrooms < parseInt(filters.bedrooms)) {
@ -622,17 +536,14 @@ export default function PropertiesPage() {
}
if (filters.minArea && property.area < parseInt(filters.minArea)) return false;
if (filters.maxArea && property.area > parseInt(filters.maxArea)) return false;
if (filters.features.length > 0) {
if (!filters.features.every(f => property.features.includes(f))) return false;
}
return true;
})
.sort((a, b) => {
switch(sortBy) {
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 b.isNew ? 1 : -1;
default: return 0;
}
});
@ -646,6 +557,11 @@ export default function PropertiesPage() {
>
<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} />
@ -668,19 +584,13 @@ export default function PropertiesPage() {
<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'
}`}
title="عرض شبكي"
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'
}`}
title="عرض قائمة"
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>
@ -688,7 +598,7 @@ export default function PropertiesPage() {
</div>
</div>
<div className={viewMode === 'grid'
<div className={viewMode === 'grid'
? 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6'
: 'space-y-4'
}>
@ -713,4 +623,4 @@ export default function PropertiesPage() {
</div>
</div>
);
}
}