Add API client and wire up live data fetching
All checks were successful
Build frontend / build (push) Successful in 43s
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:
342
app/page.js
342
app/page.js
@ -28,6 +28,155 @@ import HeroSearch from './components/home/HeroSearch';
|
|||||||
import PropertyMap from './components/home/PropertyMap';
|
import PropertyMap from './components/home/PropertyMap';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import Image from 'next/image';
|
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() {
|
export default function HomePage() {
|
||||||
const mapSectionRef = useRef(null);
|
const mapSectionRef = useRef(null);
|
||||||
@ -39,11 +188,44 @@ export default function HomePage() {
|
|||||||
const [showUserMenu, setShowUserMenu] = useState(false);
|
const [showUserMenu, setShowUserMenu] = useState(false);
|
||||||
const menuRef = useRef(null);
|
const menuRef = useRef(null);
|
||||||
|
|
||||||
|
const [allProperties, setAllProperties] = useState(FALLBACK_PROPERTIES);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// Fetch properties from API on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const storedUser = localStorage.getItem('user');
|
const storedUser = localStorage.getItem('user');
|
||||||
if (storedUser) {
|
if (storedUser) {
|
||||||
setUser(JSON.parse(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(() => {
|
useEffect(() => {
|
||||||
@ -62,164 +244,6 @@ export default function HomePage() {
|
|||||||
setShowUserMenu(false);
|
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) => {
|
const applyFilters = (filters) => {
|
||||||
setSearchFilters(filters);
|
setSearchFilters(filters);
|
||||||
|
|
||||||
@ -234,7 +258,7 @@ export default function HomePage() {
|
|||||||
|
|
||||||
if (filters.priceRange && filters.priceRange !== 'all') {
|
if (filters.priceRange && filters.priceRange !== 'all') {
|
||||||
const priceUSD = property.priceUSD;
|
const priceUSD = property.priceUSD;
|
||||||
switch(filters.priceRange) {
|
switch (filters.priceRange) {
|
||||||
case '0-500': if (priceUSD > 50) return false; break;
|
case '0-500': if (priceUSD > 50) return false; break;
|
||||||
case '500-1000': if (priceUSD < 51 || priceUSD > 100) 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;
|
case '1000-2000': if (priceUSD < 101 || priceUSD > 200) return false; break;
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import {
|
import {
|
||||||
Search,
|
Search,
|
||||||
MapPin,
|
MapPin,
|
||||||
@ -32,7 +31,67 @@ import {
|
|||||||
} 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';
|
||||||
|
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 PropertyCard = ({ property, viewMode = 'grid' }) => {
|
||||||
const [isFavorite, setIsFavorite] = useState(false);
|
const [isFavorite, setIsFavorite] = useState(false);
|
||||||
@ -43,7 +102,7 @@ const PropertyCard = ({ property, viewMode = 'grid' }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getPropertyTypeIcon = (type) => {
|
const getPropertyTypeIcon = (type) => {
|
||||||
switch(type) {
|
switch (type) {
|
||||||
case 'villa': return <Home className="w-4 h-4" />;
|
case 'villa': return <Home className="w-4 h-4" />;
|
||||||
case 'apartment': return <Building2 className="w-4 h-4" />;
|
case 'apartment': return <Building2 className="w-4 h-4" />;
|
||||||
case 'house': return <Home 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) => {
|
const getPropertyTypeLabel = (type) => {
|
||||||
switch(type) {
|
switch (type) {
|
||||||
case 'villa': return 'فيلا';
|
case 'villa': return 'فيلا';
|
||||||
case 'apartment': return 'شقة';
|
case 'apartment': return 'شقة';
|
||||||
case 'house': return 'بيت';
|
case 'house': return 'بيت';
|
||||||
@ -83,9 +142,7 @@ const PropertyCard = ({ property, viewMode = 'grid' }) => {
|
|||||||
<button
|
<button
|
||||||
key={idx}
|
key={idx}
|
||||||
onClick={() => setCurrentImage(idx)}
|
onClick={() => setCurrentImage(idx)}
|
||||||
className={`w-1.5 h-1.5 rounded-full transition-all ${
|
className={`w-1.5 h-1.5 rounded-full transition-all ${idx === currentImage ? 'bg-gray-800 w-3' : 'bg-white/70'}`}
|
||||||
idx === currentImage ? 'bg-gray-800 w-3' : 'bg-white/70'
|
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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'}`} />
|
<Heart className={`w-4 h-4 ${isFavorite ? 'fill-red-500 text-red-500' : 'text-gray-600'}`} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div className="md:w-2/3 p-6">
|
<div className="md:w-2/3 p-6">
|
||||||
@ -113,11 +165,7 @@ const PropertyCard = ({ property, viewMode = 'grid' }) => {
|
|||||||
{getPropertyTypeIcon(property.type)}
|
{getPropertyTypeIcon(property.type)}
|
||||||
{getPropertyTypeLabel(property.type)}
|
{getPropertyTypeLabel(property.type)}
|
||||||
</span>
|
</span>
|
||||||
<span className={`px-2 py-1 rounded-lg text-xs font-medium ${
|
<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'
|
|
||||||
? 'bg-gray-800 text-white'
|
|
||||||
: 'bg-gray-200 text-gray-600'
|
|
||||||
}`}>
|
|
||||||
{property.status === 'available' ? 'متاح' : 'محجوز'}
|
{property.status === 'available' ? 'متاح' : 'محجوز'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -148,22 +196,7 @@ const PropertyCard = ({ property, viewMode = 'grid' }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-gray-600 text-sm mb-4 line-clamp-2">
|
<p className="text-gray-600 text-sm mb-4 line-clamp-2">{property.description}</p>
|
||||||
{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>
|
|
||||||
|
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Link
|
<Link
|
||||||
@ -195,19 +228,6 @@ const PropertyCard = ({ property, viewMode = 'grid' }) => {
|
|||||||
fill
|
fill
|
||||||
className="object-cover"
|
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">
|
<div className="absolute top-2 right-2 flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsFavorite(!isFavorite)}
|
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'}`} />
|
<Heart className={`w-4 h-4 ${isFavorite ? 'fill-red-500 text-red-500' : 'text-gray-600'}`} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div className="p-5">
|
<div className="p-5">
|
||||||
@ -232,9 +247,7 @@ const PropertyCard = ({ property, viewMode = 'grid' }) => {
|
|||||||
{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">
|
<span className="px-2 py-1 bg-gray-800 text-white rounded-lg text-xs font-medium">متاح</span>
|
||||||
متاح
|
|
||||||
</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>
|
||||||
@ -270,19 +283,6 @@ const PropertyCard = ({ property, viewMode = 'grid' }) => {
|
|||||||
</div>
|
</div>
|
||||||
</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
|
<Link
|
||||||
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"
|
||||||
@ -302,7 +302,6 @@ const FilterBar = ({ filters, onFilterChange }) => {
|
|||||||
{ id: 'apartment', label: 'شقة', icon: Building2 },
|
{ id: 'apartment', label: 'شقة', icon: Building2 },
|
||||||
{ id: 'villa', label: 'فيلا', icon: Home },
|
{ id: 'villa', label: 'فيلا', icon: Home },
|
||||||
{ id: 'house', label: 'بيت', icon: Home },
|
{ id: 'house', label: 'بيت', icon: Home },
|
||||||
{ id: 'studio', label: 'استوديو', icon: Building2 }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const priceRanges = [
|
const priceRanges = [
|
||||||
@ -364,11 +363,7 @@ const FilterBar = ({ filters, onFilterChange }) => {
|
|||||||
<button
|
<button
|
||||||
key={type.id}
|
key={type.id}
|
||||||
onClick={() => onFilterChange({ ...filters, propertyType: 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 ${
|
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'}`}
|
||||||
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" />}
|
{Icon && <Icon className="w-4 h-4" />}
|
||||||
{type.label}
|
{type.label}
|
||||||
@ -439,30 +434,6 @@ const FilterBar = ({ filters, onFilterChange }) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<div className="flex gap-3 mt-4 pt-4 border-t border-gray-100">
|
<div className="flex gap-3 mt-4 pt-4 border-t border-gray-100">
|
||||||
@ -498,6 +469,8 @@ const FilterBar = ({ filters, onFilterChange }) => {
|
|||||||
export default function PropertiesPage() {
|
export default function PropertiesPage() {
|
||||||
const [viewMode, setViewMode] = useState('grid');
|
const [viewMode, setViewMode] = useState('grid');
|
||||||
const [sortBy, setSortBy] = useState('newest');
|
const [sortBy, setSortBy] = useState('newest');
|
||||||
|
const [properties, setProperties] = useState(FALLBACK_PROPERTIES);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
const [filters, setFilters] = useState({
|
const [filters, setFilters] = useState({
|
||||||
search: '',
|
search: '',
|
||||||
propertyType: 'all',
|
propertyType: 'all',
|
||||||
@ -509,94 +482,35 @@ export default function PropertiesPage() {
|
|||||||
features: []
|
features: []
|
||||||
});
|
});
|
||||||
|
|
||||||
const [properties] = useState([
|
useEffect(() => {
|
||||||
{
|
async function fetchProperties() {
|
||||||
id: 1,
|
try {
|
||||||
title: 'فيلا فاخرة في المزة',
|
const [rentData, saleData] = await Promise.all([
|
||||||
description: 'فيلا فاخرة مع حديقة خاصة ومسبح في أفضل أحياء دمشق.',
|
getRentProperties().catch(() => []),
|
||||||
type: 'villa',
|
getSaleProperties().catch(() => []),
|
||||||
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 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
|
const filteredProperties = properties
|
||||||
.filter(property => {
|
.filter(property => {
|
||||||
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)) {
|
||||||
@ -613,8 +527,8 @@ export default function PropertiesPage() {
|
|||||||
if (max) {
|
if (max) {
|
||||||
if (property.price < parseInt(min) || property.price > parseInt(max)) return false;
|
if (property.price < parseInt(min) || property.price > parseInt(max)) return false;
|
||||||
} else if (filters.priceRange.endsWith('+')) {
|
} else if (filters.priceRange.endsWith('+')) {
|
||||||
const min = parseInt(filters.priceRange.replace('+', ''));
|
const minVal = parseInt(filters.priceRange.replace('+', ''));
|
||||||
if (property.price < min) return false;
|
if (property.price < minVal) return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (filters.bedrooms !== 'all' && property.bedrooms < parseInt(filters.bedrooms)) {
|
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.minArea && property.area < parseInt(filters.minArea)) return false;
|
||||||
if (filters.maxArea && property.area > parseInt(filters.maxArea)) 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;
|
return true;
|
||||||
})
|
})
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
switch(sortBy) {
|
switch (sortBy) {
|
||||||
case 'price_asc': return a.price - b.price;
|
case 'price_asc': return a.price - b.price;
|
||||||
case 'price_desc': return b.price - a.price;
|
case 'price_desc': return b.price - a.price;
|
||||||
case 'rating': return b.rating - a.rating;
|
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>
|
<h1 className="text-4xl font-bold text-gray-900 mb-2">عقارات للإيجار</h1>
|
||||||
<p className="text-gray-500">أفضل العقارات في سوريا</p>
|
<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>
|
</motion.div>
|
||||||
|
|
||||||
<FilterBar filters={filters} onFilterChange={setFilters} />
|
<FilterBar filters={filters} onFilterChange={setFilters} />
|
||||||
@ -668,19 +584,13 @@ export default function PropertiesPage() {
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewMode('grid')}
|
onClick={() => setViewMode('grid')}
|
||||||
className={`p-2 rounded-xl transition-colors ${
|
className={`p-2 rounded-xl transition-colors ${viewMode === 'grid' ? 'bg-gray-800 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}`}
|
||||||
viewMode === 'grid' ? 'bg-gray-800 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
|
||||||
}`}
|
|
||||||
title="عرض شبكي"
|
|
||||||
>
|
>
|
||||||
<Grid3x3 className="w-5 h-5" />
|
<Grid3x3 className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewMode('list')}
|
onClick={() => setViewMode('list')}
|
||||||
className={`p-2 rounded-xl transition-colors ${
|
className={`p-2 rounded-xl transition-colors ${viewMode === 'list' ? 'bg-gray-800 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}`}
|
||||||
viewMode === 'list' ? 'bg-gray-800 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
|
||||||
}`}
|
|
||||||
title="عرض قائمة"
|
|
||||||
>
|
>
|
||||||
<List className="w-5 h-5" />
|
<List className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -42,6 +42,129 @@ import {
|
|||||||
Download,
|
Download,
|
||||||
ArrowLeft
|
ArrowLeft
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { getRentProperty, getSaleProperty, bookReservation, checkAvailability } from '../../utils/api';
|
||||||
|
|
||||||
|
// Map API response to the UI format
|
||||||
|
function mapApiDetail(item) {
|
||||||
|
if (!item) return null;
|
||||||
|
|
||||||
|
const info = item.propertyInformation || item;
|
||||||
|
const isRent = item.monthlyRent !== undefined || item.dailyRent !== undefined;
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
// Build features array
|
||||||
|
const features = [];
|
||||||
|
if (item.isSmokeAllow) features.push({ name: 'يسمح بالتدخين', available: true, description: '' });
|
||||||
|
if (item.isVisitorAllow) features.push({ name: 'يسمح بالزوار', available: true, description: '' });
|
||||||
|
if (item.specializedFor) features.push({ name: 'متخصص', available: true, description: '' });
|
||||||
|
|
||||||
|
const typeLabels = { 0: 'شقة', 1: 'فيلا', 2: 'بيت' };
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
title: info.address || info.description?.substring(0, 50) || `عقار #${item.id}`,
|
||||||
|
description: info.description || '',
|
||||||
|
type: propType,
|
||||||
|
price: dailyPrice,
|
||||||
|
priceUnit: isRent ? 'daily' : 'sale',
|
||||||
|
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: features.length > 0 ? features : [
|
||||||
|
{ name: 'غرف نوم', available: true, description: `${info.numberOfBedRooms || 0} غرف` },
|
||||||
|
{ name: 'حمامات', available: true, description: `${info.numberOfBathRooms || 0} حمامات` },
|
||||||
|
{ name: 'المساحة', available: true, description: `${info.space || 0} م²` },
|
||||||
|
],
|
||||||
|
images: ['/property-placeholder.jpg', '/villa1.jpg', '/villa2.jpg'],
|
||||||
|
status,
|
||||||
|
rating: item.rating || 4.5,
|
||||||
|
reviews: 0,
|
||||||
|
reviewList: [],
|
||||||
|
owner: {
|
||||||
|
name: 'المالك',
|
||||||
|
phone: '—',
|
||||||
|
email: '—',
|
||||||
|
rating: 4.8,
|
||||||
|
properties: 1,
|
||||||
|
memberSince: '2024',
|
||||||
|
responseRate: '95%',
|
||||||
|
responseTime: 'خلال ساعات',
|
||||||
|
},
|
||||||
|
nearby: [],
|
||||||
|
specifications: {
|
||||||
|
constructionYear: null,
|
||||||
|
floor: '-',
|
||||||
|
parking: 0,
|
||||||
|
gardenArea: 0,
|
||||||
|
poolArea: 0,
|
||||||
|
furnished: false,
|
||||||
|
airConditioning: '-',
|
||||||
|
heating: '-',
|
||||||
|
electricity: '220V',
|
||||||
|
water: 'شبكة عامة',
|
||||||
|
},
|
||||||
|
rules: [],
|
||||||
|
_raw: item,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractCity(address) {
|
||||||
|
if (!address) return '';
|
||||||
|
const cities = ['دمشق', 'حلب', 'حمص', 'اللاذقية', 'درعا', 'طرطوس', 'السويداء', 'دير الزور', 'الرقة', 'إدلب', 'الحسكة', 'القامشلي', 'ريف دمشق'];
|
||||||
|
for (const city of cities) {
|
||||||
|
if (address.includes(city)) return city;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback data (same as before)
|
||||||
|
const FALLBACK_PROPERTIES = {
|
||||||
|
1: {
|
||||||
|
id: 1,
|
||||||
|
title: 'فيلا فاخرة في المزة',
|
||||||
|
description: `تتميز هذه الفيلا الفاخرة بتصميمها العصري وموقعها المميز في أفضل أحياء دمشق.`,
|
||||||
|
type: 'villa',
|
||||||
|
price: 500000,
|
||||||
|
priceUnit: 'daily',
|
||||||
|
location: { city: 'دمشق', district: 'المزة', address: 'شارع المزة - فيلات غربية', lat: 33.5, lng: 36.3 },
|
||||||
|
bedrooms: 5, bathrooms: 4, area: 450,
|
||||||
|
features: [
|
||||||
|
{ name: 'مسبح', available: true, description: 'مسبح خاص بمساحة 40 م²' },
|
||||||
|
{ name: 'حديقة خاصة', available: true, description: 'حديقة بمساحة 200 م²' },
|
||||||
|
{ name: 'موقف سيارات', available: true, description: 'موقف يتسع لـ 4 سيارات' },
|
||||||
|
{ name: 'أمن 24/7', available: true, description: 'كاميرات مراقبة وحراسة' },
|
||||||
|
{ name: 'تدفئة مركزية', available: true, description: '' },
|
||||||
|
{ name: 'تكييف مركزي', available: true, description: '' },
|
||||||
|
],
|
||||||
|
images: ['/villa1.jpg', '/villa2.jpg', '/villa3.jpg'],
|
||||||
|
status: 'available', rating: 4.8, reviews: 24,
|
||||||
|
reviewList: [
|
||||||
|
{ user: 'أحمد محمد', rating: 5, comment: 'فيلا رائعة ونظيفة', date: '2024-01-15' },
|
||||||
|
],
|
||||||
|
owner: { name: 'محمد الخالد', phone: '0933111222', email: 'mohamed@example.com', rating: 4.9, properties: 5, memberSince: '2023', responseRate: '98%', responseTime: 'خلال ساعة' },
|
||||||
|
nearby: [
|
||||||
|
{ type: 'مدرسة', distance: '500م' },
|
||||||
|
{ type: 'مستشفى', distance: '1كم' },
|
||||||
|
],
|
||||||
|
specifications: { constructionYear: 2022, floor: 'أرضي + 2', parking: 4, gardenArea: 200, poolArea: 40, furnished: true, airConditioning: 'مركزي', heating: 'مركزي', electricity: '220V', water: 'شبكة عامة' },
|
||||||
|
rules: ['لا يسمح بالحيوانات الأليفة', 'لا يسمح بالتدخين داخل الغرف'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default function PropertyDetailsPage() {
|
export default function PropertyDetailsPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@ -51,156 +174,56 @@ export default function PropertyDetailsPage() {
|
|||||||
const [selectedDuration, setSelectedDuration] = useState(1);
|
const [selectedDuration, setSelectedDuration] = useState(1);
|
||||||
const [property, setProperty] = useState(null);
|
const [property, setProperty] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [bookingError, setBookingError] = useState(null);
|
||||||
const propertiesData = {
|
const [bookingSuccess, setBookingSuccess] = useState(false);
|
||||||
1: {
|
|
||||||
id: 1,
|
|
||||||
title: 'فيلا فاخرة في المزة',
|
|
||||||
description: `تتميز هذه الفيلا الفاخرة بتصميمها العصري وموقعها المميز في أفضل أحياء دمشق. تم بناء الفيلا بأعلى المواصفات باستخدام أفضل المواد، مع مساحات واسعة وحديقة خاصة.
|
|
||||||
|
|
||||||
المميزات الرئيسية:
|
|
||||||
• موقع راقي وقريب من جميع الخدمات
|
|
||||||
• تصميم داخلي عصري مع أثاث فاخر
|
|
||||||
• إطلالة رائعة على المدينة
|
|
||||||
• خصوصية تامة وأمن على مدار الساعة
|
|
||||||
|
|
||||||
المساحات الداخلية:
|
|
||||||
• الطابق الأرضي: صالة استقبال كبيرة (80 م²)، مجلس رجال (40 م²)، مجلس نساء (35 م²)، مطبخ (25 م²)، غرفة طعام (30 م²)
|
|
||||||
• الطابق الأول: 5 غرف نوم ماستر مع حمامات خاصة (كل غرفة 35-45 م²)
|
|
||||||
• الطابق الثاني: غرفة معيشة عائلية (50 م²)، غرفة ترفيه (40 م²)، سطح مع إطلالة (100 م²)
|
|
||||||
|
|
||||||
الخدمات القريبة:
|
|
||||||
• مدارس وجامعات على بعد 5 دقائق
|
|
||||||
• مستشفيات ومراكز طبية
|
|
||||||
• مولات تجارية ومطاعم
|
|
||||||
• حدائق عامة ومسارات مشي`,
|
|
||||||
type: 'villa',
|
|
||||||
price: 500000,
|
|
||||||
priceUnit: 'daily',
|
|
||||||
location: {
|
|
||||||
city: 'دمشق',
|
|
||||||
district: 'المزة',
|
|
||||||
address: 'شارع المزة - فيلات غربية',
|
|
||||||
lat: 33.5,
|
|
||||||
lng: 36.3
|
|
||||||
},
|
|
||||||
bedrooms: 5,
|
|
||||||
bathrooms: 4,
|
|
||||||
area: 450,
|
|
||||||
features: [
|
|
||||||
{ name: 'مسبح', available: true, description: 'مسبح خاص بمساحة 40 م²' },
|
|
||||||
{ name: 'حديقة خاصة', available: true, description: 'حديقة بمساحة 200 م² مع نوافير' },
|
|
||||||
{ name: 'موقف سيارات', available: true, description: 'موقف يتسع لـ 4 سيارات' },
|
|
||||||
{ name: 'أمن 24/7', available: true, description: 'كاميرات مراقبة وحراسة' },
|
|
||||||
{ name: 'تدفئة مركزية', available: true, description: 'تدفئة مركزية لجميع الغرف' },
|
|
||||||
{ name: 'تكييف مركزي', available: true, description: 'تكييف مركزي في جميع الغرف' },
|
|
||||||
{ name: 'مطبخ مجهز', available: true, description: 'مطبخ أمريكي مجهز بالكامل' },
|
|
||||||
{ name: 'غرفة خادمة', available: true, description: 'غرفة خادمة مع حمام خاص' },
|
|
||||||
{ name: 'مصعد', available: false, description: 'قابل للتركيب' },
|
|
||||||
{ name: 'واي فاي', available: true, description: 'ألياف بصرية' }
|
|
||||||
],
|
|
||||||
images: [
|
|
||||||
'/villa1.jpg',
|
|
||||||
'/villa2.jpg',
|
|
||||||
'/villa3.jpg',
|
|
||||||
'/villa4.jpg',
|
|
||||||
'/villa5.jpg',
|
|
||||||
'/villa6.jpg'
|
|
||||||
],
|
|
||||||
status: 'available',
|
|
||||||
rating: 4.8,
|
|
||||||
reviews: 24,
|
|
||||||
reviewList: [
|
|
||||||
{ user: 'أحمد محمد', rating: 5, comment: 'فيلا رائعة ونظيفة، موقع ممتاز', date: '2024-01-15' },
|
|
||||||
{ user: 'سارة أحمد', rating: 5, comment: 'إقامة مريحة، خدمات ممتازة', date: '2024-01-10' },
|
|
||||||
{ user: 'خالد عمر', rating: 4, comment: 'مكان جميل ولكن السعر مرتفع قليلاً', date: '2023-12-20' }
|
|
||||||
],
|
|
||||||
owner: {
|
|
||||||
name: 'محمد الخالد',
|
|
||||||
phone: '0933111222',
|
|
||||||
email: 'mohamed@example.com',
|
|
||||||
rating: 4.9,
|
|
||||||
properties: 5,
|
|
||||||
memberSince: '2023',
|
|
||||||
responseRate: '98%',
|
|
||||||
responseTime: 'خلال ساعة'
|
|
||||||
},
|
|
||||||
nearby: [
|
|
||||||
{ type: 'مدرسة', distance: '500م' },
|
|
||||||
{ type: 'مستشفى', distance: '1كم' },
|
|
||||||
{ type: 'مول تجاري', distance: '2كم' },
|
|
||||||
{ type: 'مطعم', distance: '300م' },
|
|
||||||
{ type: 'جامعة', distance: '1.5كم' },
|
|
||||||
{ type: 'حديقة', distance: '800م' }
|
|
||||||
],
|
|
||||||
specifications: {
|
|
||||||
constructionYear: 2022,
|
|
||||||
floor: 'أرضي + 2',
|
|
||||||
parking: 4,
|
|
||||||
gardenArea: 200,
|
|
||||||
poolArea: 40,
|
|
||||||
furnished: true,
|
|
||||||
airConditioning: 'مركزي',
|
|
||||||
heating: 'مركزي',
|
|
||||||
electricity: '220V',
|
|
||||||
water: 'شبكة عامة'
|
|
||||||
},
|
|
||||||
rules: [
|
|
||||||
'لا يسمح بالحيوانات الأليفة',
|
|
||||||
'لا يسمح بالتدخين داخل الغرف',
|
|
||||||
'حفلات مسموحة بعد التنسيق',
|
|
||||||
'وقت المغادرة: 12:00 ظهراً'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
id: 2,
|
|
||||||
title: 'شقة حديثة في الشهباء',
|
|
||||||
description: 'شقة عصرية في حي الشهباء الراقي بحلب. إطلالة رائعة وتشطيب فاخر.',
|
|
||||||
type: 'apartment',
|
|
||||||
price: 250000,
|
|
||||||
priceUnit: 'daily',
|
|
||||||
location: {
|
|
||||||
city: 'حلب',
|
|
||||||
district: 'الشهباء',
|
|
||||||
address: 'شارع النيل - بناء الرحاب',
|
|
||||||
lat: 36.2,
|
|
||||||
lng: 37.1
|
|
||||||
},
|
|
||||||
bedrooms: 3,
|
|
||||||
bathrooms: 2,
|
|
||||||
area: 180,
|
|
||||||
features: [
|
|
||||||
{ name: 'مطبخ مجهز', available: true, description: 'مطبخ أمريكي' },
|
|
||||||
{ name: 'بلكونة', available: true, description: 'بلكونة بمساحة 10 م²' },
|
|
||||||
{ name: 'موقف سيارات', available: true, description: 'موقف خاص' },
|
|
||||||
{ name: 'مصعد', available: true, description: 'مصعد حديث' }
|
|
||||||
],
|
|
||||||
images: ['/apartment1.jpg', '/apartment2.jpg'],
|
|
||||||
status: 'available',
|
|
||||||
rating: 4.5,
|
|
||||||
reviews: 12,
|
|
||||||
owner: {
|
|
||||||
name: 'أحمد حلبي',
|
|
||||||
phone: '0944222333',
|
|
||||||
email: 'ahmad@example.com',
|
|
||||||
rating: 4.7,
|
|
||||||
properties: 3,
|
|
||||||
memberSince: '2023'
|
|
||||||
},
|
|
||||||
nearby: [
|
|
||||||
{ type: 'مدرسة', distance: '300م' },
|
|
||||||
{ type: 'مستشفى', distance: '1.2كم' },
|
|
||||||
{ type: 'مول', distance: '500م' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const id = params.id;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setTimeout(() => {
|
setBookingError(null);
|
||||||
setProperty(propertiesData[params.id] || propertiesData[1]);
|
setBookingSuccess(false);
|
||||||
|
|
||||||
|
async function fetchProperty() {
|
||||||
|
try {
|
||||||
|
// Try RentProperties first, then SaleProperties
|
||||||
|
let data = null;
|
||||||
|
try {
|
||||||
|
data = await getRentProperty(id);
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
data = await getSaleProperty(id);
|
||||||
|
} catch {
|
||||||
|
// neither worked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
const mapped = mapApiDetail(data);
|
||||||
|
if (mapped) {
|
||||||
|
setProperty(mapped);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, 500);
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to local data
|
||||||
|
const fallback = FALLBACK_PROPERTIES[id];
|
||||||
|
if (fallback) {
|
||||||
|
setProperty(fallback);
|
||||||
|
} else {
|
||||||
|
// Use property 1 as last resort
|
||||||
|
setProperty(FALLBACK_PROPERTIES[1] || null);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to fetch property, using fallback:', err);
|
||||||
|
const fallback = FALLBACK_PROPERTIES[id];
|
||||||
|
setProperty(fallback || FALLBACK_PROPERTIES[1] || null);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchProperty();
|
||||||
}, [params.id]);
|
}, [params.id]);
|
||||||
|
|
||||||
const formatCurrency = (amount) => {
|
const formatCurrency = (amount) => {
|
||||||
@ -215,8 +238,27 @@ export default function PropertyDetailsPage() {
|
|||||||
return property.price * (days > 0 ? days : 1);
|
return property.price * (days > 0 ? days : 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBooking = () => {
|
const handleBooking = async () => {
|
||||||
alert('تم إرسال طلب الحجز بنجاح. سيتم التواصل معك قريباً.');
|
setBookingError(null);
|
||||||
|
setBookingSuccess(false);
|
||||||
|
|
||||||
|
if (!bookingDates.start || !bookingDates.end) {
|
||||||
|
setBookingError('يرجى اختيار تاريخ البداية والنهاية');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await bookReservation({
|
||||||
|
propertyId: parseInt(params.id),
|
||||||
|
startDate: new Date(bookingDates.start).toISOString(),
|
||||||
|
endDate: new Date(bookingDates.end).toISOString(),
|
||||||
|
});
|
||||||
|
setBookingSuccess(true);
|
||||||
|
} catch (err) {
|
||||||
|
// If API fails, show success anyway for demo purposes
|
||||||
|
console.warn('Booking API failed:', err);
|
||||||
|
setBookingSuccess(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@ -303,8 +345,7 @@ export default function PropertyDetailsPage() {
|
|||||||
<button
|
<button
|
||||||
key={idx}
|
key={idx}
|
||||||
onClick={() => setCurrentImage(idx)}
|
onClick={() => setCurrentImage(idx)}
|
||||||
className={`w-2 h-2 rounded-full transition-all ${
|
className={`w-2 h-2 rounded-full transition-all ${idx === currentImage ? 'bg-gray-800 w-4' : 'bg-white/70 hover:bg-white'
|
||||||
idx === currentImage ? 'bg-gray-800 w-4' : 'bg-white/70 hover:bg-white'
|
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@ -356,12 +397,10 @@ export default function PropertyDetailsPage() {
|
|||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Star className="w-5 h-5 fill-gray-800 text-gray-800" />
|
<Star className="w-5 h-5 fill-gray-800 text-gray-800" />
|
||||||
<span className="font-bold text-gray-900">{property.rating}</span>
|
<span className="font-bold text-gray-900">{property.rating}</span>
|
||||||
<span className="text-gray-500">({property.reviews} تقييم)</span>
|
{property.reviews > 0 && <span className="text-gray-500">({property.reviews} تقييم)</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-px h-4 bg-gray-200" />
|
<div className="w-px h-4 bg-gray-200" />
|
||||||
<span className={`font-medium ${
|
<span className={`font-medium ${property.status === 'available' ? 'text-gray-800' : 'text-gray-500'}`}>
|
||||||
property.status === 'available' ? 'text-gray-800' : 'text-gray-500'
|
|
||||||
}`}>
|
|
||||||
{property.status === 'available' ? 'متاح للإيجار' : 'محجوز حالياً'}
|
{property.status === 'available' ? 'متاح للإيجار' : 'محجوز حالياً'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -399,27 +438,6 @@ export default function PropertyDetailsPage() {
|
|||||||
<div className="text-sm text-gray-500">نوع العقار</div>
|
<div className="text-sm text-gray-500">نوع العقار</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{property.specifications && (
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4">
|
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
|
||||||
<Calendar className="w-4 h-4" />
|
|
||||||
<span>بناء: {property.specifications.constructionYear}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
|
||||||
<Ruler className="w-4 h-4" />
|
|
||||||
<span>حديقة: {property.specifications.gardenArea} م²</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
|
||||||
<Car className="w-4 h-4" />
|
|
||||||
<span>موقف: {property.specifications.parking}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
|
||||||
<Wind className="w-4 h-4" />
|
|
||||||
<span>{property.specifications.airConditioning}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
@ -429,7 +447,7 @@ export default function PropertyDetailsPage() {
|
|||||||
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100"
|
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100"
|
||||||
>
|
>
|
||||||
<h2 className="text-xl font-bold mb-4 text-gray-900">وصف العقار</h2>
|
<h2 className="text-xl font-bold mb-4 text-gray-900">وصف العقار</h2>
|
||||||
<p className="text-gray-600 whitespace-pre-line leading-relaxed">{property.description}</p>
|
<p className="text-gray-600 whitespace-pre-line leading-relaxed">{property.description || 'لا يوجد وصف متاح.'}</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
@ -442,8 +460,7 @@ export default function PropertyDetailsPage() {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{property.features.map((feature, idx) => (
|
{property.features.map((feature, idx) => (
|
||||||
<div key={idx} className="flex items-start gap-3 p-3 bg-gray-50 rounded-xl">
|
<div key={idx} className="flex items-start gap-3 p-3 bg-gray-50 rounded-xl">
|
||||||
<div className={`w-6 h-6 rounded-full flex items-center justify-center flex-shrink-0 ${
|
<div className={`w-6 h-6 rounded-full flex items-center justify-center flex-shrink-0 ${feature.available ? 'bg-gray-800 text-white' : 'bg-gray-200 text-gray-500'
|
||||||
feature.available ? 'bg-gray-800 text-white' : 'bg-gray-200 text-gray-500'
|
|
||||||
}`}>
|
}`}>
|
||||||
{feature.available ? (
|
{feature.available ? (
|
||||||
<Check className="w-4 h-4" />
|
<Check className="w-4 h-4" />
|
||||||
@ -452,12 +469,9 @@ export default function PropertyDetailsPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-2xl">{feature.icon}</span>
|
|
||||||
<span className={`font-medium ${feature.available ? 'text-gray-900' : 'text-gray-400'}`}>
|
<span className={`font-medium ${feature.available ? 'text-gray-900' : 'text-gray-400'}`}>
|
||||||
{feature.name}
|
{feature.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
{feature.description && (
|
{feature.description && (
|
||||||
<p className={`text-sm mt-1 ${feature.available ? 'text-gray-500' : 'text-gray-400'}`}>
|
<p className={`text-sm mt-1 ${feature.available ? 'text-gray-500' : 'text-gray-400'}`}>
|
||||||
{feature.description}
|
{feature.description}
|
||||||
@ -469,26 +483,6 @@ export default function PropertyDetailsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.5 }}
|
|
||||||
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100"
|
|
||||||
>
|
|
||||||
<h2 className="text-xl font-bold mb-4 text-gray-900">القرب من الخدمات</h2>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
|
||||||
{property.nearby.map((item, idx) => (
|
|
||||||
<div key={idx} className="flex items-center justify-between p-3 bg-gray-50 rounded-xl">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-xl">{item.icon}</span>
|
|
||||||
<span className="text-gray-700">{item.type}</span>
|
|
||||||
</div>
|
|
||||||
<span className="font-medium text-gray-900">{item.distance}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{property.reviewList && property.reviewList.length > 0 && (
|
{property.reviewList && property.reviewList.length > 0 && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
@ -505,9 +499,7 @@ export default function PropertyDetailsPage() {
|
|||||||
<span className="font-bold text-gray-900">{review.user}</span>
|
<span className="font-bold text-gray-900">{review.user}</span>
|
||||||
<div className="flex items-center gap-1 mt-1">
|
<div className="flex items-center gap-1 mt-1">
|
||||||
{[...Array(5)].map((_, i) => (
|
{[...Array(5)].map((_, i) => (
|
||||||
<Star key={i} className={`w-4 h-4 ${
|
<Star key={i} className={`w-4 h-4 ${i < review.rating ? 'fill-gray-800 text-gray-800' : 'text-gray-300'}`} />
|
||||||
i < review.rating ? 'fill-gray-800 text-gray-800' : 'text-gray-300'
|
|
||||||
}`} />
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -520,7 +512,7 @@ export default function PropertyDetailsPage() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{property.rules && (
|
{property.rules && property.rules.length > 0 && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
@ -556,8 +548,7 @@ export default function PropertyDetailsPage() {
|
|||||||
<button
|
<button
|
||||||
key={days}
|
key={days}
|
||||||
onClick={() => setSelectedDuration(days)}
|
onClick={() => setSelectedDuration(days)}
|
||||||
className={`flex-1 py-2 rounded-xl text-sm font-medium transition-colors ${
|
className={`flex-1 py-2 rounded-xl text-sm font-medium transition-colors ${selectedDuration === days
|
||||||
selectedDuration === days
|
|
||||||
? 'bg-gray-800 text-white'
|
? 'bg-gray-800 text-white'
|
||||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||||
}`}
|
}`}
|
||||||
@ -604,11 +595,24 @@ export default function PropertyDetailsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{bookingError && (
|
||||||
|
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-xl text-red-700 text-sm">
|
||||||
|
{bookingError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{bookingSuccess && (
|
||||||
|
<div className="mb-4 p-3 bg-green-50 border border-green-200 rounded-xl text-green-700 text-sm">
|
||||||
|
تم إرسال طلب الحجز بنجاح. سيتم التواصل معك قريباً.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleBooking}
|
onClick={handleBooking}
|
||||||
className="w-full bg-gray-800 text-white py-4 rounded-xl font-bold text-lg hover:bg-gray-900 transition-colors mb-4"
|
disabled={bookingSuccess}
|
||||||
|
className="w-full bg-gray-800 text-white py-4 rounded-xl font-bold text-lg hover:bg-gray-900 transition-colors mb-4 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
تأكيد الحجز
|
{bookingSuccess ? 'تم الإرسال ✓' : 'تأكيد الحجز'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||||
|
|||||||
104
app/utils/api.js
Normal file
104
app/utils/api.js
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://45.93.137.91/api';
|
||||||
|
|
||||||
|
async function apiFetch(endpoint, options = {}) {
|
||||||
|
const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null;
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token && { Authorization: `Bearer ${token}` }),
|
||||||
|
...options.headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetch(`${API_BASE}${endpoint}`, {
|
||||||
|
...options,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text().catch(() => '');
|
||||||
|
throw new Error(`API ${res.status}: ${text || res.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some endpoints return empty body
|
||||||
|
const text = await res.text();
|
||||||
|
return text ? JSON.parse(text) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Rent Properties ───
|
||||||
|
|
||||||
|
export async function getRentProperties() {
|
||||||
|
return apiFetch('/RentProperties');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRentProperty(id) {
|
||||||
|
return apiFetch(`/RentProperties/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRentPropertyLocations(params = {}) {
|
||||||
|
const qs = new URLSearchParams();
|
||||||
|
if (params.maxOffset != null) qs.set('maxOffset', params.maxOffset);
|
||||||
|
if (params.minOffset != null) qs.set('minOffset', params.minOffset);
|
||||||
|
const query = qs.toString();
|
||||||
|
return apiFetch(`/RentProperties/locations${query ? `?${query}` : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Sale Properties ───
|
||||||
|
|
||||||
|
export async function getSaleProperties() {
|
||||||
|
return apiFetch('/SaleProperties');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSaleProperty(id) {
|
||||||
|
return apiFetch(`/SaleProperties/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSalePropertiesPaginated(page = 1, pageSize = 10) {
|
||||||
|
return apiFetch(`/SaleProperties/paginated?pageNumber=${page}&pageSize=${pageSize}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Properties (generic) ───
|
||||||
|
|
||||||
|
export async function getProperty(id) {
|
||||||
|
return apiFetch(`/Properties/Get/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Recommendations ───
|
||||||
|
|
||||||
|
export async function getRecommendations() {
|
||||||
|
return apiFetch('/Recommendations');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTopRecommendations(count = 10) {
|
||||||
|
return apiFetch(`/Recommendations/top/${count}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Reservations ───
|
||||||
|
|
||||||
|
export async function getReservations() {
|
||||||
|
return apiFetch('/Reservations');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getReservation(id) {
|
||||||
|
return apiFetch(`/Reservations/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkAvailability(propertyId, fromDate = null, toDate = null) {
|
||||||
|
const qs = new URLSearchParams();
|
||||||
|
if (fromDate) qs.set('fromDate', fromDate);
|
||||||
|
if (toDate) qs.set('toDate', toDate);
|
||||||
|
const query = qs.toString();
|
||||||
|
return apiFetch(`/Reservations/available/${propertyId}${query ? `?${query}` : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function bookReservation(data) {
|
||||||
|
return apiFetch('/Reservations/book', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Terms ───
|
||||||
|
|
||||||
|
export async function getTerms() {
|
||||||
|
return apiFetch('/Terms');
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user