Added API to map with markers in main page
All checks were successful
Build frontend / build (push) Successful in 53s

This commit is contained in:
Rahaf
2026-05-02 17:20:35 +03:00
parent 1198c18004
commit 46cbce1d88
3 changed files with 133 additions and 39 deletions

View 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: '&copy; <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>
);
}

View File

@ -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 = () => {

View File

@ -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">