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

@ -28,6 +28,155 @@ import HeroSearch from './components/home/HeroSearch';
import PropertyMap from './components/home/PropertyMap';
import Link from 'next/link';
import Image from 'next/image';
import { getRentProperties, getSaleProperties } from './utils/api';
// Map API property data to the format the UI expects
function mapApiProperty(item, index) {
const info = item.propertyInformation || item;
const isRent = item.monthlyRent !== undefined || item.dailyRent !== undefined;
// Determine price display
const dailyPrice = item.dailyRent ?? item.monthlyRent ?? item.price ?? 0;
const monthlyPrice = item.monthlyRent ?? 0;
// Map building type integer to string
const buildingTypeMap = { 0: 'apartment', 1: 'villa', 2: 'house' };
const propType = buildingTypeMap[info.buildingType] || 'apartment';
// Map property status integer to string
const statusMap = { 0: 'available', 1: 'booked', 2: 'maintenance' };
const status = statusMap[info.status] || 'available';
// Extract features as string array
const features = [];
if (item.isSmokeAllow) features.push('يسمح بالتدخين');
if (item.isVisitorAllow) features.push('يسمح بالزوار');
if (item.specializedFor) 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,
priceUSD: dailyPrice,
priceUnit: 'daily',
location: {
city: extractCity(info.address),
district: info.address || '',
address: info.address || '',
lat: parseFloat(info.cordsX) || 0,
lng: parseFloat(info.cordsY) || 0,
},
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,
allowedIdentities: ['syrian', 'passport'],
priceDisplay: {
daily: dailyPrice,
monthly: monthlyPrice,
},
bookings: [],
_raw: item,
};
}
function extractCity(address) {
if (!address) return '';
const cities = ['دمشق', 'حلب', 'حمص', 'اللاذقية', 'درعا', 'طرطوس', 'السويداء', 'دير الزور', 'الرقة', 'إدلب', 'الحسكة', 'القامشلي', 'ريف دمشق'];
for (const city of cities) {
if (address.includes(city)) return city;
}
return address.split(' ').pop() || '';
}
// Fallback dummy data
const FALLBACK_PROPERTIES = [
{
id: 1,
title: 'فيلا فاخرة في المزة',
description: 'فيلا فاخرة مع حديقة خاصة ومسبح في أفضل أحياء دمشق.',
type: 'villa',
price: 500000,
priceUSD: 50,
priceUnit: 'daily',
location: { city: 'دمشق', district: 'المزة', address: 'شارع المزة - فيلات غربية', lat: 33.5138, lng: 36.2765 },
bedrooms: 5, bathrooms: 4, area: 450,
features: ['مسبح', 'حديقة خاصة', 'موقف سيارات', 'أمن 24/7', 'تدفئة مركزية', 'تكييف مركزي'],
images: ['/villa1.jpg', '/villa2.jpg', '/villa3.jpg'],
status: 'available', rating: 4.8, isNew: true,
allowedIdentities: ['syrian', 'passport'],
priceDisplay: { daily: 500000, monthly: 15000000 },
bookings: [{ startDate: '2024-03-10', endDate: '2024-03-15' }, { startDate: '2024-03-20', endDate: '2024-03-25' }]
},
{
id: 2,
title: 'شقة حديثة في الشهباء',
description: 'شقة عصرية في حي الشهباء الراقي بحلب.',
type: 'apartment',
price: 250000, priceUSD: 25, priceUnit: 'daily',
location: { city: 'حلب', district: 'الشهباء', address: 'شارع النيل - بناء الرحاب', lat: 36.2021, lng: 37.1347 },
bedrooms: 3, bathrooms: 2, area: 180,
features: ['مطبخ مجهز', 'بلكونة', 'موقف سيارات', 'مصعد'],
images: ['/apartment1.jpg', '/apartment2.jpg'],
status: 'available', rating: 4.5, isNew: false,
allowedIdentities: ['syrian'],
priceDisplay: { daily: 250000, monthly: 7500000 },
bookings: [{ startDate: '2024-03-05', endDate: '2024-03-08' }]
},
{
id: 3,
title: 'بيت عائلي في بابا عمرو',
description: 'بيت واسع مناسب للعائلات في حمص.',
type: 'house',
price: 350000, priceUSD: 35, priceUnit: 'daily',
location: { city: 'حمص', district: 'بابا عمرو', address: 'حي الزهور', lat: 34.7265, lng: 36.7186 },
bedrooms: 4, bathrooms: 3, area: 300,
features: ['حديقة كبيرة', 'موقف سيارات', 'مدفأة', 'كراج'],
images: ['/house1.jpg'],
status: 'booked', rating: 4.3, isNew: false,
allowedIdentities: ['syrian', 'passport'],
priceDisplay: { daily: 350000, monthly: 10500000 },
bookings: []
},
{
id: 4,
title: 'شقة بجانب البحر',
description: 'شقة رائعة مع إطلالة بحرية في اللاذقية.',
type: 'apartment',
price: 300000, priceUSD: 30, priceUnit: 'daily',
location: { city: 'اللاذقية', district: 'الشاطئ الأزرق', address: 'الكورنيش الغربي', lat: 35.5306, lng: 35.7801 },
bedrooms: 3, bathrooms: 2, area: 200,
features: ['إطلالة بحرية', 'شرفة', 'تكييف', 'أمن'],
images: ['/seaside1.jpg', '/seaside2.jpg', '/seaside3.jpg'],
status: 'available', rating: 4.9, isNew: true,
allowedIdentities: ['passport'],
priceDisplay: { daily: 300000, monthly: 9000000 },
bookings: []
},
{
id: 5,
title: 'فيلا في درعا',
description: 'فيلا فاخرة في حي الأطباء بدرعا.',
type: 'villa',
price: 400000, priceUSD: 40, priceUnit: 'daily',
location: { city: 'درعا', district: 'حي الأطباء', address: 'شارع الشفاء', lat: 32.6237, lng: 36.1016 },
bedrooms: 4, bathrooms: 3, area: 350,
features: ['حديقة مثمرة', 'أنظمة أمن', 'مسبح', 'كراج'],
images: ['/villa4.jpg', '/villa5.jpg'],
status: 'available', rating: 4.6, isNew: false,
allowedIdentities: ['syrian', 'passport'],
priceDisplay: { daily: 400000, monthly: 12000000 },
bookings: []
}
];
export default function HomePage() {
const mapSectionRef = useRef(null);
@ -39,11 +188,44 @@ export default function HomePage() {
const [showUserMenu, setShowUserMenu] = useState(false);
const menuRef = useRef(null);
const [allProperties, setAllProperties] = useState(FALLBACK_PROPERTIES);
const [loading, setLoading] = useState(true);
// Fetch properties from API on mount
useEffect(() => {
const storedUser = localStorage.getItem('user');
if (storedUser) {
setUser(JSON.parse(storedUser));
}
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) {
setAllProperties(mapped);
}
// If API returns empty, keep fallback
} catch (err) {
console.warn('Failed to fetch properties, using fallback data:', err);
// keep fallback data
} finally {
setLoading(false);
}
}
fetchProperties();
}, []);
useEffect(() => {
@ -62,179 +244,21 @@ export default function HomePage() {
setShowUserMenu(false);
};
const [allProperties] = useState([
{
id: 1,
title: 'فيلا فاخرة في المزة',
description: 'فيلا فاخرة مع حديقة خاصة ومسبح في أفضل أحياء دمشق.',
type: 'villa',
price: 500000,
priceUSD: 50,
priceUnit: 'daily',
location: {
city: 'دمشق',
district: 'المزة',
address: 'شارع المزة - فيلات غربية',
lat: 33.5138,
lng: 36.2765
},
bedrooms: 5,
bathrooms: 4,
area: 450,
features: ['مسبح', 'حديقة خاصة', 'موقف سيارات', 'أمن 24/7', 'تدفئة مركزية', 'تكييف مركزي'],
images: ['/villa1.jpg', '/villa2.jpg', '/villa3.jpg'],
status: 'available',
rating: 4.8,
isNew: true,
allowedIdentities: ['syrian', 'passport'],
priceDisplay: {
daily: 500000,
monthly: 15000000
},
bookings: [
{ startDate: '2024-03-10', endDate: '2024-03-15' },
{ startDate: '2024-03-20', endDate: '2024-03-25' }
]
},
{
id: 2,
title: 'شقة حديثة في الشهباء',
description: 'شقة عصرية في حي الشهباء الراقي بحلب.',
type: 'apartment',
price: 250000,
priceUSD: 25,
priceUnit: 'daily',
location: {
city: 'حلب',
district: 'الشهباء',
address: 'شارع النيل - بناء الرحاب',
lat: 36.2021,
lng: 37.1347
},
bedrooms: 3,
bathrooms: 2,
area: 180,
features: ['مطبخ مجهز', 'بلكونة', 'موقف سيارات', 'مصعد'],
images: ['/apartment1.jpg', '/apartment2.jpg'],
status: 'available',
rating: 4.5,
isNew: false,
allowedIdentities: ['syrian'],
priceDisplay: {
daily: 250000,
monthly: 7500000
},
bookings: [
{ startDate: '2024-03-05', endDate: '2024-03-08' }
]
},
{
id: 3,
title: 'بيت عائلي في بابا عمرو',
description: 'بيت واسع مناسب للعائلات في حمص.',
type: 'house',
price: 350000,
priceUSD: 35,
priceUnit: 'daily',
location: {
city: 'حمص',
district: 'بابا عمرو',
address: 'حي الزهور',
lat: 34.7265,
lng: 36.7186
},
bedrooms: 4,
bathrooms: 3,
area: 300,
features: ['حديقة كبيرة', 'موقف سيارات', 'مدفأة', 'كراج'],
images: ['/house1.jpg'],
status: 'booked',
rating: 4.3,
isNew: false,
allowedIdentities: ['syrian', 'passport'],
priceDisplay: {
daily: 350000,
monthly: 10500000
},
bookings: []
},
{
id: 4,
title: 'شقة بجانب البحر',
description: 'شقة رائعة مع إطلالة بحرية في اللاذقية.',
type: 'apartment',
price: 300000,
priceUSD: 30,
priceUnit: 'daily',
location: {
city: 'اللاذقية',
district: 'الشاطئ الأزرق',
address: 'الكورنيش الغربي',
lat: 35.5306,
lng: 35.7801
},
bedrooms: 3,
bathrooms: 2,
area: 200,
features: ['إطلالة بحرية', 'شرفة', 'تكييف', 'أمن'],
images: ['/seaside1.jpg', '/seaside2.jpg', '/seaside3.jpg'],
status: 'available',
rating: 4.9,
isNew: true,
allowedIdentities: ['passport'],
priceDisplay: {
daily: 300000,
monthly: 9000000
},
bookings: []
},
{
id: 5,
title: 'فيلا في درعا',
description: 'فيلا فاخرة في حي الأطباء بدرعا.',
type: 'villa',
price: 400000,
priceUSD: 40,
priceUnit: 'daily',
location: {
city: 'درعا',
district: 'حي الأطباء',
address: 'شارع الشفاء',
lat: 32.6237,
lng: 36.1016
},
bedrooms: 4,
bathrooms: 3,
area: 350,
features: ['حديقة مثمرة', 'أنظمة أمن', 'مسبح', 'كراج'],
images: ['/villa4.jpg', '/villa5.jpg'],
status: 'available',
rating: 4.6,
isNew: false,
allowedIdentities: ['syrian', 'passport'],
priceDisplay: {
daily: 400000,
monthly: 12000000
},
bookings: []
}
]);
const applyFilters = (filters) => {
setSearchFilters(filters);
const filtered = allProperties.filter(property => {
if (filters.city && filters.city !== 'all' && property.location.city !== filters.city) {
return false;
}
if (filters.propertyType && filters.propertyType !== 'all' && property.type !== filters.propertyType) {
return false;
}
if (filters.priceRange && filters.priceRange !== 'all') {
const priceUSD = property.priceUSD;
switch(filters.priceRange) {
switch (filters.priceRange) {
case '0-500': if (priceUSD > 50) return false; break;
case '500-1000': if (priceUSD < 51 || priceUSD > 100) return false; break;
case '1000-2000': if (priceUSD < 101 || priceUSD > 200) return false; break;
@ -242,37 +266,37 @@ export default function HomePage() {
case '3000+': if (priceUSD < 301) return false; break;
}
}
if (filters.identityType && property.allowedIdentities) {
if (!property.allowedIdentities.includes(filters.identityType)) {
return false;
}
}
return true;
});
setFilteredProperties(filtered);
if (!showMap) {
setShowMap(true);
setTimeout(() => {
if (mapSectionRef.current) {
setIsScrolling(true);
mapSectionRef.current.scrollIntoView({
behavior: 'smooth',
mapSectionRef.current.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
setTimeout(() => setIsScrolling(false), 1000);
}
}, 300);
} else {
if (mapSectionRef.current) {
setIsScrolling(true);
mapSectionRef.current.scrollIntoView({
behavior: 'smooth',
mapSectionRef.current.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
setTimeout(() => setIsScrolling(false), 1000);
@ -303,7 +327,7 @@ export default function HomePage() {
<div className="min-h-screen">
<section className="relative min-h-screen flex items-center justify-center overflow-hidden">
<div className="absolute inset-0 z-0">
<motion.div
<motion.div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: 'url(/hero.jpg)',
@ -316,7 +340,7 @@ export default function HomePage() {
</div>
<div className="relative z-10 container mx-auto px-4 sm:px-6 lg:px-8">
<div className="max-w-6xl mx-auto">
<motion.div
<motion.div
className="text-center mb-12"
initial="hidden"
animate="visible"
@ -328,7 +352,7 @@ export default function HomePage() {
}
}}
>
<motion.h1
<motion.h1
className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-6 leading-tight tracking-tight"
variants={{
hidden: { opacity: 0, y: 20 },
@ -336,7 +360,7 @@ export default function HomePage() {
}}
>
إيجاد منزلك الجديد<br />
<motion.span
<motion.span
className="text-amber-400"
animate={{
y: [0, -10, 0],
@ -350,7 +374,7 @@ export default function HomePage() {
أصبح سهلاً
</motion.span>
</motion.h1>
<motion.p
<motion.p
className="text-base sm:text-lg text-gray-200 max-w-2xl mx-auto leading-relaxed"
variants={{
hidden: { opacity: 0, y: 20 },
@ -360,9 +384,9 @@ export default function HomePage() {
نوفر قوائم عقارات عالية الجودة لمساعدتك في إيجاد المنزل المثالي
</motion.p>
</motion.div>
{!isOwner && <HeroSearch onSearch={applyFilters} />}
{isOwner && (
<motion.div
initial={{ opacity: 0, y: 20 }}
@ -387,9 +411,9 @@ export default function HomePage() {
)}
</div>
</div>
{!showMap && !isOwner && (
<motion.div
<motion.div
className="absolute bottom-8 left-1/2 transform -translate-x-1/2 cursor-pointer"
animate={{
y: [0, 10, 0],
@ -414,12 +438,12 @@ export default function HomePage() {
{!isOwner && (
<AnimatePresence mode="wait">
{showMap && (
<motion.section
<motion.section
ref={mapSectionRef}
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -50 }}
transition={{
transition={{
type: "spring",
damping: 20,
stiffness: 100,
@ -428,7 +452,7 @@ export default function HomePage() {
className="py-12 bg-gray-50 relative"
>
{isScrolling && (
<motion.div
<motion.div
className="absolute top-0 left-0 right-0 h-1 bg-amber-500 z-10"
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
@ -470,15 +494,15 @@ export default function HomePage() {
</p>
)}
</motion.div>
<motion.div
<motion.div
className="bg-white rounded-2xl shadow-xl overflow-hidden border border-gray-200"
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.3, type: "spring" }}
>
{filteredProperties.length > 0 ? (
<PropertyMap
<PropertyMap
properties={filteredProperties}
userIdentity={searchFilters?.identityType || 'syrian'}
/>
@ -494,7 +518,7 @@ export default function HomePage() {
</div>
)}
</motion.div>
{filteredProperties.length > 0 && searchFilters && (
<motion.div
initial={{ opacity: 0, y: 20 }}
@ -511,19 +535,19 @@ export default function HomePage() {
<div className="bg-white px-4 py-2 rounded-full shadow-sm border border-gray-200 text-sm">
<span className="text-gray-600">نوع العقار: </span>
<span className="font-bold text-gray-900">
{searchFilters.propertyType === 'all' ? 'الكل' :
searchFilters.propertyType === 'apartment' ? 'شقة' :
searchFilters.propertyType === 'villa' ? 'فيلا' : 'بيت'}
{searchFilters.propertyType === 'all' ? 'الكل' :
searchFilters.propertyType === 'apartment' ? 'شقة' :
searchFilters.propertyType === 'villa' ? 'فيلا' : 'بيت'}
</span>
</div>
<div className="bg-white px-4 py-2 rounded-full shadow-sm border border-gray-200 text-sm">
<span className="text-gray-600">نطاق السعر: </span>
<span className="font-bold text-gray-900">
{searchFilters.priceRange === 'all' ? 'جميع الأسعار' :
searchFilters.priceRange === '0-500' ? 'أقل من 50$' :
searchFilters.priceRange === '500-1000' ? '50$ - 100$' :
searchFilters.priceRange === '1000-2000' ? '100$ - 200$' :
searchFilters.priceRange === '2000-3000' ? '200$ - 300$' : 'أكثر من 300$'}
{searchFilters.priceRange === 'all' ? 'جميع الأسعار' :
searchFilters.priceRange === '0-500' ? 'أقل من 50$' :
searchFilters.priceRange === '500-1000' ? '50$ - 100$' :
searchFilters.priceRange === '1000-2000' ? '100$ - 200$' :
searchFilters.priceRange === '2000-3000' ? '200$ - 300$' : 'أكثر من 300$'}
</span>
</div>
</motion.div>
@ -536,7 +560,7 @@ export default function HomePage() {
<section className="py-20 bg-gradient-to-b from-white to-gray-50">
<div className="container mx-auto px-4">
<motion.div
<motion.div
className="text-center mb-12"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
@ -547,12 +571,12 @@ export default function HomePage() {
لماذا تختار سويت هوم؟
</h2>
<p className="text-gray-600 max-w-2xl mx-auto text-lg">
نجعل عملية إيجاد منزلك المثالي سهلة وسريعة
نجعل عملية إيجاد منزلك المثالي سهلة وسريعة
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<motion.div
<motion.div
className="group bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
@ -568,13 +592,13 @@ export default function HomePage() {
قوائم موثوقة
</h3>
</div>
<p className="text-gray-600 text-sm leading-relaxed">
كل عقار يتم التحقق منه بدقة لضمان الدقة والجودة.
كل عقار يتم التحقق منه بدقة لضمان الدقة والجودة.
</p>
</motion.div>
<motion.div
<motion.div
className="group bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
@ -590,13 +614,13 @@ export default function HomePage() {
عمليات آمنة
</h3>
</div>
<p className="text-gray-600 text-sm leading-relaxed">
سلامتك هي أولويتنا. نوفر معاملات آمنة ونحمي معلوماتك الشخصية.
</p>
</motion.div>
<motion.div
<motion.div
className="group bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
@ -612,7 +636,7 @@ export default function HomePage() {
نتائج سريعة
</h3>
</div>
<p className="text-gray-600 text-sm leading-relaxed">
اعثر على منزلك المثالي في دقائق باستخدام خوارزميات البحث والمطابقة المتقدمة لدينا.
</p>
@ -622,4 +646,4 @@ export default function HomePage() {
</section>
</div>
);
}
}