From 46cbce1d88520a8cad903eeb2df95207d43c9e3e Mon Sep 17 00:00:00 2001 From: Rahaf Date: Sat, 2 May 2026 17:20:35 +0300 Subject: [PATCH] Added API to map with markers in main page --- app/components/PropertyMapWithMarkers.js | 100 +++++++++++++++++++++++ app/components/home/HeroSearch.js | 2 + app/page.js | 70 +++++++--------- 3 files changed, 133 insertions(+), 39 deletions(-) create mode 100644 app/components/PropertyMapWithMarkers.js diff --git a/app/components/PropertyMapWithMarkers.js b/app/components/PropertyMapWithMarkers.js new file mode 100644 index 0000000..3665d53 --- /dev/null +++ b/app/components/PropertyMapWithMarkers.js @@ -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: '© OpenStreetMap', + 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 = ` +
+

${property.title || 'عقار'}

+

${property.address || property.location?.address || ''}

+
+ ${formatPrice(property)} +
+ ${property.images && property.images.length > 0 ? `${property.title}` : ''} +
+ ${property.type ? `

النوع: ${property.type}

` : ''} + ${property.bedrooms > 0 ? `

غرف نوم: ${property.bedrooms}

` : ''} + ${property.bathrooms > 0 ? `

حمامات: ${property.bathrooms}

` : ''} +
+
+ `; + + 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 ( +
+
+
+ ); +} \ No newline at end of file diff --git a/app/components/home/HeroSearch.js b/app/components/home/HeroSearch.js index 7f21093..ff6d0c4 100644 --- a/app/components/home/HeroSearch.js +++ b/app/components/home/HeroSearch.js @@ -67,7 +67,9 @@ export default function HeroSearch({ onSearch, isAuthenticated }) { setActiveTab(tab); if ((tab === 'rent' || tab === 'sell') && !isAuthenticated) { setShowLoginDialog(true); + return; } + handleSearch(); }; const handleSearch = () => { diff --git a/app/page.js b/app/page.js index 67b3065..4d965ce 100644 --- a/app/page.js +++ b/app/page.js @@ -26,15 +26,13 @@ import { MessageCircle } from 'lucide-react'; import HeroSearch from './components/home/HeroSearch'; -import PropertyMap from './components/home/PropertyMap'; +import PropertyMapWithMarkers from './components/PropertyMapWithMarkers'; import Link from 'next/link'; import Image from 'next/image'; import { getRentProperties, getSaleProperties } from './utils/api'; import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from './enums'; 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) { const info = item.propertyInformation || {}; @@ -56,7 +54,6 @@ function mapApiProperty(item, index) { if (info.numberOfBedRooms) features.push(`${info.numberOfBedRooms} غرف نوم`); 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 rawImages = Array.isArray(info.images) ? info.images : []; 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() { const mapSectionRef = useRef(null); const [searchFilters, setSearchFilters] = useState(null); @@ -121,9 +114,10 @@ export default function HomePage() { const pathname = usePathname(); const [allProperties, setAllProperties] = useState([]); + const [rentProperties, setRentProperties] = useState([]); + const [saleProperties, setSaleProperties] = useState([]); const [loading, setLoading] = useState(true); - // Re-read user from JWT on every route change useEffect(() => { const authUser = AuthService.getUser(); if (authUser) { @@ -137,7 +131,6 @@ export default function HomePage() { } }, [pathname]); - // Fetch properties from API on mount useEffect(() => { async function fetchProperties() { @@ -150,15 +143,12 @@ export default function HomePage() { 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)), - ]; + const mappedRent = rentList.map((p, i) => mapApiProperty(p, i)); + const mappedSale = saleList.map((p, i) => mapApiProperty(p, rentList.length + i)); - if (mapped.length > 0) { - setAllProperties(mapped); - } - // If API returns empty, keep fallback + setRentProperties(mappedRent); + setSaleProperties(mappedSale); + setAllProperties([...mappedRent, ...mappedSale]); } catch (err) { console.error('[Home] Failed to fetch properties:', err); } finally { @@ -170,14 +160,10 @@ export default function HomePage() { }, []); useEffect(() => { - const handleClickOutside = (event) => { - if (menuRef.current && !menuRef.current.contains(event.target)) { - setShowUserMenu(false); - } - }; - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); - }, []); + if (searchFilters) { + applyFilters(searchFilters); + } + }, [rentProperties, saleProperties, searchFilters]); const logout = () => { AuthService.deleteToken(); @@ -188,17 +174,16 @@ export default function HomePage() { const applyFilters = (filters) => { setSearchFilters(filters); - const filtered = allProperties.filter(property => { - if (filters.mode === 'rent' && property.listingType !== 'rent') { - return false; - } - if (filters.mode === 'sell' && property.listingType !== 'sale') { - return false; - } - if (filters.mode === 'buy' && property.listingType !== 'sale') { - return false; - } + let propertiesToFilter = []; + if (filters.mode === 'rent') { + propertiesToFilter = rentProperties; + } else if (filters.mode === 'buy' || filters.mode === 'sell') { + propertiesToFilter = saleProperties; + } else { + propertiesToFilter = allProperties; + } + const filtered = propertiesToFilter.filter(property => { if (filters.city && filters.city !== 'all' && property.location.city !== filters.city) { return false; } @@ -467,9 +452,16 @@ export default function HomePage() { transition={{ delay: 0.3, type: "spring" }} > {filteredProperties.length > 0 ? ( - ({ + ...p, + lat: p.location.lat, + lng: p.location.lng, + address: p.location.address + }))} + onPropertyClick={(property) => { + console.log('Property clicked:', property); + }} /> ) : (