Added map for home

This commit is contained in:
Rahaf
2026-03-07 07:34:31 +03:00
parent 6d81ff56a8
commit 8c75c7c659
9 changed files with 1256 additions and 392 deletions

View File

@ -4,10 +4,9 @@ import { useState } from 'react';
import { motion } from 'framer-motion';
import { Calendar as CalendarIcon, ChevronLeft, ChevronRight } from 'lucide-react';
export default function BookingCalendar({ property, onDateSelect }) {
export default function BookingCalendar({ property }) {
const [currentMonth, setCurrentMonth] = useState(new Date());
const [selectedRange, setSelectedRange] = useState({ start: null, end: null });
const [bookedDates, setBookedDates] = useState(property.bookings || []);
const daysInMonth = new Date(
currentMonth.getFullYear(),
@ -27,8 +26,9 @@ export default function BookingCalendar({ property, onDateSelect }) {
];
const isDateBooked = (date) => {
if (!property.bookings) return false;
const dateStr = date.toISOString().split('T')[0];
return bookedDates.some(booking => {
return property.bookings.some(booking => {
const start = new Date(booking.startDate);
const end = new Date(booking.endDate);
const current = new Date(date);
@ -36,35 +36,8 @@ export default function BookingCalendar({ property, onDateSelect }) {
});
};
const isInSelectedRange = (date) => {
if (!selectedRange.start || !selectedRange.end) return false;
const dateStr = date.toISOString().split('T')[0];
return dateStr >= selectedRange.start && dateStr <= selectedRange.end;
};
const handleDateClick = (date) => {
if (isDateBooked(date)) return;
const dateStr = date.toISOString().split('T')[0];
if (!selectedRange.start || (selectedRange.start && selectedRange.end)) {
setSelectedRange({ start: dateStr, end: null });
} else {
if (dateStr > selectedRange.start) {
setSelectedRange({ ...selectedRange, end: dateStr });
onDateSelect?.({ start: selectedRange.start, end: dateStr });
} else {
setSelectedRange({ start: dateStr, end: null });
}
}
};
const changeMonth = (direction) => {
setCurrentMonth(new Date(
currentMonth.getFullYear(),
currentMonth.getMonth() + direction,
1
));
};
const renderDays = () => {
@ -83,24 +56,19 @@ export default function BookingCalendar({ property, onDateSelect }) {
);
const isBooked = isDateBooked(date);
const isSelected = isInSelectedRange(date);
const isToday = date.toDateString() === new Date().toDateString();
days.push(
<motion.button
<button
key={dayNumber}
whileHover={!isBooked ? { scale: 1.1 } : {}}
onClick={() => handleDateClick(date)}
disabled={isBooked}
className={`
p-2 rounded-lg text-center transition-all
p-2 rounded-lg text-center text-sm transition-all
${isBooked ? 'bg-gray-200 text-gray-400 cursor-not-allowed line-through' : 'hover:bg-amber-100 cursor-pointer'}
${isSelected ? 'bg-amber-500 text-white hover:bg-amber-600' : ''}
${isToday && !isSelected && !isBooked ? 'border-2 border-amber-500' : ''}
`}
>
{dayNumber}
</motion.button>
</button>
);
}
}
@ -108,53 +76,47 @@ export default function BookingCalendar({ property, onDateSelect }) {
};
return (
<div className="bg-white rounded-xl p-6 shadow-lg">
<div className="flex items-center justify-between mb-6">
<div className="bg-white rounded-xl p-4 border border-gray-200">
<div className="flex items-center justify-between mb-4">
<button
onClick={() => changeMonth(-1)}
className="p-2 hover:bg-gray-100 rounded-lg"
onClick={() => setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1))}
className="p-1 hover:bg-gray-100 rounded"
>
<ChevronRight className="w-5 h-5" />
<ChevronRight className="w-4 h-4" />
</button>
<h3 className="text-lg font-bold flex items-center gap-2">
<CalendarIcon className="w-5 h-5 text-amber-500" />
<h4 className="font-medium text-sm flex items-center gap-1">
<CalendarIcon className="w-4 h-4 text-amber-500" />
{monthNames[currentMonth.getMonth()]} {currentMonth.getFullYear()}
</h3>
</h4>
<button
onClick={() => changeMonth(1)}
className="p-2 hover:bg-gray-100 rounded-lg"
onClick={() => setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1))}
className="p-1 hover:bg-gray-100 rounded"
>
<ChevronLeft className="w-5 h-5" />
<ChevronLeft className="w-4 h-4" />
</button>
</div>
<div className="grid grid-cols-7 gap-1 mb-2 text-center text-sm font-semibold text-gray-600">
<div>جمعة</div>
<div>سبت</div>
<div className="grid grid-cols-7 gap-1 mb-2 text-center text-xs font-medium text-gray-500">
<div>أحد</div>
<div>إثنين</div>
<div>ثلاثاء</div>
<div>أربعاء</div>
<div>خميس</div>
<div>جمعة</div>
<div>سبت</div>
</div>
<div className="grid grid-cols-7 gap-1">
{renderDays()}
</div>
<div className="flex gap-4 mt-6 pt-4 border-t text-sm">
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-amber-500 rounded" />
<span>محدد</span>
<div className="flex gap-3 mt-3 pt-3 border-t text-xs">
<div className="flex items-center gap-1">
<div className="w-3 h-3 bg-gray-200 rounded" />
<span className="text-gray-500">محجوز</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-gray-200 rounded line-through" />
<span>محجوز</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-amber-500 rounded" />
<span>اليوم</span>
<div className="flex items-center gap-1">
<div className="w-3 h-3 bg-white border border-gray-300 rounded" />
<span className="text-gray-500">متاح</span>
</div>
</div>
</div>

View File

@ -0,0 +1,166 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { MapPin, DollarSign, X, Navigation } from 'lucide-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 PropertyMap({ properties, onPropertySelect }) {
const [selectedProperty, setSelectedProperty] = useState(null);
const [mapLoaded, setMapLoaded] = useState(false);
const mapRef = useRef(null);
const mapInstanceRef = useRef(null);
const markersRef = useRef([]);
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: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 19,
}).addTo(map);
mapInstanceRef.current = map;
setMapLoaded(true);
return () => {
if (mapInstanceRef.current) {
mapInstanceRef.current.remove();
mapInstanceRef.current = null;
}
};
}, []);
useEffect(() => {
if (!mapInstanceRef.current || !properties.length) return;
markersRef.current.forEach(marker => marker.remove());
markersRef.current = [];
const customIcon = L.divIcon({
className: 'custom-marker',
html: `<div class="w-10 h-10 bg-amber-500 rounded-full flex items-center justify-center text-white font-bold shadow-lg border-2 border-white">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
</div>`,
iconSize: [40, 40],
iconAnchor: [20, 40],
popupAnchor: [0, -40],
});
properties.forEach(property => {
if (property.location?.lat && property.location?.lng) {
const marker = L.marker([property.location.lat, property.location.lng], { icon: customIcon })
.addTo(mapInstanceRef.current)
.on('click', () => {
setSelectedProperty(property);
onPropertySelect?.(property);
});
marker.bindTooltip(property.title, {
permanent: false,
direction: 'top',
offset: [0, -40],
className: 'property-tooltip'
});
markersRef.current.push(marker);
}
});
if (markersRef.current.length > 0) {
const group = L.featureGroup(markersRef.current);
mapInstanceRef.current.fitBounds(group.getBounds(), { padding: [50, 50] });
}
}, [properties, mapLoaded, onPropertySelect]);
return (
<div className="relative w-full h-[600px] rounded-xl overflow-hidden">
<div ref={mapRef} className="w-full h-full z-0" />
<AnimatePresence>
{selectedProperty && (
<motion.div
initial={{ opacity: 0, y: 20, scale: 0.9 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 20, scale: 0.9 }}
className="absolute bottom-4 left-4 right-4 md:left-auto md:right-4 md:w-80 bg-white rounded-xl shadow-xl p-4 z-10 border border-gray-200"
>
<button
onClick={() => setSelectedProperty(null)}
className="absolute top-2 right-2 p-1 hover:bg-gray-100 rounded-full transition-colors"
>
<X className="w-4 h-4 text-gray-600" />
</button>
<h3 className="font-bold text-lg mb-2 text-gray-900">{selectedProperty.title}</h3>
<div className="flex items-center gap-1 text-gray-600 text-sm mb-3">
<MapPin className="w-4 h-4 flex-shrink-0" />
<span className="line-clamp-1">{selectedProperty.location?.address || `${selectedProperty.location.city}، ${selectedProperty.location.district}`}</span>
</div>
<div className="grid grid-cols-2 gap-2 mb-3">
<div className="bg-gray-50 p-2 rounded-lg text-center">
<div className="text-xs text-gray-500">يومي</div>
<div className="font-bold text-gray-900">
{selectedProperty.priceDisplay?.daily?.toLocaleString() || selectedProperty.price?.toLocaleString()} ل.س
</div>
</div>
<div className="bg-gray-50 p-2 rounded-lg text-center">
<div className="text-xs text-gray-500">شهري</div>
<div className="font-bold text-gray-900">
{selectedProperty.priceDisplay?.monthly?.toLocaleString() || (selectedProperty.price * 30)?.toLocaleString()} ل.س
</div>
</div>
</div>
<div className="flex gap-2">
<button
onClick={() => {
if (selectedProperty.location?.lat && selectedProperty.location?.lng) {
window.open(`https://www.openstreetmap.org/directions?from=&to=${selectedProperty.location.lat},${selectedProperty.location.lng}`, '_blank');
}
}}
className="flex-1 bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700 transition-colors flex items-center justify-center gap-2 text-sm"
>
<Navigation className="w-4 h-4" />
الاتجاهات
</button>
<button
onClick={() => window.location.href = `/property/${selectedProperty.id}`}
className="flex-1 bg-gray-800 text-white py-2 rounded-lg hover:bg-gray-900 transition-colors text-sm"
>
عرض التفاصيل
</button>
</div>
</motion.div>
)}
</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-3 rounded-full shadow-lg hover:bg-gray-50 transition-colors z-10 border border-gray-200"
title="الموقع الحالي"
>
<Navigation className="w-5 h-5 text-gray-700" />
</button>
</div>
);
}