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 { motion } from 'framer-motion';
|
||||||
import { Calendar as CalendarIcon, ChevronLeft, ChevronRight } from 'lucide-react';
|
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 [currentMonth, setCurrentMonth] = useState(new Date());
|
||||||
const [selectedRange, setSelectedRange] = useState({ start: null, end: null });
|
const [selectedRange, setSelectedRange] = useState({ start: null, end: null });
|
||||||
const [bookedDates, setBookedDates] = useState(property.bookings || []);
|
|
||||||
|
|
||||||
const daysInMonth = new Date(
|
const daysInMonth = new Date(
|
||||||
currentMonth.getFullYear(),
|
currentMonth.getFullYear(),
|
||||||
@ -27,8 +26,9 @@ export default function BookingCalendar({ property, onDateSelect }) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const isDateBooked = (date) => {
|
const isDateBooked = (date) => {
|
||||||
|
if (!property.bookings) return false;
|
||||||
const dateStr = date.toISOString().split('T')[0];
|
const dateStr = date.toISOString().split('T')[0];
|
||||||
return bookedDates.some(booking => {
|
return property.bookings.some(booking => {
|
||||||
const start = new Date(booking.startDate);
|
const start = new Date(booking.startDate);
|
||||||
const end = new Date(booking.endDate);
|
const end = new Date(booking.endDate);
|
||||||
const current = new Date(date);
|
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) => {
|
const handleDateClick = (date) => {
|
||||||
if (isDateBooked(date)) return;
|
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 = () => {
|
const renderDays = () => {
|
||||||
@ -83,24 +56,19 @@ export default function BookingCalendar({ property, onDateSelect }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isBooked = isDateBooked(date);
|
const isBooked = isDateBooked(date);
|
||||||
const isSelected = isInSelectedRange(date);
|
|
||||||
const isToday = date.toDateString() === new Date().toDateString();
|
|
||||||
|
|
||||||
days.push(
|
days.push(
|
||||||
<motion.button
|
<button
|
||||||
key={dayNumber}
|
key={dayNumber}
|
||||||
whileHover={!isBooked ? { scale: 1.1 } : {}}
|
|
||||||
onClick={() => handleDateClick(date)}
|
onClick={() => handleDateClick(date)}
|
||||||
disabled={isBooked}
|
disabled={isBooked}
|
||||||
className={`
|
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'}
|
${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}
|
{dayNumber}
|
||||||
</motion.button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -108,53 +76,47 @@ export default function BookingCalendar({ property, onDateSelect }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-xl p-6 shadow-lg">
|
<div className="bg-white rounded-xl p-4 border border-gray-200">
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => changeMonth(-1)}
|
onClick={() => setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1))}
|
||||||
className="p-2 hover:bg-gray-100 rounded-lg"
|
className="p-1 hover:bg-gray-100 rounded"
|
||||||
>
|
>
|
||||||
<ChevronRight className="w-5 h-5" />
|
<ChevronRight className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<h3 className="text-lg font-bold flex items-center gap-2">
|
<h4 className="font-medium text-sm flex items-center gap-1">
|
||||||
<CalendarIcon className="w-5 h-5 text-amber-500" />
|
<CalendarIcon className="w-4 h-4 text-amber-500" />
|
||||||
{monthNames[currentMonth.getMonth()]} {currentMonth.getFullYear()}
|
{monthNames[currentMonth.getMonth()]} {currentMonth.getFullYear()}
|
||||||
</h3>
|
</h4>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => changeMonth(1)}
|
onClick={() => setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1))}
|
||||||
className="p-2 hover:bg-gray-100 rounded-lg"
|
className="p-1 hover:bg-gray-100 rounded"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="w-5 h-5" />
|
<ChevronLeft className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid grid-cols-7 gap-1 mb-2 text-center text-xs font-medium text-gray-500">
|
||||||
<div className="grid grid-cols-7 gap-1 mb-2 text-center text-sm font-semibold text-gray-600">
|
|
||||||
<div>جمعة</div>
|
|
||||||
<div>سبت</div>
|
|
||||||
<div>أحد</div>
|
<div>أحد</div>
|
||||||
<div>إثنين</div>
|
<div>إثنين</div>
|
||||||
<div>ثلاثاء</div>
|
<div>ثلاثاء</div>
|
||||||
<div>أربعاء</div>
|
<div>أربعاء</div>
|
||||||
<div>خميس</div>
|
<div>خميس</div>
|
||||||
|
<div>جمعة</div>
|
||||||
|
<div>سبت</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-7 gap-1">
|
<div className="grid grid-cols-7 gap-1">
|
||||||
{renderDays()}
|
{renderDays()}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex gap-3 mt-3 pt-3 border-t text-xs">
|
||||||
<div className="flex gap-4 mt-6 pt-4 border-t text-sm">
|
<div className="flex items-center gap-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="w-3 h-3 bg-gray-200 rounded" />
|
||||||
<div className="w-4 h-4 bg-amber-500 rounded" />
|
<span className="text-gray-500">محجوز</span>
|
||||||
<span>محدد</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-1">
|
||||||
<div className="w-4 h-4 bg-gray-200 rounded line-through" />
|
<div className="w-3 h-3 bg-white border border-gray-300 rounded" />
|
||||||
<span>محجوز</span>
|
<span className="text-gray-500">متاح</span>
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-4 h-4 border-2 border-amber-500 rounded" />
|
|
||||||
<span>اليوم</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -24,3 +24,51 @@ body {
|
|||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.leaflet-container {
|
||||||
|
font-family: inherit;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-popup-content-wrapper {
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-popup-content {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-popup-tip {
|
||||||
|
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-marker {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-marker div {
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-marker:hover div {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-tooltip {
|
||||||
|
background: white !important;
|
||||||
|
color: #1f2937 !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
padding: 0.5rem 1rem !important;
|
||||||
|
border-radius: 0.75rem !important;
|
||||||
|
border: 1px solid #e5e7eb !important;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-tooltip::before {
|
||||||
|
border-top-color: white !important;
|
||||||
|
}
|
||||||
809
app/page.js
809
app/page.js
@ -1,67 +1,271 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
import { useRef } from 'react';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
ShieldCheck,
|
||||||
|
Lock,
|
||||||
|
Zap,
|
||||||
|
Star,
|
||||||
|
Rocket,
|
||||||
|
Search,
|
||||||
|
MapPin,
|
||||||
|
Home,
|
||||||
|
DollarSign,
|
||||||
|
ChevronDown,
|
||||||
|
Shield,
|
||||||
|
Award,
|
||||||
|
Sparkles
|
||||||
|
} from 'lucide-react';
|
||||||
import './i18n/config';
|
import './i18n/config';
|
||||||
|
import HeroSearch from './components/home/HeroSearch';
|
||||||
|
import PropertyMap from './components/home/PropertyMap';
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const constraintsRef = useRef(null);
|
const mapSectionRef = useRef(null);
|
||||||
|
const [searchFilters, setSearchFilters] = useState(null);
|
||||||
|
const [showMap, setShowMap] = useState(false);
|
||||||
|
const [filteredProperties, setFilteredProperties] = useState([]);
|
||||||
|
const [isScrolling, setIsScrolling] = useState(false);
|
||||||
|
|
||||||
const fadeInUp = {
|
const [allProperties] = useState([
|
||||||
hidden: { opacity: 0, y: 20 },
|
{
|
||||||
visible: {
|
id: 1,
|
||||||
opacity: 1,
|
title: 'فيلا فاخرة في المزة',
|
||||||
y: 0,
|
description: 'فيلا فاخرة مع حديقة خاصة ومسبح في أفضل أحياء دمشق.',
|
||||||
transition: {
|
type: 'villa',
|
||||||
duration: 0.6,
|
price: 500000,
|
||||||
ease: "easeOut"
|
priceUSD: 50,
|
||||||
}
|
priceUnit: 'daily',
|
||||||
}
|
location: {
|
||||||
};
|
city: 'دمشق',
|
||||||
|
district: 'المزة',
|
||||||
const staggerContainer = {
|
address: 'شارع المزة - فيلات غربية',
|
||||||
hidden: { opacity: 0 },
|
lat: 33.5138,
|
||||||
visible: {
|
lng: 36.2765
|
||||||
opacity: 1,
|
},
|
||||||
transition: {
|
bedrooms: 5,
|
||||||
staggerChildren: 0.2,
|
bathrooms: 4,
|
||||||
delayChildren: 0.3
|
area: 450,
|
||||||
}
|
features: ['مسبح', 'حديقة خاصة', 'موقف سيارات', 'أمن 24/7', 'تدفئة مركزية', 'تكييف مركزي'],
|
||||||
}
|
images: ['/villa1.jpg', '/villa2.jpg', '/villa3.jpg'],
|
||||||
};
|
status: 'available',
|
||||||
|
rating: 4.8,
|
||||||
const buttonHover = {
|
isNew: true,
|
||||||
rest: { scale: 1 },
|
allowedIdentities: ['syrian', 'passport'],
|
||||||
hover: {
|
priceDisplay: {
|
||||||
scale: 1.05,
|
daily: 500000,
|
||||||
transition: {
|
monthly: 15000000
|
||||||
type: "spring",
|
},
|
||||||
stiffness: 400,
|
bookings: [
|
||||||
damping: 10
|
{ startDate: '2024-03-10', endDate: '2024-03-15' },
|
||||||
}
|
{ startDate: '2024-03-20', endDate: '2024-03-25' }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
tap: { scale: 0.95 }
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'شقة حديثة في الشهباء',
|
||||||
|
description: 'شقة عصرية في حي الشهباء الراقي بحلب.',
|
||||||
|
type: 'apartment',
|
||||||
|
price: 250000,
|
||||||
|
priceUSD: 25,
|
||||||
|
priceUnit: 'daily',
|
||||||
|
location: {
|
||||||
|
city: 'حلب',
|
||||||
|
district: 'الشهباء',
|
||||||
|
address: 'شارع النيل - بناء الرحاب',
|
||||||
|
lat: 36.2021,
|
||||||
|
lng: 37.1347
|
||||||
|
},
|
||||||
|
bedrooms: 3,
|
||||||
|
bathrooms: 2,
|
||||||
|
area: 180,
|
||||||
|
features: ['مطبخ مجهز', 'بلكونة', 'موقف سيارات', 'مصعد'],
|
||||||
|
images: ['/apartment1.jpg', '/apartment2.jpg'],
|
||||||
|
status: 'available',
|
||||||
|
rating: 4.5,
|
||||||
|
isNew: false,
|
||||||
|
allowedIdentities: ['syrian'],
|
||||||
|
priceDisplay: {
|
||||||
|
daily: 250000,
|
||||||
|
monthly: 7500000
|
||||||
|
},
|
||||||
|
bookings: [
|
||||||
|
{ startDate: '2024-03-05', endDate: '2024-03-08' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'بيت عائلي في بابا عمرو',
|
||||||
|
description: 'بيت واسع مناسب للعائلات في حمص.',
|
||||||
|
type: 'house',
|
||||||
|
price: 350000,
|
||||||
|
priceUSD: 35,
|
||||||
|
priceUnit: 'daily',
|
||||||
|
location: {
|
||||||
|
city: 'حمص',
|
||||||
|
district: 'بابا عمرو',
|
||||||
|
address: 'حي الزهور',
|
||||||
|
lat: 34.7265,
|
||||||
|
lng: 36.7186
|
||||||
|
},
|
||||||
|
bedrooms: 4,
|
||||||
|
bathrooms: 3,
|
||||||
|
area: 300,
|
||||||
|
features: ['حديقة كبيرة', 'موقف سيارات', 'مدفأة', 'كراج'],
|
||||||
|
images: ['/house1.jpg'],
|
||||||
|
status: 'booked',
|
||||||
|
rating: 4.3,
|
||||||
|
isNew: false,
|
||||||
|
allowedIdentities: ['syrian', 'passport'],
|
||||||
|
priceDisplay: {
|
||||||
|
daily: 350000,
|
||||||
|
monthly: 10500000
|
||||||
|
},
|
||||||
|
bookings: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: 'شقة بجانب البحر',
|
||||||
|
description: 'شقة رائعة مع إطلالة بحرية في اللاذقية.',
|
||||||
|
type: 'apartment',
|
||||||
|
price: 300000,
|
||||||
|
priceUSD: 30,
|
||||||
|
priceUnit: 'daily',
|
||||||
|
location: {
|
||||||
|
city: 'اللاذقية',
|
||||||
|
district: 'الشاطئ الأزرق',
|
||||||
|
address: 'الكورنيش الغربي',
|
||||||
|
lat: 35.5306,
|
||||||
|
lng: 35.7801
|
||||||
|
},
|
||||||
|
bedrooms: 3,
|
||||||
|
bathrooms: 2,
|
||||||
|
area: 200,
|
||||||
|
features: ['إطلالة بحرية', 'شرفة', 'تكييف', 'أمن'],
|
||||||
|
images: ['/seaside1.jpg', '/seaside2.jpg', '/seaside3.jpg'],
|
||||||
|
status: 'available',
|
||||||
|
rating: 4.9,
|
||||||
|
isNew: true,
|
||||||
|
allowedIdentities: ['passport'],
|
||||||
|
priceDisplay: {
|
||||||
|
daily: 300000,
|
||||||
|
monthly: 9000000
|
||||||
|
},
|
||||||
|
bookings: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
title: 'فيلا في درعا',
|
||||||
|
description: 'فيلا فاخرة في حي الأطباء بدرعا.',
|
||||||
|
type: 'villa',
|
||||||
|
price: 400000,
|
||||||
|
priceUSD: 40,
|
||||||
|
priceUnit: 'daily',
|
||||||
|
location: {
|
||||||
|
city: 'درعا',
|
||||||
|
district: 'حي الأطباء',
|
||||||
|
address: 'شارع الشفاء',
|
||||||
|
lat: 32.6237,
|
||||||
|
lng: 36.1016
|
||||||
|
},
|
||||||
|
bedrooms: 4,
|
||||||
|
bathrooms: 3,
|
||||||
|
area: 350,
|
||||||
|
features: ['حديقة مثمرة', 'أنظمة أمن', 'مسبح', 'كراج'],
|
||||||
|
images: ['/villa4.jpg', '/villa5.jpg'],
|
||||||
|
status: 'available',
|
||||||
|
rating: 4.6,
|
||||||
|
isNew: false,
|
||||||
|
allowedIdentities: ['syrian', 'passport'],
|
||||||
|
priceDisplay: {
|
||||||
|
daily: 400000,
|
||||||
|
monthly: 12000000
|
||||||
|
},
|
||||||
|
bookings: []
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const applyFilters = (filters) => {
|
||||||
|
setSearchFilters(filters);
|
||||||
|
|
||||||
|
const filtered = allProperties.filter(property => {
|
||||||
|
if (filters.city && filters.city !== 'all' && property.location.city !== filters.city) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.propertyType && filters.propertyType !== 'all' && property.type !== filters.propertyType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.priceRange && filters.priceRange !== 'all') {
|
||||||
|
const priceUSD = property.priceUSD;
|
||||||
|
switch(filters.priceRange) {
|
||||||
|
case '0-500': if (priceUSD > 50) return false; break;
|
||||||
|
case '500-1000': if (priceUSD < 51 || priceUSD > 100) return false; break;
|
||||||
|
case '1000-2000': if (priceUSD < 101 || priceUSD > 200) return false; break;
|
||||||
|
case '2000-3000': if (priceUSD < 201 || priceUSD > 300) return false; break;
|
||||||
|
case '3000+': if (priceUSD < 301) return false; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.identityType && property.allowedIdentities) {
|
||||||
|
if (!property.allowedIdentities.includes(filters.identityType)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
setFilteredProperties(filtered);
|
||||||
|
|
||||||
|
if (!showMap) {
|
||||||
|
setShowMap(true);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (mapSectionRef.current) {
|
||||||
|
setIsScrolling(true);
|
||||||
|
mapSectionRef.current.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'center'
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => setIsScrolling(false), 1000);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
} else {
|
||||||
|
if (mapSectionRef.current) {
|
||||||
|
setIsScrolling(true);
|
||||||
|
mapSectionRef.current.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'center'
|
||||||
|
});
|
||||||
|
setTimeout(() => setIsScrolling(false), 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const floatingAnimation = {
|
const resetSearch = () => {
|
||||||
y: [0, -10, 0],
|
setShowMap(false);
|
||||||
transition: {
|
setSearchFilters(null);
|
||||||
duration: 2,
|
setFilteredProperties([]);
|
||||||
repeat: Infinity,
|
window.scrollTo({
|
||||||
ease: "easeInOut"
|
top: 0,
|
||||||
}
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen" ref={constraintsRef}>
|
<div className="min-h-screen">
|
||||||
<section className="relative min-h-screen flex items-center justify-center overflow-hidden">
|
<section className="relative min-h-screen flex items-center justify-center overflow-hidden">
|
||||||
<div className="absolute inset-0 z-0">
|
<div className="absolute inset-0 z-0">
|
||||||
<motion.div
|
<motion.div
|
||||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: 'url(/Home.jpg)',
|
backgroundImage: 'url(/hero.jpg)',
|
||||||
}}
|
}}
|
||||||
initial={{ scale: 1.1 }}
|
initial={{ scale: 1.1 }}
|
||||||
animate={{ scale: 1 }}
|
animate={{ scale: 1 }}
|
||||||
@ -69,315 +273,282 @@ export default function HomePage() {
|
|||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-gradient-to-r from-black/70 via-black/60 to-black/50" />
|
<div className="absolute inset-0 bg-gradient-to-r from-black/70 via-black/60 to-black/50" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative z-10 container mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="relative z-10 container mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="max-w-6xl mx-auto">
|
<div className="max-w-6xl mx-auto">
|
||||||
<motion.div
|
<motion.div
|
||||||
className="text-center mb-12"
|
className="text-center mb-12"
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate="visible"
|
animate="visible"
|
||||||
variants={staggerContainer}
|
variants={{
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: { staggerChildren: 0.2 }
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<motion.h1
|
<motion.h1
|
||||||
className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-6 leading-tight tracking-tight"
|
className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-6 leading-tight tracking-tight"
|
||||||
variants={fadeInUp}
|
variants={{
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: { opacity: 1, y: 0 }
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t("heroTitleLine1")}<br />
|
{t("heroTitleLine1")}<br />
|
||||||
<motion.span
|
<motion.span
|
||||||
className="text-amber-400"
|
className="text-amber-400"
|
||||||
animate={floatingAnimation}
|
animate={{
|
||||||
|
y: [0, -10, 0],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 2,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "easeInOut"
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t("heroTitleLine2")}
|
{t("heroTitleLine2")}
|
||||||
</motion.span>
|
</motion.span>
|
||||||
</motion.h1>
|
</motion.h1>
|
||||||
<motion.p
|
<motion.p
|
||||||
className="text-base sm:text-lg text-gray-200 max-w-2xl mx-auto leading-relaxed"
|
className="text-base sm:text-lg text-gray-200 max-w-2xl mx-auto leading-relaxed"
|
||||||
variants={fadeInUp}
|
variants={{
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: { opacity: 1, y: 0 }
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t("heroSubtitle")}
|
{t("heroSubtitle")}
|
||||||
</motion.p>
|
</motion.p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
<HeroSearch onSearch={applyFilters} />
|
||||||
<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 }}
|
|
||||||
whileHover={{
|
|
||||||
boxShadow: "0 20px 40px rgba(0,0,0,0.3)",
|
|
||||||
transition: { duration: 0.3 }
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex flex-wrap gap-2 mb-8">
|
|
||||||
<motion.button
|
|
||||||
className="px-4 py-2 bg-amber-500 text-white font-medium rounded-lg text-sm"
|
|
||||||
variants={buttonHover}
|
|
||||||
initial="rest"
|
|
||||||
whileHover="hover"
|
|
||||||
whileTap="tap"
|
|
||||||
>
|
|
||||||
{t("rentTab")}
|
|
||||||
</motion.button>
|
|
||||||
<motion.button
|
|
||||||
className="px-4 py-2 bg-white/20 text-white font-medium rounded-lg hover:bg-white/30 transition-colors text-sm"
|
|
||||||
variants={buttonHover}
|
|
||||||
initial="rest"
|
|
||||||
whileHover="hover"
|
|
||||||
whileTap="tap"
|
|
||||||
>
|
|
||||||
{t("buyTab")}
|
|
||||||
</motion.button>
|
|
||||||
<motion.button
|
|
||||||
className="px-4 py-2 bg-white/20 text-white font-medium rounded-lg hover:bg-white/30 transition-colors text-sm"
|
|
||||||
variants={buttonHover}
|
|
||||||
initial="rest"
|
|
||||||
whileHover="hover"
|
|
||||||
whileTap="tap"
|
|
||||||
>
|
|
||||||
{t("sellTab")}
|
|
||||||
</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">
|
|
||||||
{t("cityStreetLabel")}
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-lg bg-white/90 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-sm"
|
|
||||||
placeholder={t("cityStreetPlaceholder")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-white mb-2">
|
|
||||||
{t("rentTypeLabel")}
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<select
|
|
||||||
className="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-lg bg-white/90 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent appearance-none text-sm"
|
|
||||||
>
|
|
||||||
<option value="">{t("selectType")}</option>
|
|
||||||
<option value="apartment">{t("apartment")}</option>
|
|
||||||
<option value="house">{t("house")}</option>
|
|
||||||
<option value="villa">{t("villa")}</option>
|
|
||||||
<option value="studio">{t("studio")}</option>
|
|
||||||
{/* <option value="penthouse">{t("penthouse")}</option> */}
|
|
||||||
</select>
|
|
||||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
|
||||||
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-white mb-2">
|
|
||||||
{t("priceLabel")}
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<select
|
|
||||||
className="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-lg bg-white/90 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent appearance-none text-sm"
|
|
||||||
>
|
|
||||||
<option value="">{t("selectPriceRange")}</option>
|
|
||||||
<option value="0-500">{t("priceRange1")}</option>
|
|
||||||
<option value="500-1000">{t("priceRange2")}</option>
|
|
||||||
<option value="1000-2000">{t("priceRange3")}</option>
|
|
||||||
<option value="2000-3000">{t("priceRange4")}</option>
|
|
||||||
<option value="3000+">{t("priceRange5")}</option>
|
|
||||||
</select>
|
|
||||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
|
||||||
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-end">
|
|
||||||
<motion.button
|
|
||||||
className="w-full bg-amber-500 hover:bg-amber-600 text-white font-bold py-3 px-6 rounded-lg transition-all duration-300 flex items-center justify-center text-sm"
|
|
||||||
variants={buttonHover}
|
|
||||||
initial="rest"
|
|
||||||
whileHover="hover"
|
|
||||||
whileTap="tap"
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
||||||
</svg>
|
|
||||||
{t("searchButton")}
|
|
||||||
</motion.button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{!showMap && (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="absolute bottom-8 left-1/2 transform -translate-x-1/2"
|
className="absolute bottom-8 left-1/2 transform -translate-x-1/2 cursor-pointer"
|
||||||
animate={{
|
animate={{
|
||||||
y: [0, 10, 0],
|
y: [0, 10, 0],
|
||||||
}}
|
}}
|
||||||
transition={{
|
transition={{
|
||||||
duration: 1.5,
|
duration: 1.5,
|
||||||
repeat: Infinity,
|
repeat: Infinity,
|
||||||
ease: "easeInOut"
|
ease: "easeInOut"
|
||||||
}}
|
}}
|
||||||
>
|
onClick={() => window.scrollTo({
|
||||||
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
top: window.innerHeight,
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
behavior: 'smooth'
|
||||||
</svg>
|
})}
|
||||||
</motion.div>
|
>
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
||||||
|
</svg>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
<section className="py-20 bg-gray-50">
|
{showMap && (
|
||||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
<motion.section
|
||||||
<motion.div
|
ref={mapSectionRef}
|
||||||
className="text-center mb-16"
|
initial={{ opacity: 0, y: 50 }}
|
||||||
initial={{ opacity: 0, y: 20 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
exit={{ opacity: 0, y: -50 }}
|
||||||
viewport={{ once: true, amount: 0.5 }}
|
transition={{
|
||||||
transition={{ duration: 0.6 }}
|
type: "spring",
|
||||||
|
damping: 20,
|
||||||
|
stiffness: 100,
|
||||||
|
duration: 0.6
|
||||||
|
}}
|
||||||
|
className="py-12 bg-gray-50 relative"
|
||||||
>
|
>
|
||||||
<h2 className="text-2xl md:text-3xl font-bold text-gray-900 mb-4 tracking-tight">
|
{isScrolling && (
|
||||||
{t("whyChooseUsTitle")}
|
|
||||||
</h2>
|
|
||||||
<p className="text-gray-600 max-w-2xl mx-auto text-base">
|
|
||||||
{t("whyChooseUsSubtitle")}
|
|
||||||
</p>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
className="grid grid-cols-1 md:grid-cols-3 gap-8"
|
|
||||||
variants={staggerContainer}
|
|
||||||
initial="hidden"
|
|
||||||
whileInView="visible"
|
|
||||||
viewport={{ once: true, amount: 0.2 }}
|
|
||||||
>
|
|
||||||
<motion.div
|
|
||||||
className="bg-white p-6 rounded-xl shadow-lg hover:shadow-xl transition-shadow duration-300 cursor-pointer overflow-hidden relative group"
|
|
||||||
variants={fadeInUp}
|
|
||||||
initial="rest"
|
|
||||||
whileHover={{ y: -5 }}
|
|
||||||
animate="rest"
|
|
||||||
>
|
|
||||||
<motion.div
|
<motion.div
|
||||||
className="absolute inset-0 bg-gradient-to-br from-amber-500/5 to-amber-600/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"
|
className="absolute top-0 left-0 right-0 h-1 bg-amber-500 z-10"
|
||||||
initial={{ scale: 0.5, opacity: 0 }}
|
initial={{ scaleX: 0 }}
|
||||||
whileHover={{ scale: 1, opacity: 1 }}
|
animate={{ scaleX: 1 }}
|
||||||
transition={{ duration: 0.4 }}
|
transition={{ duration: 1, ease: "easeInOut" }}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<div
|
|
||||||
className="w-12 h-12 bg-amber-100 rounded-lg flex items-center justify-center mb-4 relative z-10"
|
|
||||||
>
|
|
||||||
<svg className="w-6 h-6 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3
|
|
||||||
className="text-lg font-bold text-gray-900 mb-3 relative z-10"
|
|
||||||
>
|
|
||||||
{t("feature1Title")}
|
|
||||||
</h3>
|
|
||||||
<p
|
|
||||||
className="text-gray-600 relative z-10 text-sm leading-relaxed"
|
|
||||||
>
|
|
||||||
{t("feature1Description")}
|
|
||||||
</p>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
<div className="container mx-auto px-4">
|
||||||
className="bg-white p-6 rounded-xl shadow-lg hover:shadow-xl transition-shadow duration-300 cursor-pointer overflow-hidden relative group"
|
<motion.div
|
||||||
variants={fadeInUp}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
initial="rest"
|
animate={{ opacity: 1, y: 0 }}
|
||||||
whileHover={{ y: -5 }}
|
transition={{ delay: 0.2 }}
|
||||||
animate="rest"
|
className="text-center mb-8"
|
||||||
>
|
|
||||||
<motion.div
|
|
||||||
className="absolute inset-0 bg-gradient-to-br from-amber-500/5 to-amber-600/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"
|
|
||||||
initial={{ scale: 0.5, opacity: 0 }}
|
|
||||||
whileHover={{ scale: 1, opacity: 1 }}
|
|
||||||
transition={{ duration: 0.4, delay: 0.1 }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="w-12 h-12 bg-amber-100 rounded-lg flex items-center justify-center mb-4 relative z-10"
|
|
||||||
>
|
>
|
||||||
<svg className="w-6 h-6 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div className="flex items-center justify-center gap-4 mb-2">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
<h2 className="text-3xl font-bold text-gray-900">
|
||||||
</svg>
|
{filteredProperties.length > 0 ? 'نتائج البحث' : 'لا توجد نتائج'}
|
||||||
</div>
|
</h2>
|
||||||
|
<motion.button
|
||||||
<h3
|
onClick={resetSearch}
|
||||||
className="text-lg font-bold text-gray-900 mb-3 relative z-10"
|
className="px-4 py-2 bg-white border border-gray-300 rounded-full text-sm font-medium text-gray-700 hover:bg-gray-50 shadow-sm flex items-center gap-2"
|
||||||
>
|
whileHover={{ scale: 1.05 }}
|
||||||
{t("feature2Title")}
|
whileTap={{ scale: 0.95 }}
|
||||||
</h3>
|
>
|
||||||
<p
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
className="text-gray-600 relative z-10 text-sm leading-relaxed"
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||||
>
|
</svg>
|
||||||
{t("feature2Description")}
|
بحث جديد
|
||||||
</p>
|
</motion.button>
|
||||||
</motion.div>
|
</div>
|
||||||
|
|
||||||
<motion.div
|
{filteredProperties.length > 0 ? (
|
||||||
className="bg-white p-6 rounded-xl shadow-lg hover:shadow-xl transition-shadow duration-300 cursor-pointer overflow-hidden relative group"
|
<p className="text-gray-600">
|
||||||
variants={fadeInUp}
|
تم العثور على {filteredProperties.length} عقار يطابق معايير البحث
|
||||||
initial="rest"
|
</p>
|
||||||
whileHover={{ y: -5 }}
|
) : (
|
||||||
animate="rest"
|
<p className="text-gray-600">
|
||||||
>
|
لا توجد عقارات تطابق معايير البحث. جرب تغيير الفلاتر.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="absolute inset-0 bg-gradient-to-br from-amber-500/5 to-amber-600/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"
|
className="bg-white rounded-2xl shadow-xl overflow-hidden border border-gray-200"
|
||||||
initial={{ scale: 0.5, opacity: 0 }}
|
initial={{ scale: 0.95, opacity: 0 }}
|
||||||
whileHover={{ scale: 1, opacity: 1 }}
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
transition={{ duration: 0.4, delay: 0.2 }}
|
transition={{ delay: 0.3, type: "spring" }}
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="w-12 h-12 bg-amber-100 rounded-lg flex items-center justify-center mb-4 relative z-10"
|
|
||||||
>
|
>
|
||||||
<svg className="w-6 h-6 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
{filteredProperties.length > 0 ? (
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
<PropertyMap
|
||||||
</svg>
|
properties={filteredProperties}
|
||||||
</div>
|
userIdentity={searchFilters?.identityType || 'syrian'}
|
||||||
|
/>
|
||||||
<h3
|
) : (
|
||||||
className="text-lg font-bold text-gray-900 mb-3 relative z-10"
|
<div className="h-[400px] flex flex-col items-center justify-center bg-gray-50">
|
||||||
>
|
<div className="w-24 h-24 bg-gray-200 rounded-full flex items-center justify-center mb-4">
|
||||||
{t("feature3Title")}
|
<svg className="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
</h3>
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
<p
|
</svg>
|
||||||
className="text-gray-600 relative z-10 text-sm leading-relaxed"
|
</div>
|
||||||
>
|
<h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد نتائج</h3>
|
||||||
{t("feature3Description")}
|
<p className="text-gray-500">حاول تغيير معايير البحث</p>
|
||||||
</p>
|
</div>
|
||||||
</motion.div>
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
{filteredProperties.length > 0 && searchFilters && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.5 }}
|
||||||
|
className="mt-6 flex flex-wrap gap-3 justify-center"
|
||||||
|
>
|
||||||
|
<div className="bg-white px-4 py-2 rounded-full shadow-sm border border-gray-200 text-sm">
|
||||||
|
<span className="text-gray-600">المدينة: </span>
|
||||||
|
<span className="font-bold text-gray-900">
|
||||||
|
{searchFilters.city === 'all' ? 'جميع المدن' : searchFilters.city}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white px-4 py-2 rounded-full shadow-sm border border-gray-200 text-sm">
|
||||||
|
<span className="text-gray-600">نوع العقار: </span>
|
||||||
|
<span className="font-bold text-gray-900">
|
||||||
|
{searchFilters.propertyType === 'all' ? 'الكل' :
|
||||||
|
searchFilters.propertyType === 'apartment' ? 'شقة' :
|
||||||
|
searchFilters.propertyType === 'villa' ? 'فيلا' : 'بيت'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white px-4 py-2 rounded-full shadow-sm border border-gray-200 text-sm">
|
||||||
|
<span className="text-gray-600">نطاق السعر: </span>
|
||||||
|
<span className="font-bold text-gray-900">
|
||||||
|
{searchFilters.priceRange === 'all' ? 'جميع الأسعار' :
|
||||||
|
searchFilters.priceRange === '0-500' ? 'أقل من 50$' :
|
||||||
|
searchFilters.priceRange === '500-1000' ? '50$ - 100$' :
|
||||||
|
searchFilters.priceRange === '1000-2000' ? '100$ - 200$' :
|
||||||
|
searchFilters.priceRange === '2000-3000' ? '200$ - 300$' : 'أكثر من 300$'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.section>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
<section className="py-20 bg-gradient-to-b from-white to-gray-50">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<motion.div
|
||||||
|
className="text-center mb-12"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
>
|
||||||
|
<div className="inline-block px-4 py-1 bg-amber-100 text-amber-700 rounded-full text-sm font-medium mb-4">
|
||||||
|
لماذا نحن؟
|
||||||
|
</div>
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4 tracking-tight">
|
||||||
|
{t("whyChooseUsTitle")}
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600 max-w-2xl mx-auto text-lg">
|
||||||
|
{t("whyChooseUsSubtitle")}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<motion.div
|
||||||
|
className="group bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.1 }}
|
||||||
|
whileHover={{ y: -4 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4 mb-4">
|
||||||
|
<div className="w-12 h-12 bg-amber-100 rounded-xl flex items-center justify-center group-hover:bg-amber-200 transition-colors duration-300">
|
||||||
|
<ShieldCheck className="w-6 h-6 text-amber-600" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-bold text-gray-900">
|
||||||
|
{t("feature1Title")}
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
<p className="text-gray-600 text-sm leading-relaxed">
|
||||||
|
{t("feature1Description")}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
className="group bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.2 }}
|
||||||
|
whileHover={{ y: -4 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4 mb-4">
|
||||||
|
<div className="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center group-hover:bg-blue-200 transition-colors duration-300">
|
||||||
|
<Lock className="w-6 h-6 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-bold text-gray-900">
|
||||||
|
{t("feature2Title")}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-gray-600 text-sm leading-relaxed">
|
||||||
|
{t("feature2Description")}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
className="group bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.3 }}
|
||||||
|
whileHover={{ y: -4 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4 mb-4">
|
||||||
|
<div className="w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center group-hover:bg-green-200 transition-colors duration-300">
|
||||||
|
<Zap className="w-6 h-6 text-green-600" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-bold text-gray-900">
|
||||||
|
{t("feature3Title")}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-gray-600 text-sm leading-relaxed">
|
||||||
|
{t("feature3Description")}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -33,6 +33,7 @@ import {
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
|
||||||
const PropertyCard = ({ property, viewMode = 'grid' }) => {
|
const PropertyCard = ({ property, viewMode = 'grid' }) => {
|
||||||
const [isFavorite, setIsFavorite] = useState(false);
|
const [isFavorite, setIsFavorite] = useState(false);
|
||||||
const [currentImage, setCurrentImage] = useState(0);
|
const [currentImage, setCurrentImage] = useState(0);
|
||||||
@ -315,11 +316,11 @@ const FilterBar = ({ filters, onFilterChange }) => {
|
|||||||
|
|
||||||
const cities = [
|
const cities = [
|
||||||
{ id: 'all', label: 'جميع المدن' },
|
{ id: 'all', label: 'جميع المدن' },
|
||||||
{ id: 'damascus', label: 'دمشق' },
|
{ id: 'دمشق', label: 'دمشق' },
|
||||||
{ id: 'aleppo', label: 'حلب' },
|
{ id: 'حلب', label: 'حلب' },
|
||||||
{ id: 'homs', label: 'حمص' },
|
{ id: 'حمص', label: 'حمص' },
|
||||||
{ id: 'latakia', label: 'اللاذقية' },
|
{ id: 'اللاذقية', label: 'اللاذقية' },
|
||||||
{ id: 'daraa', label: 'درعا' }
|
{ id: 'درعا', label: 'درعا' }
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -495,7 +496,7 @@ const FilterBar = ({ filters, onFilterChange }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function PropertiesPage() {
|
export default function PropertiesPage() {
|
||||||
const [viewMode, setViewMode] = useState('grid');
|
const [viewMode, setViewMode] = useState('grid');
|
||||||
const [sortBy, setSortBy] = useState('newest');
|
const [sortBy, setSortBy] = useState('newest');
|
||||||
const [filters, setFilters] = useState({
|
const [filters, setFilters] = useState({
|
||||||
search: '',
|
search: '',
|
||||||
@ -670,6 +671,7 @@ export default function PropertiesPage() {
|
|||||||
className={`p-2 rounded-xl transition-colors ${
|
className={`p-2 rounded-xl transition-colors ${
|
||||||
viewMode === 'grid' ? 'bg-gray-800 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
viewMode === 'grid' ? 'bg-gray-800 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||||
}`}
|
}`}
|
||||||
|
title="عرض شبكي"
|
||||||
>
|
>
|
||||||
<Grid3x3 className="w-5 h-5" />
|
<Grid3x3 className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
@ -678,6 +680,7 @@ export default function PropertiesPage() {
|
|||||||
className={`p-2 rounded-xl transition-colors ${
|
className={`p-2 rounded-xl transition-colors ${
|
||||||
viewMode === 'list' ? 'bg-gray-800 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
viewMode === 'list' ? 'bg-gray-800 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||||
}`}
|
}`}
|
||||||
|
title="عرض قائمة"
|
||||||
>
|
>
|
||||||
<List className="w-5 h-5" />
|
<List className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
35
package-lock.json
generated
35
package-lock.json
generated
@ -13,11 +13,13 @@
|
|||||||
"framer-motion": "^12.29.2",
|
"framer-motion": "^12.29.2",
|
||||||
"i18next": "^25.8.0",
|
"i18next": "^25.8.0",
|
||||||
"i18next-browser-languagedetector": "^8.2.0",
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
"lucide-react": "^0.563.0",
|
"lucide-react": "^0.563.0",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
"react-i18next": "^16.5.4"
|
"react-i18next": "^16.5.4",
|
||||||
|
"react-leaflet": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
@ -851,6 +853,17 @@
|
|||||||
"url": "https://opencollective.com/popperjs"
|
"url": "https://opencollective.com/popperjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-leaflet/core": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==",
|
||||||
|
"license": "Hippocratic-2.1",
|
||||||
|
"peerDependencies": {
|
||||||
|
"leaflet": "^1.9.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rollup/plugin-node-resolve": {
|
"node_modules/@rollup/plugin-node-resolve": {
|
||||||
"version": "15.3.1",
|
"version": "15.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz",
|
||||||
@ -1886,6 +1899,12 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/leaflet": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
"node_modules/lightningcss": {
|
"node_modules/lightningcss": {
|
||||||
"version": "1.30.2",
|
"version": "1.30.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
|
||||||
@ -2471,6 +2490,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-leaflet": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==",
|
||||||
|
"license": "Hippocratic-2.1",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-leaflet/core": "^3.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"leaflet": "^1.9.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
|
|||||||
@ -13,11 +13,13 @@
|
|||||||
"framer-motion": "^12.29.2",
|
"framer-motion": "^12.29.2",
|
||||||
"i18next": "^25.8.0",
|
"i18next": "^25.8.0",
|
||||||
"i18next-browser-languagedetector": "^8.2.0",
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
"lucide-react": "^0.563.0",
|
"lucide-react": "^0.563.0",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
"react-i18next": "^16.5.4"
|
"react-i18next": "^16.5.4",
|
||||||
|
"react-leaflet": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
|||||||
Reference in New Issue
Block a user