287 lines
12 KiB
JavaScript
287 lines
12 KiB
JavaScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { useState, useEffect, useRef } from 'react';
|
||
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
||
|
|
import { MapPin, DollarSign, X, Navigation, Bed, Bath, Square, Star, Calendar, Heart, ChevronLeft, ChevronRight } from 'lucide-react';
|
||
|
|
import L from 'leaflet';
|
||
|
|
import 'leaflet/dist/leaflet.css';
|
||
|
|
import Image from 'next/image';
|
||
|
|
import Link from 'next/link';
|
||
|
|
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',
|
||
|
|
});
|
||
|
|
|
||
|
|
const PropertyPopup = ({ property, onClose, onBook }) => {
|
||
|
|
const [currentImage, setCurrentImage] = useState(0);
|
||
|
|
const [isFavorite, setIsFavorite] = useState(false);
|
||
|
|
const [selectedDays, setSelectedDays] = useState(1);
|
||
|
|
|
||
|
|
const formatCurrency = (amount) => {
|
||
|
|
return amount?.toLocaleString() + ' ل.س';
|
||
|
|
};
|
||
|
|
|
||
|
|
const quickDays = [1, 3, 7, 14];
|
||
|
|
|
||
|
|
return (
|
||
|
|
<motion.div
|
||
|
|
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||
|
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||
|
|
exit={{ opacity: 0, y: 20, scale: 0.95 }}
|
||
|
|
transition={{ type: "spring", damping: 25, stiffness: 300 }}
|
||
|
|
className="absolute bottom-6 left-1/2 transform -translate-x-1/2 w-[90%] max-w-md bg-white/95 backdrop-blur-sm rounded-2xl shadow-2xl overflow-hidden z-20 border border-gray-200/50"
|
||
|
|
style={{ maxHeight: '70vh' }}
|
||
|
|
>
|
||
|
|
<div className="relative h-32 bg-gradient-to-r from-gray-900 to-gray-700">
|
||
|
|
<Image
|
||
|
|
src={property.images[currentImage] || '/property-placeholder.jpg'}
|
||
|
|
alt={property.title}
|
||
|
|
fill
|
||
|
|
className="object-cover opacity-80"
|
||
|
|
/>
|
||
|
|
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent" />
|
||
|
|
<button
|
||
|
|
onClick={onClose}
|
||
|
|
className="absolute top-2 right-2 w-8 h-8 bg-black/50 backdrop-blur-sm rounded-full flex items-center justify-center hover:bg-black/70 transition-colors z-10"
|
||
|
|
>
|
||
|
|
<X className="w-4 h-4 text-white" />
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
onClick={() => setIsFavorite(!isFavorite)}
|
||
|
|
className="absolute top-2 left-2 w-8 h-8 bg-black/50 backdrop-blur-sm rounded-full flex items-center justify-center hover:bg-black/70 transition-colors z-10"
|
||
|
|
>
|
||
|
|
<Heart className={`w-4 h-4 ${isFavorite ? 'fill-red-500 text-red-500' : 'text-white'}`} />
|
||
|
|
</button>
|
||
|
|
<div className="absolute bottom-2 left-3">
|
||
|
|
<div className="text-2xl font-bold text-white">{formatCurrency(property.price)}</div>
|
||
|
|
<div className="text-xs text-white/80">/ليلة</div>
|
||
|
|
</div>
|
||
|
|
<div className="absolute bottom-2 right-3 flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg">
|
||
|
|
<Star className="w-3 h-3 fill-amber-400 text-amber-400" />
|
||
|
|
<span className="text-sm font-medium text-white">{property.rating}</span>
|
||
|
|
</div>
|
||
|
|
{property.images.length > 1 && (
|
||
|
|
<>
|
||
|
|
<button
|
||
|
|
onClick={() => setCurrentImage(prev => Math.max(0, prev - 1))}
|
||
|
|
className="absolute left-2 top-1/2 transform -translate-y-1/2 w-6 h-6 bg-black/30 rounded-full flex items-center justify-center hover:bg-black/50 transition-colors"
|
||
|
|
>
|
||
|
|
<ChevronLeft className="w-4 h-4 text-white" />
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
onClick={() => setCurrentImage(prev => Math.min(property.images.length - 1, prev + 1))}
|
||
|
|
className="absolute right-2 top-1/2 transform -translate-y-1/2 w-6 h-6 bg-black/30 rounded-full flex items-center justify-center hover:bg-black/50 transition-colors"
|
||
|
|
>
|
||
|
|
<ChevronRight className="w-4 h-4 text-white" />
|
||
|
|
</button>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
{property.images.length > 1 && (
|
||
|
|
<div className="absolute bottom-2 left-1/2 transform -translate-x-1/2 flex gap-1">
|
||
|
|
{property.images.map((_, idx) => (
|
||
|
|
<div
|
||
|
|
key={idx}
|
||
|
|
className={`w-1 h-1 rounded-full transition-all ${
|
||
|
|
idx === currentImage ? 'w-3 bg-white' : 'bg-white/50'
|
||
|
|
}`}
|
||
|
|
/>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
<div className="p-4 space-y-3">
|
||
|
|
<div>
|
||
|
|
<h3 className="font-bold text-gray-900 text-base line-clamp-1">{property.title}</h3>
|
||
|
|
<div className="flex items-center gap-1 text-gray-500 text-xs mt-1">
|
||
|
|
<MapPin className="w-3 h-3 flex-shrink-0" />
|
||
|
|
<span className="line-clamp-1">{property.location.address || `${property.location.city}، ${property.location.district}`}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center justify-between bg-gray-50 p-2 rounded-xl">
|
||
|
|
<div className="flex items-center gap-1 text-xs">
|
||
|
|
<Bed className="w-3 h-3 text-gray-600" />
|
||
|
|
<span className="text-gray-700">{property.bedrooms} غرف</span>
|
||
|
|
</div>
|
||
|
|
<div className="w-px h-4 bg-gray-200" />
|
||
|
|
<div className="flex items-center gap-1 text-xs">
|
||
|
|
<Bath className="w-3 h-3 text-gray-600" />
|
||
|
|
<span className="text-gray-700">{property.bathrooms} حمامات</span>
|
||
|
|
</div>
|
||
|
|
<div className="w-px h-4 bg-gray-200" />
|
||
|
|
<div className="flex items-center gap-1 text-xs">
|
||
|
|
<Square className="w-3 h-3 text-gray-600" />
|
||
|
|
<span className="text-gray-700">{property.area}م²</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label className="block text-xs font-medium text-gray-600 mb-2">اختر المدة</label>
|
||
|
|
<div className="flex gap-2">
|
||
|
|
{quickDays.map(days => (
|
||
|
|
<button
|
||
|
|
key={days}
|
||
|
|
onClick={() => setSelectedDays(days)}
|
||
|
|
className={`flex-1 py-2 rounded-lg text-xs font-medium transition-all ${
|
||
|
|
selectedDays === days
|
||
|
|
? 'bg-amber-500 text-white shadow-sm'
|
||
|
|
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
{days} {days === 1 ? 'يوم' : 'أيام'}
|
||
|
|
</button>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="flex justify-between items-center p-3 bg-amber-50 rounded-xl">
|
||
|
|
<span className="text-sm text-gray-700">الإجمالي</span>
|
||
|
|
<div>
|
||
|
|
<span className="text-lg font-bold text-amber-600">{formatCurrency(property.price * selectedDays)}</span>
|
||
|
|
<span className="text-xs text-gray-500 mr-1">لـ {selectedDays} {selectedDays === 1 ? 'يوم' : 'أيام'}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="overflow-x-auto scrollbar-hide">
|
||
|
|
<div className="flex gap-2 pb-1" style={{ minWidth: 'min-content' }}>
|
||
|
|
{property.features.slice(0, 6).map((feature, idx) => (
|
||
|
|
<span key={idx} className="px-2 py-1 bg-gray-100 text-gray-700 rounded-lg text-xs whitespace-nowrap">
|
||
|
|
{feature}
|
||
|
|
</span>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex gap-2 pt-2">
|
||
|
|
<Link
|
||
|
|
href={`/property/${property.id}`}
|
||
|
|
className="flex-1 bg-white border border-gray-300 text-gray-700 py-2.5 rounded-xl font-medium hover:bg-gray-50 transition-colors text-center text-xs"
|
||
|
|
>
|
||
|
|
التفاصيل
|
||
|
|
</Link>
|
||
|
|
<button
|
||
|
|
onClick={() => {
|
||
|
|
alert('تم إرسال طلب الحجز بنجاح');
|
||
|
|
}}
|
||
|
|
className="flex-1 bg-amber-500 text-white py-2.5 rounded-xl font-medium hover:bg-amber-600 transition-colors text-xs shadow-sm"
|
||
|
|
>
|
||
|
|
حجز سريع
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="text-center pb-2">
|
||
|
|
<div className="w-10 h-1 bg-gray-300 rounded-full mx-auto"></div>
|
||
|
|
</div>
|
||
|
|
</motion.div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default function PropertyMap({ properties, userIdentity = 'syrian' }) {
|
||
|
|
const [selectedProperty, setSelectedProperty] = useState(null);
|
||
|
|
const [mapLoaded, setMapLoaded] = useState(false);
|
||
|
|
const mapRef = useRef(null);
|
||
|
|
const mapInstanceRef = useRef(null);
|
||
|
|
const markersRef = useRef([]);
|
||
|
|
|
||
|
|
const filteredProperties = properties.filter(property => {
|
||
|
|
if (!property.allowedIdentities) return true;
|
||
|
|
return property.allowedIdentities.includes(userIdentity);
|
||
|
|
});
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (!mapRef.current || mapInstanceRef.current) return;
|
||
|
|
|
||
|
|
const map = L.map(mapRef.current).setView([34.8021, 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 || !filteredProperties.length) return;
|
||
|
|
|
||
|
|
markersRef.current.forEach(marker => marker.remove());
|
||
|
|
markersRef.current = [];
|
||
|
|
|
||
|
|
filteredProperties.forEach(property => {
|
||
|
|
if (property.location?.lat && property.location?.lng) {
|
||
|
|
const customIcon = L.divIcon({
|
||
|
|
className: 'custom-marker',
|
||
|
|
html: `
|
||
|
|
<div class="relative group cursor-pointer">
|
||
|
|
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-1">
|
||
|
|
<div class="w-px h-6 bg-gradient-to-t from-amber-400 to-transparent"></div>
|
||
|
|
</div>
|
||
|
|
<div class="w-9 h-9 bg-amber-500 rounded-full flex items-center justify-center text-white font-bold shadow-lg border-2 border-white hover:bg-amber-600 transition-colors transform group-hover:scale-110">
|
||
|
|
<span class="text-xs">$${property.priceUSD}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`,
|
||
|
|
iconSize: [36, 45],
|
||
|
|
iconAnchor: [18, 45],
|
||
|
|
popupAnchor: [0, -45],
|
||
|
|
});
|
||
|
|
|
||
|
|
const marker = L.marker([property.location.lat, property.location.lng], { icon: customIcon })
|
||
|
|
.addTo(mapInstanceRef.current)
|
||
|
|
.on('click', () => {
|
||
|
|
setSelectedProperty(property);
|
||
|
|
mapInstanceRef.current.panBy([0, -100], { animate: true, duration: 0.5 });
|
||
|
|
});
|
||
|
|
|
||
|
|
markersRef.current.push(marker);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
if (markersRef.current.length > 0) {
|
||
|
|
const group = L.featureGroup(markersRef.current);
|
||
|
|
mapInstanceRef.current.fitBounds(group.getBounds(), { padding: [50, 100] });
|
||
|
|
}
|
||
|
|
}, [filteredProperties, mapLoaded]);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="relative w-full h-[600px] rounded-xl overflow-hidden">
|
||
|
|
<div ref={mapRef} className="w-full h-full z-0" />
|
||
|
|
|
||
|
|
<AnimatePresence>
|
||
|
|
{selectedProperty && (
|
||
|
|
<PropertyPopup
|
||
|
|
property={selectedProperty}
|
||
|
|
onClose={() => setSelectedProperty(null)}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
</AnimatePresence>
|
||
|
|
<button
|
||
|
|
onClick={() => {
|
||
|
|
if (navigator.geolocation) {
|
||
|
|
navigator.geolocation.getCurrentPosition(
|
||
|
|
(position) => {
|
||
|
|
mapInstanceRef.current?.setView([position.coords.latitude, position.coords.longitude], 13);
|
||
|
|
},
|
||
|
|
(error) => console.error('Error getting location:', error)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}}
|
||
|
|
className="absolute top-4 right-4 bg-white p-2.5 rounded-full shadow-md hover:bg-gray-50 transition-colors z-10 border border-gray-200"
|
||
|
|
title="الموقع الحالي"
|
||
|
|
>
|
||
|
|
<Navigation className="w-4 h-4 text-gray-700" />
|
||
|
|
</button>
|
||
|
|
<div className="absolute top-4 left-4 bg-white px-3 py-1.5 rounded-full shadow-md z-10 border border-gray-200">
|
||
|
|
<span className="font-bold text-gray-900 text-sm">{filteredProperties.length}</span>
|
||
|
|
<span className="text-gray-600 text-xs mr-1">عقار</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|