1177 lines
42 KiB
JavaScript
1177 lines
42 KiB
JavaScript
'use client';
|
|
|
|
import { useState, useRef, useEffect } from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { useRouter } from 'next/navigation';
|
|
import Link from 'next/link';
|
|
import Image from 'next/image';
|
|
import dynamic from 'next/dynamic';
|
|
import 'leaflet/dist/leaflet.css';
|
|
import {
|
|
ArrowLeft,
|
|
MapPin,
|
|
Camera,
|
|
X,
|
|
Home,
|
|
Building,
|
|
Bed,
|
|
Bath,
|
|
Square,
|
|
DollarSign,
|
|
Calendar,
|
|
Clock,
|
|
CheckCircle,
|
|
AlertCircle,
|
|
Info,
|
|
ChevronRight,
|
|
ChevronLeft,
|
|
Loader2,
|
|
Upload,
|
|
FileText,
|
|
Shield,
|
|
HelpCircle,
|
|
Search,
|
|
Navigation,
|
|
Wifi,
|
|
Zap,
|
|
Flame,
|
|
Droplets,
|
|
Cigarette,
|
|
Dog,
|
|
Music,
|
|
Star,
|
|
Sofa,
|
|
DoorOpen,
|
|
Warehouse,
|
|
Layers,
|
|
Plus,
|
|
Minus,
|
|
Save,
|
|
Wind,
|
|
Move
|
|
} from 'lucide-react';
|
|
import toast, { Toaster } from 'react-hot-toast';
|
|
|
|
const MapContainer = dynamic(() => import('react-leaflet').then(mod => mod.MapContainer), { ssr: false });
|
|
const TileLayer = dynamic(() => import('react-leaflet').then(mod => mod.TileLayer), { ssr: false });
|
|
const Marker = dynamic(() => import('react-leaflet').then(mod => mod.Marker), { ssr: false });
|
|
const Popup = dynamic(() => import('react-leaflet').then(mod => mod.Popup), { ssr: false });
|
|
const useMapEvents = dynamic(() => import('react-leaflet').then(mod => mod.useMapEvents), { ssr: false });
|
|
|
|
function MapClickHandler({ onMapClick }) {
|
|
const map = useMapEvents({
|
|
click: (e) => {
|
|
const { lat, lng } = e.latlng;
|
|
onMapClick([lat, lng]);
|
|
},
|
|
});
|
|
return null;
|
|
}
|
|
|
|
export default function AddPropertyPage() {
|
|
const router = useRouter();
|
|
|
|
const [step, setStep] = useState(1);
|
|
const totalSteps = 4;
|
|
|
|
const [formData, setFormData] = useState({
|
|
propertyType: 'apartment', // apartment, villa, suite, room
|
|
|
|
furnished: false,
|
|
|
|
bedrooms: 1,
|
|
bathrooms: 1,
|
|
livingRooms: 1,
|
|
|
|
services: {
|
|
electricity: false,
|
|
internet: false,
|
|
heating: false,
|
|
water: false,
|
|
airConditioning: false,
|
|
parking: false,
|
|
elevator: false
|
|
},
|
|
|
|
terms: {
|
|
noSmoking: false,
|
|
noPets: false,
|
|
noParties: false,
|
|
noAlcohol: false,
|
|
suitableForChildren: true,
|
|
suitableForElderly: true
|
|
},
|
|
|
|
offerType: 'daily',
|
|
|
|
dailyPrice: '',
|
|
monthlyPrice: '',
|
|
salePrice: '',
|
|
|
|
city: '',
|
|
district: '',
|
|
address: '',
|
|
lat: null,
|
|
lng: null,
|
|
|
|
description: '',
|
|
|
|
images: []
|
|
});
|
|
|
|
const [imagePreviews, setImagePreviews] = useState([]);
|
|
|
|
const [selectedLocation, setSelectedLocation] = useState(null);
|
|
const [mapCenter, setMapCenter] = useState([33.5138, 36.2765]);
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [mapLoaded, setMapLoaded] = useState(false);
|
|
|
|
const [errors, setErrors] = useState({});
|
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
const fileInputRef = useRef(null);
|
|
|
|
const propertyTypes = [
|
|
{ id: 'apartment', label: 'شقة', icon: Building },
|
|
{ id: 'villa', label: 'فيلا', icon: Home },
|
|
{ id: 'suite', label: 'سويت', icon: Sofa },
|
|
{ id: 'room', label: 'غرفة ضمن شقة', icon: DoorOpen }
|
|
];
|
|
|
|
const serviceList = [
|
|
{ id: 'electricity', label: 'كهرباء', icon: Zap },
|
|
{ id: 'internet', label: 'انترنت', icon: Wifi },
|
|
{ id: 'heating', label: 'تدفئة', icon: Flame },
|
|
{ id: 'water', label: 'ماء', icon: Droplets },
|
|
{ id: 'airConditioning', label: 'تكييف', icon: Wind },
|
|
{ id: 'parking', label: 'موقف سيارات', icon: Warehouse },
|
|
{ id: 'elevator', label: 'مصعد', icon: Layers }
|
|
];
|
|
|
|
const termsList = [
|
|
{ id: 'noSmoking', label: 'ممنوع التدخين', icon: Cigarette },
|
|
{ id: 'noPets', label: 'ممنوع الحيوانات', icon: Dog },
|
|
{ id: 'noParties', label: 'عدم إقامة حفلات', icon: Music },
|
|
{ id: 'noAlcohol', label: 'ممنوع الكحول', icon: X },
|
|
{ id: 'suitableForChildren', label: 'مناسب للأطفال', icon: Star },
|
|
{ id: 'suitableForElderly', label: 'مناسب لكبار السن', icon: Star }
|
|
];
|
|
|
|
const offerTypes = [
|
|
{ id: 'daily', label: 'إيجار يومي', icon: Clock },
|
|
{ id: 'monthly', label: 'إيجار شهري', icon: Calendar },
|
|
{ id: 'both', label: 'إيجار يومي وشهري', icon: Calendar },
|
|
{ id: 'sale', label: 'للبيع', icon: DollarSign }
|
|
];
|
|
|
|
useEffect(() => {
|
|
if (typeof window !== 'undefined') {
|
|
const L = require('leaflet');
|
|
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',
|
|
});
|
|
}
|
|
setMapLoaded(true);
|
|
}, []);
|
|
|
|
const handleSearch = async () => {
|
|
if (!searchQuery) return;
|
|
|
|
toast.loading('جاري البحث...', { id: 'search' });
|
|
|
|
try {
|
|
const response = await fetch(
|
|
`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(searchQuery)}&limit=1&accept-language=ar`
|
|
);
|
|
const data = await response.json();
|
|
|
|
if (data && data.length > 0) {
|
|
const result = data[0];
|
|
const lat = parseFloat(result.lat);
|
|
const lng = parseFloat(result.lon);
|
|
|
|
setMapCenter([lat, lng]);
|
|
setMapZoom(18);
|
|
|
|
const addressResponse = await fetch(
|
|
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&accept-language=ar`
|
|
);
|
|
const addressData = await addressResponse.json();
|
|
|
|
setSelectedLocation({
|
|
lat: lat,
|
|
lng: lng,
|
|
address: addressData.display_name || result.display_name
|
|
});
|
|
|
|
toast.success('تم العثور على الموقع', { id: 'search' });
|
|
} else {
|
|
toast.error('لم يتم العثور على العنوان', { id: 'search' });
|
|
}
|
|
} catch (error) {
|
|
console.error('خطأ في البحث:', error);
|
|
toast.error('حدث خطأ في البحث', { id: 'search' });
|
|
}
|
|
};
|
|
|
|
const handleGeolocation = () => {
|
|
if (!navigator.geolocation) {
|
|
toast.error('المتصفح لا يدعم تحديد الموقع');
|
|
return;
|
|
}
|
|
|
|
toast.loading('جاري تحديد موقعك...', { id: 'geolocation' });
|
|
|
|
navigator.geolocation.getCurrentPosition(
|
|
async (position) => {
|
|
const { latitude, longitude } = position.coords;
|
|
setMapCenter([latitude, longitude]);
|
|
setMapZoom(18);
|
|
|
|
try {
|
|
const response = await fetch(
|
|
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}&accept-language=ar`
|
|
);
|
|
const data = await response.json();
|
|
|
|
setSelectedLocation({
|
|
lat: latitude,
|
|
lng: longitude,
|
|
address: data.display_name || 'موقعك الحالي'
|
|
});
|
|
|
|
toast.success('تم تحديد موقعك', { id: 'geolocation' });
|
|
} catch (error) {
|
|
setSelectedLocation({
|
|
lat: latitude,
|
|
lng: longitude,
|
|
address: 'موقعك الحالي'
|
|
});
|
|
toast.success('تم تحديد موقعك', { id: 'geolocation' });
|
|
}
|
|
},
|
|
(error) => {
|
|
toast.error('فشل في تحديد الموقع', { id: 'geolocation' });
|
|
}
|
|
);
|
|
};
|
|
|
|
const handleMapClick = async (coords) => {
|
|
try {
|
|
const [lat, lng] = coords;
|
|
|
|
toast.loading('جاري تحديد الموقع...', { id: 'location' });
|
|
|
|
const response = await fetch(
|
|
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&accept-language=ar`
|
|
);
|
|
const data = await response.json();
|
|
|
|
setSelectedLocation({
|
|
lat: lat,
|
|
lng: lng,
|
|
address: data.display_name || 'موقع محدد'
|
|
});
|
|
|
|
setMapZoom(18);
|
|
toast.success('تم تحديد الموقع بنجاح!', { id: 'location' });
|
|
|
|
} catch (error) {
|
|
console.error('خطأ في تحديد الموقع:', error);
|
|
const [lat, lng] = coords;
|
|
setSelectedLocation({
|
|
lat: lat,
|
|
lng: lng,
|
|
address: 'موقع محدد'
|
|
});
|
|
setMapZoom(18);
|
|
toast.success('تم تحديد الموقع', { id: 'location' });
|
|
}
|
|
};
|
|
const confirmLocation = () => {
|
|
if (selectedLocation) {
|
|
setFormData({
|
|
...formData,
|
|
lat: selectedLocation.lat,
|
|
lng: selectedLocation.lng,
|
|
address: selectedLocation.address
|
|
});
|
|
|
|
toast.success('تم تأكيد الموقع بنجاح');
|
|
}
|
|
};
|
|
|
|
const resetLocation = () => {
|
|
setSelectedLocation(null);
|
|
setFormData({
|
|
...formData,
|
|
lat: null,
|
|
lng: null,
|
|
address: ''
|
|
});
|
|
setMapZoom(15);
|
|
toast.info('تم إلغاء تحديد الموقع');
|
|
};
|
|
|
|
const handleImageUpload = (files) => {
|
|
const newImages = Array.from(files);
|
|
|
|
if (formData.images.length + newImages.length > 10) {
|
|
toast.error('يمكنك رفع 10 صور كحد أقصى');
|
|
return;
|
|
}
|
|
|
|
newImages.forEach(file => {
|
|
if (!file.type.startsWith('image/')) {
|
|
toast.error('الرجاء اختيار صور صالحة فقط');
|
|
return;
|
|
}
|
|
|
|
if (file.size > 5 * 1024 * 1024) {
|
|
toast.error('حجم الصورة يجب أن يكون أقل من 5 ميجابايت');
|
|
return;
|
|
}
|
|
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => {
|
|
setImagePreviews(prev => [...prev, reader.result]);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
|
|
setFormData({
|
|
...formData,
|
|
images: [...formData.images, file]
|
|
});
|
|
});
|
|
};
|
|
|
|
const removeImage = (index) => {
|
|
const newImages = [...formData.images];
|
|
newImages.splice(index, 1);
|
|
|
|
const newPreviews = [...imagePreviews];
|
|
newPreviews.splice(index, 1);
|
|
|
|
setFormData({
|
|
...formData,
|
|
images: newImages
|
|
});
|
|
setImagePreviews(newPreviews);
|
|
};
|
|
|
|
const toggleService = (serviceId) => {
|
|
setFormData({
|
|
...formData,
|
|
services: {
|
|
...formData.services,
|
|
[serviceId]: !formData.services[serviceId]
|
|
}
|
|
});
|
|
};
|
|
|
|
const toggleTerm = (termId) => {
|
|
setFormData({
|
|
...formData,
|
|
terms: {
|
|
...formData.terms,
|
|
[termId]: !formData.terms[termId]
|
|
}
|
|
});
|
|
};
|
|
|
|
const incrementBedrooms = () => {
|
|
setFormData({
|
|
...formData,
|
|
bedrooms: formData.bedrooms + 1
|
|
});
|
|
};
|
|
|
|
const decrementBedrooms = () => {
|
|
if (formData.bedrooms > 1) {
|
|
setFormData({
|
|
...formData,
|
|
bedrooms: formData.bedrooms - 1
|
|
});
|
|
}
|
|
};
|
|
|
|
const incrementBathrooms = () => {
|
|
setFormData({
|
|
...formData,
|
|
bathrooms: formData.bathrooms + 1
|
|
});
|
|
};
|
|
|
|
const decrementBathrooms = () => {
|
|
if (formData.bathrooms > 1) {
|
|
setFormData({
|
|
...formData,
|
|
bathrooms: formData.bathrooms - 1
|
|
});
|
|
}
|
|
};
|
|
|
|
const incrementLivingRooms = () => {
|
|
setFormData({
|
|
...formData,
|
|
livingRooms: formData.livingRooms + 1
|
|
});
|
|
};
|
|
|
|
const decrementLivingRooms = () => {
|
|
if (formData.livingRooms > 1) {
|
|
setFormData({
|
|
...formData,
|
|
livingRooms: formData.livingRooms - 1
|
|
});
|
|
}
|
|
};
|
|
|
|
const validateStep = () => {
|
|
const newErrors = {};
|
|
|
|
switch(step) {
|
|
case 1:
|
|
if (!formData.propertyType) {
|
|
newErrors.propertyType = 'نوع العقار مطلوب';
|
|
}
|
|
break;
|
|
|
|
case 2:
|
|
if (!formData.bedrooms) {
|
|
newErrors.bedrooms = 'عدد الغرف مطلوب';
|
|
}
|
|
if (!formData.bathrooms) {
|
|
newErrors.bathrooms = 'عدد الحمامات مطلوب';
|
|
}
|
|
if (!formData.livingRooms) {
|
|
newErrors.livingRooms = 'عدد الصالونات مطلوب';
|
|
}
|
|
break;
|
|
|
|
case 3:
|
|
if (formData.offerType === 'daily' && !formData.dailyPrice) {
|
|
newErrors.dailyPrice = 'السعر اليومي مطلوب';
|
|
}
|
|
if (formData.offerType === 'monthly' && !formData.monthlyPrice) {
|
|
newErrors.monthlyPrice = 'السعر الشهري مطلوب';
|
|
}
|
|
if (formData.offerType === 'both') {
|
|
if (!formData.dailyPrice) newErrors.dailyPrice = 'السعر اليومي مطلوب';
|
|
if (!formData.monthlyPrice) newErrors.monthlyPrice = 'السعر الشهري مطلوب';
|
|
}
|
|
if (formData.offerType === 'sale' && !formData.salePrice) {
|
|
newErrors.salePrice = 'سعر البيع مطلوب';
|
|
}
|
|
break;
|
|
|
|
case 4:
|
|
if (!formData.lat || !formData.lng) {
|
|
newErrors.location = 'الرجاء تحديد موقع العقار على الخريطة';
|
|
}
|
|
if (formData.images.length === 0) {
|
|
newErrors.images = 'يجب رفع صورة واحدة على الأقل';
|
|
}
|
|
break;
|
|
}
|
|
|
|
setErrors(newErrors);
|
|
return Object.keys(newErrors).length === 0;
|
|
};
|
|
|
|
const handleNext = () => {
|
|
if (validateStep()) {
|
|
setStep(step + 1);
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
}
|
|
};
|
|
|
|
const handleBack = () => {
|
|
setStep(step - 1);
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
if (!validateStep()) return;
|
|
|
|
setIsLoading(true);
|
|
|
|
setTimeout(() => {
|
|
console.log('Property Data:', formData);
|
|
setIsLoading(false);
|
|
toast.success('تم إضافة العقار بنجاح!');
|
|
|
|
setTimeout(() => {
|
|
router.push('/owner/properties');
|
|
}, 1500);
|
|
}, 2000);
|
|
};
|
|
|
|
const fadeInUp = {
|
|
initial: { opacity: 0, y: 20 },
|
|
animate: { opacity: 1, y: 0 },
|
|
transition: { duration: 0.5 }
|
|
};
|
|
|
|
function MapClickHandler({ onMapClick }) {
|
|
const map = useMapEvents({
|
|
dblclick: (e) => {
|
|
const { lat, lng } = e.latlng;
|
|
onMapClick([lat, lng]);
|
|
},
|
|
});
|
|
return null;
|
|
}
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 py-8">
|
|
<Toaster position="top-center" reverseOrder={false} />
|
|
|
|
<div className="container mx-auto px-4 max-w-4xl">
|
|
<div className="mb-8">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<Link
|
|
href="/owner/properties"
|
|
className="flex items-center gap-2 text-gray-600 hover:text-amber-600 transition-colors group"
|
|
>
|
|
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
|
|
<span>العودة للعقارات</span>
|
|
</Link>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm font-medium text-amber-600">خطوة {step} من {totalSteps}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
{[1, 2, 3, 4].map((s) => (
|
|
<motion.div
|
|
key={s}
|
|
className={`h-2 flex-1 rounded-full ${
|
|
s <= step ? 'bg-amber-500' : 'bg-gray-200'
|
|
}`}
|
|
animate={{ scaleX: s <= step ? 1 : 0.5 }}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
<div className="flex justify-between mt-2 text-xs text-gray-500">
|
|
<span>معلومات العقار</span>
|
|
<span>التفاصيل والخدمات</span>
|
|
<span>السعر</span>
|
|
<span>الموقع والصور</span>
|
|
</div>
|
|
</div>
|
|
|
|
<motion.div
|
|
key={step}
|
|
initial={{ opacity: 0, x: 20 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
exit={{ opacity: 0, x: -20 }}
|
|
transition={{ duration: 0.3 }}
|
|
className="bg-white rounded-2xl shadow-xl p-6 md:p-8"
|
|
>
|
|
{step === 1 && (
|
|
<motion.div variants={fadeInUp} className="space-y-8">
|
|
<div className="text-center mb-6">
|
|
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
|
<Home className="w-10 h-10 text-amber-600" />
|
|
</div>
|
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">معلومات العقار</h2>
|
|
<p className="text-gray-600">اختر نوع العقار والحالة</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-3">
|
|
نوع العقار <span className="text-red-500">*</span>
|
|
</label>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
{propertyTypes.map((type) => {
|
|
const Icon = type.icon;
|
|
return (
|
|
<button
|
|
key={type.id}
|
|
type="button"
|
|
onClick={() => setFormData({...formData, propertyType: type.id})}
|
|
className={`p-4 border rounded-xl flex flex-col items-center gap-2 transition-all ${
|
|
formData.propertyType === type.id
|
|
? 'border-amber-500 bg-amber-50 text-amber-700'
|
|
: 'border-gray-200 hover:border-amber-200 hover:bg-amber-50/50'
|
|
}`}
|
|
>
|
|
<Icon className="w-6 h-6" />
|
|
<span className="text-sm font-medium">{type.label}</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
{errors.propertyType && (
|
|
<p className="text-red-500 text-sm mt-2">{errors.propertyType}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-3">
|
|
حالة العقار
|
|
</label>
|
|
<div className="flex gap-4">
|
|
<label className="flex items-center gap-2 p-3 border rounded-xl cursor-pointer hover:bg-gray-50 flex-1">
|
|
<input
|
|
type="radio"
|
|
name="furnished"
|
|
checked={formData.furnished === true}
|
|
onChange={() => setFormData({...formData, furnished: true})}
|
|
className="w-4 h-4 text-amber-500"
|
|
/>
|
|
<span className="text-gray-700">مفروش</span>
|
|
</label>
|
|
<label className="flex items-center gap-2 p-3 border rounded-xl cursor-pointer hover:bg-gray-50 flex-1">
|
|
<input
|
|
type="radio"
|
|
name="furnished"
|
|
checked={formData.furnished === false}
|
|
onChange={() => setFormData({...formData, furnished: false})}
|
|
className="w-4 h-4 text-amber-500"
|
|
/>
|
|
<span className="text-gray-700">غير مفروش</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
وصف إضافي (اختياري)
|
|
</label>
|
|
<textarea
|
|
value={formData.description}
|
|
onChange={(e) => setFormData({...formData, description: e.target.value})}
|
|
rows="4"
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
|
placeholder="أضف وصفاً إضافياً للعقار..."
|
|
/>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
|
|
{step === 2 && (
|
|
<motion.div variants={fadeInUp} className="space-y-8">
|
|
<div className="text-center mb-6">
|
|
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
|
<Layers className="w-10 h-10 text-amber-600" />
|
|
</div>
|
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">تفاصيل العقار</h2>
|
|
<p className="text-gray-600">أدخل التفاصيل والخدمات المتاحة</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
عدد الغرف <span className="text-red-500">*</span>
|
|
</label>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={decrementBedrooms}
|
|
className="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center hover:bg-gray-200 transition-colors"
|
|
>
|
|
<Minus className="w-4 h-4" />
|
|
</button>
|
|
<div className="flex-1 text-center font-bold text-xl">
|
|
{formData.bedrooms}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={incrementBedrooms}
|
|
className="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center hover:bg-gray-200 transition-colors"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
{errors.bedrooms && (
|
|
<p className="text-red-500 text-sm mt-1">{errors.bedrooms}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
عدد الحمامات <span className="text-red-500">*</span>
|
|
</label>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={decrementBathrooms}
|
|
className="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center hover:bg-gray-200"
|
|
>
|
|
<Minus className="w-4 h-4" />
|
|
</button>
|
|
<div className="flex-1 text-center font-bold text-xl">
|
|
{formData.bathrooms}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={incrementBathrooms}
|
|
className="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center hover:bg-gray-200"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
{errors.bathrooms && (
|
|
<p className="text-red-500 text-sm mt-1">{errors.bathrooms}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
عدد الصالونات <span className="text-red-500">*</span>
|
|
</label>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={decrementLivingRooms}
|
|
className="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center hover:bg-gray-200"
|
|
>
|
|
<Minus className="w-4 h-4" />
|
|
</button>
|
|
<div className="flex-1 text-center font-bold text-xl">
|
|
{formData.livingRooms}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={incrementLivingRooms}
|
|
className="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center hover:bg-gray-200"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
{errors.livingRooms && (
|
|
<p className="text-red-500 text-sm mt-1">{errors.livingRooms}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h3 className="text-lg font-bold text-gray-900 mb-4">الخدمات المتوفرة</h3>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
{serviceList.map((service) => {
|
|
const Icon = service.icon;
|
|
return (
|
|
<label
|
|
key={service.id}
|
|
className={`flex items-center gap-2 p-3 border rounded-xl cursor-pointer transition-all ${
|
|
formData.services[service.id]
|
|
? 'border-amber-500 bg-amber-50'
|
|
: 'border-gray-200 hover:border-amber-200 hover:bg-amber-50/50'
|
|
}`}
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={formData.services[service.id]}
|
|
onChange={() => toggleService(service.id)}
|
|
className="hidden"
|
|
/>
|
|
<Icon className={`w-5 h-5 ${
|
|
formData.services[service.id] ? 'text-amber-600' : 'text-gray-400'
|
|
}`} />
|
|
<span className={`text-sm ${
|
|
formData.services[service.id] ? 'text-amber-700' : 'text-gray-600'
|
|
}`}>
|
|
{service.label}
|
|
</span>
|
|
</label>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h3 className="text-lg font-bold text-gray-900 mb-4">شروط استخدام العقار</h3>
|
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
|
{termsList.map((term) => {
|
|
const Icon = term.icon;
|
|
return (
|
|
<label
|
|
key={term.id}
|
|
className={`flex items-center gap-2 p-3 border rounded-xl cursor-pointer transition-all ${
|
|
formData.terms[term.id]
|
|
? 'border-amber-500 bg-amber-50'
|
|
: 'border-gray-200 hover:border-amber-200 hover:bg-amber-50/50'
|
|
}`}
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={formData.terms[term.id]}
|
|
onChange={() => toggleTerm(term.id)}
|
|
className="hidden"
|
|
/>
|
|
<Icon className={`w-5 h-5 ${
|
|
formData.terms[term.id] ? 'text-amber-600' : 'text-gray-400'
|
|
}`} />
|
|
<span className={`text-sm ${
|
|
formData.terms[term.id] ? 'text-amber-700' : 'text-gray-600'
|
|
}`}>
|
|
{term.label}
|
|
</span>
|
|
</label>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
|
|
{step === 3 && (
|
|
<motion.div variants={fadeInUp} className="space-y-8">
|
|
<div className="text-center mb-6">
|
|
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
|
<DollarSign className="w-10 h-10 text-amber-600" />
|
|
</div>
|
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">نوع العرض والسعر</h2>
|
|
<p className="text-gray-600">اختر نوع العرض وحدد السعر المناسب</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-3">
|
|
نوع العرض <span className="text-red-500">*</span>
|
|
</label>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
{offerTypes.map((type) => {
|
|
const Icon = type.icon;
|
|
return (
|
|
<button
|
|
key={type.id}
|
|
type="button"
|
|
onClick={() => setFormData({...formData, offerType: type.id})}
|
|
className={`p-4 border rounded-xl flex flex-col items-center gap-2 transition-all ${
|
|
formData.offerType === type.id
|
|
? 'border-amber-500 bg-amber-50 text-amber-700'
|
|
: 'border-gray-200 hover:border-amber-200 hover:bg-amber-50/50'
|
|
}`}
|
|
>
|
|
<Icon className="w-6 h-6" />
|
|
<span className="text-sm font-medium">{type.label}</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<AnimatePresence mode="wait">
|
|
{(formData.offerType === 'daily' || formData.offerType === 'both') && (
|
|
<motion.div
|
|
key="daily"
|
|
initial={{ opacity: 0, height: 0 }}
|
|
animate={{ opacity: 1, height: 'auto' }}
|
|
exit={{ opacity: 0, height: 0 }}
|
|
className="space-y-4"
|
|
>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
السعر اليومي (ل.س) <span className="text-red-500">*</span>
|
|
</label>
|
|
<div className="relative">
|
|
<DollarSign className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
|
<input
|
|
type="number"
|
|
value={formData.dailyPrice}
|
|
onChange={(e) => setFormData({...formData, dailyPrice: e.target.value})}
|
|
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 ${
|
|
errors.dailyPrice ? 'border-red-500' : 'border-gray-300'
|
|
}`}
|
|
placeholder="مثال: 50000"
|
|
/>
|
|
</div>
|
|
{errors.dailyPrice && (
|
|
<p className="text-red-500 text-sm mt-1">{errors.dailyPrice}</p>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
|
|
{(formData.offerType === 'monthly' || formData.offerType === 'both') && (
|
|
<motion.div
|
|
key="monthly"
|
|
initial={{ opacity: 0, height: 0 }}
|
|
animate={{ opacity: 1, height: 'auto' }}
|
|
exit={{ opacity: 0, height: 0 }}
|
|
className="space-y-4"
|
|
>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
السعر الشهري (ل.س) <span className="text-red-500">*</span>
|
|
</label>
|
|
<div className="relative">
|
|
<DollarSign className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
|
<input
|
|
type="number"
|
|
value={formData.monthlyPrice}
|
|
onChange={(e) => setFormData({...formData, monthlyPrice: e.target.value})}
|
|
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 ${
|
|
errors.monthlyPrice ? 'border-red-500' : 'border-gray-300'
|
|
}`}
|
|
placeholder="مثال: 1000000"
|
|
/>
|
|
</div>
|
|
{errors.monthlyPrice && (
|
|
<p className="text-red-500 text-sm mt-1">{errors.monthlyPrice}</p>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
|
|
{formData.offerType === 'sale' && (
|
|
<motion.div
|
|
key="sale"
|
|
initial={{ opacity: 0, height: 0 }}
|
|
animate={{ opacity: 1, height: 'auto' }}
|
|
exit={{ opacity: 0, height: 0 }}
|
|
className="space-y-4"
|
|
>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
سعر البيع (ل.س) <span className="text-red-500">*</span>
|
|
</label>
|
|
<div className="relative">
|
|
<DollarSign className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
|
<input
|
|
type="number"
|
|
value={formData.salePrice}
|
|
onChange={(e) => setFormData({...formData, salePrice: e.target.value})}
|
|
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 ${
|
|
errors.salePrice ? 'border-red-500' : 'border-gray-300'
|
|
}`}
|
|
placeholder="أدخل السعر المطلوب"
|
|
/>
|
|
</div>
|
|
{errors.salePrice && (
|
|
<p className="text-red-500 text-sm mt-1">{errors.salePrice}</p>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</motion.div>
|
|
)}
|
|
|
|
{step === 4 && (
|
|
<motion.div variants={fadeInUp} className="space-y-8">
|
|
<div className="text-center mb-6">
|
|
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
|
<MapPin className="w-10 h-10 text-amber-600" />
|
|
</div>
|
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">الموقع والصور</h2>
|
|
<p className="text-gray-600">حدد موقع العقار وأضف الصور</p>
|
|
</div>
|
|
|
|
<div>
|
|
<h3 className="text-lg font-bold text-gray-900 mb-4">حدد موقع العقار على الخريطة</h3>
|
|
|
|
<div className="flex gap-2 mb-4">
|
|
<div className="flex-1 relative">
|
|
<input
|
|
type="text"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
|
|
placeholder="ابحث عن عنوان..."
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 pr-12"
|
|
/>
|
|
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
|
</div>
|
|
<button
|
|
onClick={handleSearch}
|
|
className="px-6 py-3 bg-amber-500 text-white rounded-xl hover:bg-amber-600 transition-colors"
|
|
>
|
|
بحث
|
|
</button>
|
|
</div>
|
|
|
|
<div className="relative w-full h-96 rounded-xl overflow-hidden border-2 border-gray-200 mb-4">
|
|
{mapLoaded && (
|
|
<MapContainer
|
|
center={mapCenter}
|
|
zoom={mapZoom}
|
|
style={{ height: '100%', width: '100%' }}
|
|
className="z-0"
|
|
doubleClickZoom={false}
|
|
>
|
|
<TileLayer
|
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
|
/>
|
|
|
|
<MapClickHandler onMapClick={handleMapClick} />
|
|
|
|
{selectedLocation && (
|
|
<Marker
|
|
position={[selectedLocation.lat, selectedLocation.lng]}
|
|
draggable={true}
|
|
eventHandlers={{
|
|
dragend: async (e) => {
|
|
const marker = e.target;
|
|
const position = marker.getLatLng();
|
|
|
|
try {
|
|
const response = await fetch(
|
|
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${position.lat}&lon=${position.lng}&accept-language=ar`
|
|
);
|
|
const data = await response.json();
|
|
|
|
setSelectedLocation({
|
|
lat: position.lat,
|
|
lng: position.lng,
|
|
address: data.display_name || 'موقع محدد'
|
|
});
|
|
} catch (error) {
|
|
setSelectedLocation({
|
|
lat: position.lat,
|
|
lng: position.lng,
|
|
address: 'موقع محدد'
|
|
});
|
|
}
|
|
}
|
|
}}
|
|
>
|
|
<Popup>
|
|
<div className="text-right p-2 max-w-xs">
|
|
<p className="font-bold text-sm mb-1">موقع العقار</p>
|
|
<p className="text-xs text-gray-600">{selectedLocation.address}</p>
|
|
</div>
|
|
</Popup>
|
|
</Marker>
|
|
)}
|
|
</MapContainer>
|
|
)}
|
|
|
|
</div>
|
|
|
|
{selectedLocation && !formData.lat && (
|
|
<button
|
|
onClick={confirmLocation}
|
|
className="w-full mb-4 bg-green-500 text-white py-3 rounded-xl font-medium hover:bg-green-600 transition-colors flex items-center justify-center gap-2"
|
|
>
|
|
<CheckCircle className="w-5 h-5" />
|
|
تأكيد هذا الموقع
|
|
</button>
|
|
)}
|
|
|
|
{formData.lat && (
|
|
<div className="bg-green-50 border border-green-200 rounded-xl p-4 mb-4">
|
|
<div className="flex items-center gap-2">
|
|
<CheckCircle className="w-5 h-5 text-green-600" />
|
|
<span className="text-green-800 font-medium">تم تأكيد الموقع:</span>
|
|
</div>
|
|
<p className="text-green-700 text-sm mt-2 line-clamp-2">{formData.address}</p>
|
|
</div>
|
|
)}
|
|
|
|
{errors.location && (
|
|
<p className="text-red-500 text-sm text-center mb-4">{errors.location}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<h3 className="text-lg font-bold text-gray-900 mb-4">صور العقار</h3>
|
|
|
|
<div
|
|
onClick={() => fileInputRef.current?.click()}
|
|
className={`border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-all ${
|
|
errors.images ? 'border-red-500 bg-red-50' : 'border-gray-300 hover:border-amber-500 hover:bg-amber-50'
|
|
}`}
|
|
>
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
multiple
|
|
accept="image/*"
|
|
onChange={(e) => handleImageUpload(e.target.files)}
|
|
className="hidden"
|
|
/>
|
|
|
|
<Upload className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
|
<p className="text-gray-600 font-medium">اضغط لرفع الصور</p>
|
|
<p className="text-xs text-gray-500 mt-2">
|
|
JPEG, PNG, JPG • حتى 5MB • 800x600 بكسل • حد أقصى 10 صور
|
|
</p>
|
|
</div>
|
|
|
|
{errors.images && (
|
|
<p className="text-red-500 text-sm text-center mt-2">{errors.images}</p>
|
|
)}
|
|
|
|
{imagePreviews.length > 0 && (
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-6">
|
|
{imagePreviews.map((preview, index) => (
|
|
<motion.div
|
|
key={index}
|
|
initial={{ opacity: 0, scale: 0.9 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
className="relative group aspect-square"
|
|
>
|
|
<Image
|
|
src={preview}
|
|
alt={`Property ${index + 1}`}
|
|
fill
|
|
className="object-cover rounded-lg"
|
|
/>
|
|
<button
|
|
onClick={() => removeImage(index)}
|
|
className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
|
|
>
|
|
<X className="w-4 h-4 text-white" />
|
|
</button>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
|
|
<div className="flex gap-3 mt-8 pt-6 border-t border-gray-200">
|
|
{step > 1 && (
|
|
<button
|
|
onClick={handleBack}
|
|
className="flex-1 py-3 px-4 bg-gray-100 text-gray-700 rounded-xl font-medium hover:bg-gray-200 transition-colors flex items-center justify-center gap-2"
|
|
>
|
|
<ChevronRight className="w-5 h-5" />
|
|
السابق
|
|
</button>
|
|
)}
|
|
|
|
{step < totalSteps ? (
|
|
<button
|
|
onClick={handleNext}
|
|
className={`flex-1 py-3 px-4 bg-amber-500 text-white rounded-xl font-medium hover:bg-amber-600 transition-colors flex items-center justify-center gap-2 ${
|
|
step === 1 ? 'w-full' : ''
|
|
}`}
|
|
>
|
|
التالي
|
|
<ChevronLeft className="w-5 h-5" />
|
|
</button>
|
|
) : (
|
|
<button
|
|
onClick={handleSubmit}
|
|
disabled={isLoading}
|
|
className="flex-1 py-3 px-4 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-xl font-medium hover:from-amber-600 hover:to-amber-700 transition-all disabled:opacity-50 flex items-center justify-center gap-2"
|
|
>
|
|
{isLoading ? (
|
|
<>
|
|
<Loader2 className="w-5 h-5 animate-spin" />
|
|
جاري الحفظ...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Save className="w-5 h-5" />
|
|
حفظ العقار
|
|
</>
|
|
)}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |