Added map for home
This commit is contained in:
192
app/components/home/HeroSearch.js
Normal file
192
app/components/home/HeroSearch.js
Normal file
@ -0,0 +1,192 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Search, MapPin, Home, DollarSign } from 'lucide-react';
|
||||
|
||||
export default function HeroSearch({ onSearch }) {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState('rent');
|
||||
const [filters, setFilters] = useState({
|
||||
city: '',
|
||||
propertyType: '',
|
||||
priceRange: '',
|
||||
identityType: 'syrian'
|
||||
});
|
||||
|
||||
const cities = [
|
||||
{ id: 'all', label: 'جميع المدن' },
|
||||
{ id: 'دمشق', label: 'دمشق' },
|
||||
{ id: 'حلب', label: 'حلب' },
|
||||
{ id: 'حمص', label: 'حمص' },
|
||||
{ id: 'اللاذقية', label: 'اللاذقية' },
|
||||
{ id: 'درعا', label: 'درعا' }
|
||||
];
|
||||
|
||||
const propertyTypes = [
|
||||
{ id: 'all', label: 'الكل' },
|
||||
{ id: 'apartment', label: 'شقة' },
|
||||
{ id: 'villa', label: 'فيلا' },
|
||||
{ id: 'house', label: 'بيت' },
|
||||
{ id: 'studio', label: 'استوديو' }
|
||||
];
|
||||
|
||||
const priceRanges = [
|
||||
{ id: 'all', label: 'جميع الأسعار' },
|
||||
{ id: '0-500', label: 'أقل من 50$' },
|
||||
{ id: '500-1000', label: '50$ - 100$' },
|
||||
{ id: '1000-2000', label: '100$ - 200$' },
|
||||
{ id: '2000-3000', label: '200$ - 300$' },
|
||||
{ id: '3000+', label: 'أكثر من 300$' }
|
||||
];
|
||||
|
||||
const identityTypes = [
|
||||
{ id: 'syrian', label: 'هوية سورية' },
|
||||
{ id: 'passport', label: 'جواز سفر' }
|
||||
];
|
||||
|
||||
const handleSearch = () => {
|
||||
onSearch({
|
||||
...filters,
|
||||
propertyType: filters.propertyType || 'all',
|
||||
city: filters.city || 'all',
|
||||
priceRange: filters.priceRange || 'all'
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="bg-white/10 backdrop-blur-lg rounded-2xl p-6 sm:p-8 border border-white/20 shadow-2xl"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.5 }}
|
||||
>
|
||||
<div className="flex flex-wrap gap-2 mb-8">
|
||||
{['rent', 'buy', 'sell'].map((tab) => (
|
||||
<motion.button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={`px-4 py-2 rounded-lg font-medium text-sm transition-all ${
|
||||
activeTab === tab
|
||||
? 'bg-amber-500 text-white'
|
||||
: 'bg-white/20 text-white hover:bg-white/30'
|
||||
}`}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
{t(`${tab}Tab`)}
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white mb-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<MapPin className="w-4 h-4" />
|
||||
{t("cityStreetLabel")}
|
||||
</div>
|
||||
</label>
|
||||
<select
|
||||
value={filters.city}
|
||||
onChange={(e) => setFilters({...filters, city: e.target.value})}
|
||||
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
|
||||
style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'left 1rem center',
|
||||
backgroundSize: '1rem',
|
||||
paddingLeft: '2.5rem'
|
||||
}}
|
||||
>
|
||||
{cities.map(city => (
|
||||
<option key={city.id} value={city.id}>{city.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white mb-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<Home className="w-4 h-4" />
|
||||
{t("rentTypeLabel")}
|
||||
</div>
|
||||
</label>
|
||||
<select
|
||||
value={filters.propertyType}
|
||||
onChange={(e) => setFilters({...filters, propertyType: e.target.value})}
|
||||
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
|
||||
style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'left 1rem center',
|
||||
backgroundSize: '1rem',
|
||||
paddingLeft: '2.5rem'
|
||||
}}
|
||||
>
|
||||
{propertyTypes.map(type => (
|
||||
<option key={type.id} value={type.id}>{type.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white mb-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<DollarSign className="w-4 h-4" />
|
||||
{t("priceLabel")}
|
||||
</div>
|
||||
</label>
|
||||
<select
|
||||
value={filters.priceRange}
|
||||
onChange={(e) => setFilters({...filters, priceRange: e.target.value})}
|
||||
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
|
||||
style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'left 1rem center',
|
||||
backgroundSize: '1rem',
|
||||
paddingLeft: '2.5rem'
|
||||
}}
|
||||
>
|
||||
{priceRanges.map(range => (
|
||||
<option key={range.id} value={range.id}>{range.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white mb-2">
|
||||
<div className="flex items-center gap-1">
|
||||
نوع الوثيقة
|
||||
</div>
|
||||
</label>
|
||||
<select
|
||||
value={filters.identityType}
|
||||
onChange={(e) => setFilters({...filters, identityType: e.target.value})}
|
||||
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
|
||||
style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'left 1rem center',
|
||||
backgroundSize: '1rem',
|
||||
paddingLeft: '2.5rem'
|
||||
}}
|
||||
>
|
||||
{identityTypes.map(type => (
|
||||
<option key={type.id} value={type.id}>{type.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<motion.button
|
||||
onClick={handleSearch}
|
||||
className="w-full bg-amber-500 hover:bg-amber-600 text-white font-bold py-4 px-6 rounded-xl transition-all duration-300 flex items-center justify-center text-base gap-3 shadow-lg hover:shadow-xl"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<Search className="w-5 h-5" />
|
||||
{t("searchButton")}
|
||||
</motion.button>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
287
app/components/home/PropertyMap.js
Normal file
287
app/components/home/PropertyMap.js
Normal file
@ -0,0 +1,287 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
|
||||
166
app/components/property/PropertyMap.js
Normal file
166
app/components/property/PropertyMap.js
Normal 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: '© <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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user