Added API to map with markers in main page
All checks were successful
Build frontend / build (push) Successful in 53s
All checks were successful
Build frontend / build (push) Successful in 53s
This commit is contained in:
100
app/components/PropertyMapWithMarkers.js
Normal file
100
app/components/PropertyMapWithMarkers.js
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import L from 'leaflet';
|
||||||
|
import 'leaflet/dist/leaflet.css';
|
||||||
|
|
||||||
|
delete L.Icon.Default.prototype._getIconUrl;
|
||||||
|
L.Icon.Default.mergeOptions({
|
||||||
|
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
|
||||||
|
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
|
||||||
|
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function PropertyMapWithMarkers({ properties = [], onPropertyClick }) {
|
||||||
|
const mapRef = useRef(null);
|
||||||
|
const mapInstanceRef = useRef(null);
|
||||||
|
const markersRef = useRef([]);
|
||||||
|
const [mapLoaded, setMapLoaded] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mapRef.current || mapInstanceRef.current) return;
|
||||||
|
|
||||||
|
const map = L.map(mapRef.current).setView([33.5138, 38.9968], 7);
|
||||||
|
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||||
|
maxZoom: 19,
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
mapInstanceRef.current = map;
|
||||||
|
setMapLoaded(true);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (mapInstanceRef.current) {
|
||||||
|
mapInstanceRef.current.remove();
|
||||||
|
mapInstanceRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mapInstanceRef.current || !mapLoaded) return;
|
||||||
|
|
||||||
|
markersRef.current.forEach(marker => marker.remove());
|
||||||
|
markersRef.current = [];
|
||||||
|
|
||||||
|
properties.forEach(property => {
|
||||||
|
if (property.lat && property.lng) {
|
||||||
|
const marker = L.marker([property.lat, property.lng]).addTo(mapInstanceRef.current);
|
||||||
|
|
||||||
|
const popupContent = `
|
||||||
|
<div dir="rtl" style="text-align: right; padding: 12px; max-width: 250px;">
|
||||||
|
<h3 style="font-weight: bold; font-size: 16px; margin-bottom: 8px; color: #111;">${property.title || 'عقار'}</h3>
|
||||||
|
<p style="font-size: 14px; color: #666; margin-bottom: 8px;">${property.address || property.location?.address || ''}</p>
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
||||||
|
<span style="font-weight: bold; font-size: 18px; color: #d97706;">${formatPrice(property)}</span>
|
||||||
|
</div>
|
||||||
|
${property.images && property.images.length > 0 ? `<img src="${property.images[0]}" alt="${property.title}" style="width: 100%; height: 96px; object-fit: cover; border-radius: 8px; margin-bottom: 8px;" onerror="this.src='/property-placeholder.jpg'" />` : ''}
|
||||||
|
<div style="font-size: 12px; color: #888;">
|
||||||
|
${property.type ? `<p>النوع: ${property.type}</p>` : ''}
|
||||||
|
${property.bedrooms > 0 ? `<p>غرف نوم: ${property.bedrooms}</p>` : ''}
|
||||||
|
${property.bathrooms > 0 ? `<p>حمامات: ${property.bathrooms}</p>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
marker.bindPopup(popupContent);
|
||||||
|
|
||||||
|
marker.on('click', () => {
|
||||||
|
if (onPropertyClick) {
|
||||||
|
onPropertyClick(property);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
markersRef.current.push(marker);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (markersRef.current.length > 0) {
|
||||||
|
const group = L.featureGroup(markersRef.current);
|
||||||
|
mapInstanceRef.current.fitBounds(group.getBounds(), { padding: [50, 100] });
|
||||||
|
}
|
||||||
|
}, [properties, mapLoaded]);
|
||||||
|
|
||||||
|
const formatPrice = (property) => {
|
||||||
|
if (property.priceUnit === 'monthly') {
|
||||||
|
return `${property.price?.toLocaleString() || 0} ل.س/شهر`;
|
||||||
|
} else if (property.priceUnit === 'daily') {
|
||||||
|
return `${property.price?.toLocaleString() || 0} ل.س/يوم`;
|
||||||
|
} else {
|
||||||
|
return `${property.price?.toLocaleString() || 0} ل.س`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-full h-[600px] rounded-xl overflow-hidden border-2 border-gray-200">
|
||||||
|
<div ref={mapRef} className="w-full h-full z-0" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -67,7 +67,9 @@ export default function HeroSearch({ onSearch, isAuthenticated }) {
|
|||||||
setActiveTab(tab);
|
setActiveTab(tab);
|
||||||
if ((tab === 'rent' || tab === 'sell') && !isAuthenticated) {
|
if ((tab === 'rent' || tab === 'sell') && !isAuthenticated) {
|
||||||
setShowLoginDialog(true);
|
setShowLoginDialog(true);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
handleSearch();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
|
|||||||
70
app/page.js
70
app/page.js
@ -26,15 +26,13 @@ import {
|
|||||||
MessageCircle
|
MessageCircle
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import HeroSearch from './components/home/HeroSearch';
|
import HeroSearch from './components/home/HeroSearch';
|
||||||
import PropertyMap from './components/home/PropertyMap';
|
import PropertyMapWithMarkers from './components/PropertyMapWithMarkers';
|
||||||
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';
|
import { getRentProperties, getSaleProperties } from './utils/api';
|
||||||
import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from './enums';
|
import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from './enums';
|
||||||
import AuthService from './services/AuthService';
|
import AuthService from './services/AuthService';
|
||||||
|
|
||||||
// Map API property data to the format the UI expects
|
|
||||||
// API returns { propertyInformationId, deposit, monthlyRent, dailyRent, rating, propertyInformation: {...}, ... }
|
|
||||||
function mapApiProperty(item, index) {
|
function mapApiProperty(item, index) {
|
||||||
const info = item.propertyInformation || {};
|
const info = item.propertyInformation || {};
|
||||||
|
|
||||||
@ -56,7 +54,6 @@ function mapApiProperty(item, index) {
|
|||||||
if (info.numberOfBedRooms) features.push(`${info.numberOfBedRooms} غرف نوم`);
|
if (info.numberOfBedRooms) features.push(`${info.numberOfBedRooms} غرف نوم`);
|
||||||
if (info.numberOfBathRooms) features.push(`${info.numberOfBathRooms} حمامات`);
|
if (info.numberOfBathRooms) features.push(`${info.numberOfBathRooms} حمامات`);
|
||||||
|
|
||||||
// Extract images from API and build full URLs
|
|
||||||
const apiBase = typeof window !== 'undefined' ? (process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api') : '';
|
const apiBase = typeof window !== 'undefined' ? (process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api') : '';
|
||||||
const rawImages = Array.isArray(info.images) ? info.images : [];
|
const rawImages = Array.isArray(info.images) ? info.images : [];
|
||||||
const images = rawImages.length > 0
|
const images = rawImages.length > 0
|
||||||
@ -105,10 +102,6 @@ function mapApiProperty(item, index) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractCity is now imported from @/app/enums
|
|
||||||
|
|
||||||
// API-only — no fallback data
|
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const mapSectionRef = useRef(null);
|
const mapSectionRef = useRef(null);
|
||||||
const [searchFilters, setSearchFilters] = useState(null);
|
const [searchFilters, setSearchFilters] = useState(null);
|
||||||
@ -121,9 +114,10 @@ export default function HomePage() {
|
|||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
const [allProperties, setAllProperties] = useState([]);
|
const [allProperties, setAllProperties] = useState([]);
|
||||||
|
const [rentProperties, setRentProperties] = useState([]);
|
||||||
|
const [saleProperties, setSaleProperties] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
// Re-read user from JWT on every route change
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const authUser = AuthService.getUser();
|
const authUser = AuthService.getUser();
|
||||||
if (authUser) {
|
if (authUser) {
|
||||||
@ -137,7 +131,6 @@ export default function HomePage() {
|
|||||||
}
|
}
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
// Fetch properties from API on mount
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
async function fetchProperties() {
|
async function fetchProperties() {
|
||||||
@ -150,15 +143,12 @@ export default function HomePage() {
|
|||||||
const rentList = Array.isArray(rentData) ? rentData : [];
|
const rentList = Array.isArray(rentData) ? rentData : [];
|
||||||
const saleList = Array.isArray(saleData) ? saleData : [];
|
const saleList = Array.isArray(saleData) ? saleData : [];
|
||||||
|
|
||||||
const mapped = [
|
const mappedRent = rentList.map((p, i) => mapApiProperty(p, i));
|
||||||
...rentList.map((p, i) => mapApiProperty(p, i)),
|
const mappedSale = saleList.map((p, i) => mapApiProperty(p, rentList.length + i));
|
||||||
...saleList.map((p, i) => mapApiProperty(p, rentList.length + i)),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (mapped.length > 0) {
|
setRentProperties(mappedRent);
|
||||||
setAllProperties(mapped);
|
setSaleProperties(mappedSale);
|
||||||
}
|
setAllProperties([...mappedRent, ...mappedSale]);
|
||||||
// If API returns empty, keep fallback
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Home] Failed to fetch properties:', err);
|
console.error('[Home] Failed to fetch properties:', err);
|
||||||
} finally {
|
} finally {
|
||||||
@ -170,14 +160,10 @@ export default function HomePage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event) => {
|
if (searchFilters) {
|
||||||
if (menuRef.current && !menuRef.current.contains(event.target)) {
|
applyFilters(searchFilters);
|
||||||
setShowUserMenu(false);
|
}
|
||||||
}
|
}, [rentProperties, saleProperties, searchFilters]);
|
||||||
};
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
|
||||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
AuthService.deleteToken();
|
AuthService.deleteToken();
|
||||||
@ -188,17 +174,16 @@ export default function HomePage() {
|
|||||||
const applyFilters = (filters) => {
|
const applyFilters = (filters) => {
|
||||||
setSearchFilters(filters);
|
setSearchFilters(filters);
|
||||||
|
|
||||||
const filtered = allProperties.filter(property => {
|
let propertiesToFilter = [];
|
||||||
if (filters.mode === 'rent' && property.listingType !== 'rent') {
|
if (filters.mode === 'rent') {
|
||||||
return false;
|
propertiesToFilter = rentProperties;
|
||||||
}
|
} else if (filters.mode === 'buy' || filters.mode === 'sell') {
|
||||||
if (filters.mode === 'sell' && property.listingType !== 'sale') {
|
propertiesToFilter = saleProperties;
|
||||||
return false;
|
} else {
|
||||||
}
|
propertiesToFilter = allProperties;
|
||||||
if (filters.mode === 'buy' && property.listingType !== 'sale') {
|
}
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const filtered = propertiesToFilter.filter(property => {
|
||||||
if (filters.city && filters.city !== 'all' && property.location.city !== filters.city) {
|
if (filters.city && filters.city !== 'all' && property.location.city !== filters.city) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -467,9 +452,16 @@ export default function HomePage() {
|
|||||||
transition={{ delay: 0.3, type: "spring" }}
|
transition={{ delay: 0.3, type: "spring" }}
|
||||||
>
|
>
|
||||||
{filteredProperties.length > 0 ? (
|
{filteredProperties.length > 0 ? (
|
||||||
<PropertyMap
|
<PropertyMapWithMarkers
|
||||||
properties={filteredProperties}
|
properties={filteredProperties.map(p => ({
|
||||||
userIdentity={searchFilters?.identityType || 'syrian'}
|
...p,
|
||||||
|
lat: p.location.lat,
|
||||||
|
lng: p.location.lng,
|
||||||
|
address: p.location.address
|
||||||
|
}))}
|
||||||
|
onPropertyClick={(property) => {
|
||||||
|
console.log('Property clicked:', property);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-[400px] flex flex-col items-center justify-center bg-gray-50">
|
<div className="h-[400px] flex flex-col items-center justify-center bg-gray-50">
|
||||||
|
|||||||
Reference in New Issue
Block a user