Fix Facebook share: server-side OG metadata via generateMetadata
All checks were successful
Build frontend / build (push) Successful in 41s
All checks were successful
Build frontend / build (push) Successful in 41s
- Split page.js into server component + PropertyDetail client component - Server component exports generateMetadata that fetches property data - OG tags now rendered in initial HTML (visible to Facebook/Twitter crawlers) - Removes client-side useEffect OG tag injection (crawlers don't execute JS)
This commit is contained in:
908
app/property/[id]/PropertyDetail.js
Normal file
908
app/property/[id]/PropertyDetail.js
Normal file
@ -0,0 +1,908 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import toast, { Toaster } from 'react-hot-toast';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useParams } from 'next/navigation';
|
||||||
|
import {
|
||||||
|
MapPin,
|
||||||
|
Bed,
|
||||||
|
Bath,
|
||||||
|
Square,
|
||||||
|
DollarSign,
|
||||||
|
Heart,
|
||||||
|
Share2,
|
||||||
|
Phone,
|
||||||
|
Mail,
|
||||||
|
MessageCircle,
|
||||||
|
Calendar,
|
||||||
|
Shield,
|
||||||
|
Star,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
Check,
|
||||||
|
X,
|
||||||
|
Wifi,
|
||||||
|
Car,
|
||||||
|
Coffee,
|
||||||
|
Wind,
|
||||||
|
Thermometer,
|
||||||
|
Lock,
|
||||||
|
Camera,
|
||||||
|
Home,
|
||||||
|
Building2,
|
||||||
|
Users,
|
||||||
|
Ruler,
|
||||||
|
CalendarDays,
|
||||||
|
Clock,
|
||||||
|
Award,
|
||||||
|
FileText,
|
||||||
|
Printer,
|
||||||
|
Download,
|
||||||
|
ArrowLeft,
|
||||||
|
LogIn
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { getRentProperty, getSaleProperty, bookReservation, checkAvailability, getAvailableDateRanges } from '../../utils/api';
|
||||||
|
import AuthService from '../../services/AuthService';
|
||||||
|
import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from '../../enums';
|
||||||
|
|
||||||
|
// Map API response to the UI format
|
||||||
|
function mapApiDetail(item) {
|
||||||
|
if (!item) return null;
|
||||||
|
|
||||||
|
const info = item.propertyInformation || {};
|
||||||
|
|
||||||
|
const dailyPrice = item.dailyRent ?? item.monthlyRent ?? item.price ?? 0;
|
||||||
|
const monthlyPrice = item.monthlyRent ?? 0;
|
||||||
|
|
||||||
|
const propType = BuildingTypeKeys[info.buildingType] ?? BuildingTypeKeys[item.type] ?? 'apartment';
|
||||||
|
const status = PropertyStatusKeys[info.status] ?? PropertyStatusKeys[item.status] ?? 'available';
|
||||||
|
|
||||||
|
const features = [];
|
||||||
|
if (item.isSmokeAllow) features.push({ name: 'يسمح بالتدخين', available: true, description: '' });
|
||||||
|
if (item.isVisitorAllow) features.push({ name: 'يسمح بالزوار', available: true, description: '' });
|
||||||
|
if (item.specializedFor) features.push({ name: 'متخصص', available: true, description: '' });
|
||||||
|
if (info.numberOfBedRooms) features.push({ name: 'غرف النوم', available: true, description: `${info.numberOfBedRooms} غرف` });
|
||||||
|
if (info.numberOfBathRooms) features.push({ name: 'الحمامات', available: true, description: `${info.numberOfBathRooms} حمامات` });
|
||||||
|
if (info.space) features.push({ name: 'المساحة', available: true, description: `${info.space} م²` });
|
||||||
|
|
||||||
|
const typeLabels = { 0: 'شقة', 1: 'فيلا', 2: 'بيت' };
|
||||||
|
|
||||||
|
// Extract images from API and build full URLs
|
||||||
|
const apiBase = typeof window !== 'undefined' ? (process.env.NEXT_PUBLIC_API_URL || 'http://45.93.137.91/api') : '';
|
||||||
|
const rawImages = Array.isArray(info.images) ? info.images : [];
|
||||||
|
const images = rawImages.length > 0
|
||||||
|
? rawImages.map(img => img.startsWith('http') ? img : `${apiBase}${img.startsWith('/') ? '' : '/'}${img}`)
|
||||||
|
: ['/property-placeholder.jpg', '/villa1.jpg', '/villa2.jpg'];
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
title: info.address || `عقار #${item.id}`,
|
||||||
|
description: info.description || 'عقار سكني مميز في موقع استراتيجي.',
|
||||||
|
type: propType,
|
||||||
|
price: dailyPrice,
|
||||||
|
priceUnit: 'daily',
|
||||||
|
location: {
|
||||||
|
city: extractCity(info.address) || 'دمشق',
|
||||||
|
district: info.address || '',
|
||||||
|
address: info.address || '',
|
||||||
|
lat: parseFloat(info.cordsX) || 0,
|
||||||
|
lng: parseFloat(info.cordsY) || 0,
|
||||||
|
},
|
||||||
|
bedrooms: info.numberOfBedRooms || 0,
|
||||||
|
bathrooms: info.numberOfBathRooms || 0,
|
||||||
|
area: info.space || 0,
|
||||||
|
features: features.length > 0 ? features : [
|
||||||
|
{ name: 'متاح للإيجار', available: true, description: '' },
|
||||||
|
],
|
||||||
|
images,
|
||||||
|
status,
|
||||||
|
rating: item.rating || 4.5,
|
||||||
|
reviews: 0,
|
||||||
|
reviewList: [],
|
||||||
|
owner: {
|
||||||
|
name: 'المالك',
|
||||||
|
phone: '—',
|
||||||
|
email: '—',
|
||||||
|
rating: 4.8,
|
||||||
|
properties: 1,
|
||||||
|
memberSince: '2024',
|
||||||
|
responseRate: '95%',
|
||||||
|
responseTime: 'خلال ساعات',
|
||||||
|
},
|
||||||
|
nearby: [],
|
||||||
|
specifications: {
|
||||||
|
constructionYear: null,
|
||||||
|
floor: '-',
|
||||||
|
parking: 0,
|
||||||
|
gardenArea: 0,
|
||||||
|
poolArea: 0,
|
||||||
|
furnished: false,
|
||||||
|
airConditioning: '-',
|
||||||
|
heating: '-',
|
||||||
|
electricity: '220V',
|
||||||
|
water: 'شبكة عامة',
|
||||||
|
},
|
||||||
|
rules: [],
|
||||||
|
_raw: item,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractCity is now imported from @/app/enums
|
||||||
|
|
||||||
|
// API-only — no fallback data
|
||||||
|
|
||||||
|
export default function PropertyDetailsPage() {
|
||||||
|
const params = useParams();
|
||||||
|
const [currentImage, setCurrentImage] = useState(0);
|
||||||
|
const [showContact, setShowContact] = useState(false);
|
||||||
|
const [bookingDates, setBookingDates] = useState({ start: '', end: '' });
|
||||||
|
const [selectedDuration, setSelectedDuration] = useState(1);
|
||||||
|
const [property, setProperty] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [bookingError, setBookingError] = useState(null);
|
||||||
|
const [bookingSuccess, setBookingSuccess] = useState(false);
|
||||||
|
const [availableRanges, setAvailableRanges] = useState([]);
|
||||||
|
const [calendarMonth, setCalendarMonth] = useState(new Date());
|
||||||
|
const [selectingEnd, setSelectingEnd] = useState(false);
|
||||||
|
const [showLoginDialog, setShowLoginDialog] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const id = params.id;
|
||||||
|
setLoading(true);
|
||||||
|
setBookingError(null);
|
||||||
|
setBookingSuccess(false);
|
||||||
|
|
||||||
|
async function fetchProperty() {
|
||||||
|
try {
|
||||||
|
// Try RentProperties first, then SaleProperties
|
||||||
|
let data = null;
|
||||||
|
try {
|
||||||
|
data = await getRentProperty(id);
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
data = await getSaleProperty(id);
|
||||||
|
} catch {
|
||||||
|
// neither worked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
const mapped = mapApiDetail(data);
|
||||||
|
if (mapped) {
|
||||||
|
setProperty(mapped);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setProperty(null);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Property] Failed to fetch property:', err);
|
||||||
|
setProperty(null);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchProperty();
|
||||||
|
}, [params.id]);
|
||||||
|
|
||||||
|
// Fetch available date ranges
|
||||||
|
useEffect(() => {
|
||||||
|
if (!property) return;
|
||||||
|
const propId = property._raw?.id || params.id;
|
||||||
|
console.log('[Property] Fetching available dates for:', propId);
|
||||||
|
getAvailableDateRanges(propId)
|
||||||
|
.then((data) => {
|
||||||
|
const ranges = Array.isArray(data) ? data : [];
|
||||||
|
console.log('[Property] Available date ranges:', ranges);
|
||||||
|
setAvailableRanges(ranges);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.warn('[Property] Failed to fetch available dates:', err);
|
||||||
|
});
|
||||||
|
}, [property, params.id]);
|
||||||
|
|
||||||
|
// Set Open Graph meta tags dynamically for Facebook/Twitter sharing
|
||||||
|
useEffect(() => {
|
||||||
|
if (!property) return;
|
||||||
|
|
||||||
|
const typeLabel = property.type === 'villa' ? 'فيلا' : property.type === 'apartment' ? 'شقة' : 'بيت';
|
||||||
|
const priceLabel = `${formatCurrency(property.price)} / ${property.priceUnit === 'daily' ? 'يوم' : 'شهر'}`;
|
||||||
|
const desc = `${typeLabel} في ${property.location?.address || ''} · ${property.bedrooms} غرف نوم · ${property.bathrooms} حمامات · ${property.area} م²`;
|
||||||
|
const imageUrl = property.images?.[0]
|
||||||
|
? (property.images[0].startsWith('http') ? property.images[0] : `http://45.93.137.91${property.images[0]}`)
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const setMeta = (prop, content) => {
|
||||||
|
let tag = document.querySelector(`meta[property="${prop}"]`);
|
||||||
|
if (!tag) {
|
||||||
|
tag = document.createElement('meta');
|
||||||
|
tag.setAttribute('property', prop);
|
||||||
|
document.head.appendChild(tag);
|
||||||
|
}
|
||||||
|
tag.setAttribute('content', content);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setMetaName = (name, content) => {
|
||||||
|
let tag = document.querySelector(`meta[name="${name}"]`);
|
||||||
|
if (!tag) {
|
||||||
|
tag = document.createElement('meta');
|
||||||
|
tag.setAttribute('name', name);
|
||||||
|
document.head.appendChild(tag);
|
||||||
|
}
|
||||||
|
tag.setAttribute('content', content);
|
||||||
|
};
|
||||||
|
|
||||||
|
setMeta('og:title', `${property.title} - ${priceLabel}`);
|
||||||
|
setMeta('og:description', desc);
|
||||||
|
if (imageUrl) setMeta('og:image', imageUrl);
|
||||||
|
setMeta('og:url', window.location.href);
|
||||||
|
setMeta('og:type', 'website');
|
||||||
|
setMeta('og:site_name', 'SweetHome');
|
||||||
|
|
||||||
|
// Twitter cards
|
||||||
|
setMetaName('twitter:card', 'summary_large_image');
|
||||||
|
setMetaName('twitter:title', `${property.title} - ${priceLabel}`);
|
||||||
|
setMetaName('twitter:description', desc);
|
||||||
|
if (imageUrl) setMetaName('twitter:image', imageUrl);
|
||||||
|
}, [property]);
|
||||||
|
|
||||||
|
const formatCurrency = (amount) => {
|
||||||
|
return amount?.toLocaleString() + ' ل.س';
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateTotalPrice = () => {
|
||||||
|
if (!property) return 0;
|
||||||
|
const days = bookingDates.start && bookingDates.end
|
||||||
|
? Math.ceil((new Date(bookingDates.end) - new Date(bookingDates.start)) / (1000 * 60 * 60 * 24))
|
||||||
|
: selectedDuration;
|
||||||
|
return property.price * (days > 0 ? days : 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calendar helpers
|
||||||
|
const isDateAvailable = (dateStr) => {
|
||||||
|
const d = new Date(dateStr + 'T00:00:00');
|
||||||
|
return availableRanges.some((range) => {
|
||||||
|
const start = new Date(range.startDate);
|
||||||
|
const end = new Date(range.endDate);
|
||||||
|
return d >= start && d <= end;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isInRange = (dateStr) => {
|
||||||
|
if (!bookingDates.start) return false;
|
||||||
|
const d = new Date(dateStr + 'T00:00:00');
|
||||||
|
const start = new Date(bookingDates.start + 'T00:00:00');
|
||||||
|
const end = bookingDates.end ? new Date(bookingDates.end + 'T00:00:00') : start;
|
||||||
|
return d >= start && d <= end;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isRangeFullyAvailable = (startStr, endStr) => {
|
||||||
|
const start = new Date(startStr + 'T00:00:00');
|
||||||
|
const end = new Date(endStr + 'T00:00:00');
|
||||||
|
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
|
||||||
|
if (!isDateAvailable(d.toISOString().split('T')[0])) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCalendarClick = (dateStr) => {
|
||||||
|
if (!isDateAvailable(dateStr)) return;
|
||||||
|
|
||||||
|
if (!bookingDates.start || selectingEnd) {
|
||||||
|
if (!bookingDates.start) {
|
||||||
|
setBookingDates({ start: dateStr, end: '' });
|
||||||
|
setSelectingEnd(true);
|
||||||
|
} else {
|
||||||
|
const start = bookingDates.start;
|
||||||
|
const end = dateStr;
|
||||||
|
const [s, e] = end > start ? [start, end] : [end, start];
|
||||||
|
if (isRangeFullyAvailable(s, e)) {
|
||||||
|
setBookingDates({ start: s, end: e });
|
||||||
|
setSelectingEnd(false);
|
||||||
|
} else {
|
||||||
|
toast.error('بعض التواريخ في هذه الفترة غير متاحة');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setBookingDates({ start: dateStr, end: '' });
|
||||||
|
setSelectingEnd(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDaysInMonth = (year, month) => new Date(year, month + 1, 0).getDate();
|
||||||
|
const getFirstDayOfMonth = (year, month) => new Date(year, month, 1).getDay();
|
||||||
|
|
||||||
|
const formatDateStr = (year, month, day) => {
|
||||||
|
return `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const monthNames = ['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'];
|
||||||
|
const dayNames = ['أح', 'إث', 'ثل', 'أر', 'خم', 'جم', 'سب'];
|
||||||
|
|
||||||
|
const handleBooking = async () => {
|
||||||
|
if (!AuthService.isAuthenticated()) {
|
||||||
|
setShowLoginDialog(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setBookingError(null);
|
||||||
|
setBookingSuccess(false);
|
||||||
|
|
||||||
|
if (!bookingDates.start || !bookingDates.end) {
|
||||||
|
setBookingError('يرجى اختيار تاريخ البداية والنهاية');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const propId = property?._raw?.id || parseInt(params.id);
|
||||||
|
const startDate = new Date(bookingDates.start).toISOString();
|
||||||
|
const endDate = new Date(bookingDates.end).toISOString();
|
||||||
|
|
||||||
|
console.log('[Booking] Reserving:', { propertyId: propId, startDate, endDate });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await bookReservation(propId, startDate, endDate);
|
||||||
|
console.log('[Booking] Success:', res);
|
||||||
|
setBookingSuccess(true);
|
||||||
|
toast.success('تم إرسال طلب الحجز بنجاح!');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Booking] Failed:', err);
|
||||||
|
setBookingError(err.message || 'فشل في إرسال طلب الحجز');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-16 h-16 border-4 border-gray-200 border-t-gray-800 rounded-full animate-spin mx-auto mb-4"></div>
|
||||||
|
<p className="text-gray-600">جاري تحميل تفاصيل العقار...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!property) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<Home className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">العقار غير موجود</h2>
|
||||||
|
<p className="text-gray-600 mb-4">لم نتمكن من العثور على العقار المطلوب</p>
|
||||||
|
<Link href="/properties" className="bg-gray-800 text-white px-6 py-3 rounded-xl font-medium hover:bg-gray-900 transition-colors">
|
||||||
|
العودة إلى العقارات
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
<div className="bg-white border-b sticky top-16 z-40 shadow-sm">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<div className="flex items-center justify-between h-16">
|
||||||
|
<Link href="/properties" className="flex items-center gap-2 text-gray-600 hover:text-gray-900 transition-colors">
|
||||||
|
<ArrowLeft className="w-5 h-5" />
|
||||||
|
<span>العودة إلى العقارات</span>
|
||||||
|
</Link>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button className="p-2 hover:bg-gray-100 rounded-full transition-colors">
|
||||||
|
<Heart className="w-5 h-5 text-gray-600" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const typeLabel = property.type === 'villa' ? 'فيلا' : property.type === 'apartment' ? 'شقة' : 'بيت';
|
||||||
|
const priceLabel = `${formatCurrency(property.price)} / ${property.priceUnit === 'daily' ? 'يوم' : 'شهر'}`;
|
||||||
|
const lines = [
|
||||||
|
`🏠 ${typeLabel} مفروشة في ${property.location?.address || ''}`,
|
||||||
|
`💰 ${priceLabel}`,
|
||||||
|
`🛏️ ${property.bedrooms} غرف نوم | 🚿 ${property.bathrooms} حمامات | 📐 ${property.area} م²`,
|
||||||
|
property.description ? `📝 ${property.description.substring(0, 80)}...` : '',
|
||||||
|
'',
|
||||||
|
'🔥 احجز الآن على SweetHome',
|
||||||
|
].filter(Boolean);
|
||||||
|
const quote = encodeURIComponent(lines.join('\n'));
|
||||||
|
const url = encodeURIComponent(window.location.href);
|
||||||
|
window.open(`https://www.facebook.com/sharer/sharer.php?u=${url}"e=${quote}`, '_blank', 'width=600,height=400');
|
||||||
|
}}
|
||||||
|
className="p-2 hover:bg-blue-50 rounded-full transition-colors flex items-center gap-1"
|
||||||
|
title="مشاركة على فيسبوك"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5 text-blue-600" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="mb-8"
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
|
<div className="relative h-[500px] rounded-2xl overflow-hidden group bg-gray-100">
|
||||||
|
<Image
|
||||||
|
src={property.images[currentImage] || '/property-placeholder.jpg'}
|
||||||
|
alt={property.title}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{property.images.length > 1 && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentImage(prev => Math.max(0, prev - 1))}
|
||||||
|
className="absolute left-4 top-1/2 transform -translate-y-1/2 w-10 h-10 bg-white/90 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity shadow-lg hover:bg-white"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentImage(prev => Math.min(property.images.length - 1, prev + 1))}
|
||||||
|
className="absolute right-4 top-1/2 transform -translate-y-1/2 w-10 h-10 bg-white/90 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity shadow-lg hover:bg-white"
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex gap-2">
|
||||||
|
{property.images.map((_, idx) => (
|
||||||
|
<button
|
||||||
|
key={idx}
|
||||||
|
onClick={() => setCurrentImage(idx)}
|
||||||
|
className={`w-2 h-2 rounded-full transition-all ${idx === currentImage ? 'bg-gray-800 w-4' : 'bg-white/70 hover:bg-white'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute bottom-4 right-4 bg-black/50 text-white px-3 py-1 rounded-full text-sm backdrop-blur-sm">
|
||||||
|
<Camera className="w-4 h-4 inline ml-1" />
|
||||||
|
{currentImage + 1} / {property.images.length}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
{property.images.slice(1, 5).map((img, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
onClick={() => setCurrentImage(idx + 1)}
|
||||||
|
className="relative h-[240px] rounded-2xl overflow-hidden cursor-pointer hover:opacity-90 transition-opacity bg-gray-100"
|
||||||
|
>
|
||||||
|
<Image src={img} alt={`${property.title} ${idx + 2}`} fill className="object-cover" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||||
|
<div className="lg:col-span-2 space-y-6">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.1 }}
|
||||||
|
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100"
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start mb-4">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">{property.title}</h1>
|
||||||
|
<div className="flex items-center gap-2 text-gray-500">
|
||||||
|
<MapPin className="w-5 h-5" />
|
||||||
|
<span>{property.location.address}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-left">
|
||||||
|
<div className="text-3xl font-bold text-gray-900">{formatCurrency(property.price)}</div>
|
||||||
|
<div className="text-sm text-gray-500">/{property.priceUnit === 'daily' ? 'يوم' : 'شهر'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Star className="w-5 h-5 fill-gray-800 text-gray-800" />
|
||||||
|
<span className="font-bold text-gray-900">{property.rating}</span>
|
||||||
|
{property.reviews > 0 && <span className="text-gray-500">({property.reviews} تقييم)</span>}
|
||||||
|
</div>
|
||||||
|
<div className="w-px h-4 bg-gray-200" />
|
||||||
|
<span className={`font-medium ${property.status === 'available' ? 'text-gray-800' : 'text-gray-500'}`}>
|
||||||
|
{property.status === 'available' ? 'متاح للإيجار' : 'محجوز حالياً'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100"
|
||||||
|
>
|
||||||
|
<h2 className="text-xl font-bold mb-4 text-gray-900">المواصفات الرئيسية</h2>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<div className="text-center p-4 bg-gray-50 rounded-xl">
|
||||||
|
<Bed className="w-6 h-6 text-gray-700 mx-auto mb-2" />
|
||||||
|
<div className="font-bold text-gray-900">{property.bedrooms}</div>
|
||||||
|
<div className="text-sm text-gray-500">غرف نوم</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-gray-50 rounded-xl">
|
||||||
|
<Bath className="w-6 h-6 text-gray-700 mx-auto mb-2" />
|
||||||
|
<div className="font-bold text-gray-900">{property.bathrooms}</div>
|
||||||
|
<div className="text-sm text-gray-500">حمامات</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-gray-50 rounded-xl">
|
||||||
|
<Square className="w-6 h-6 text-gray-700 mx-auto mb-2" />
|
||||||
|
<div className="font-bold text-gray-900">{property.area}</div>
|
||||||
|
<div className="text-sm text-gray-500">م²</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-gray-50 rounded-xl">
|
||||||
|
<Home className="w-6 h-6 text-gray-700 mx-auto mb-2" />
|
||||||
|
<div className="font-bold text-gray-900">
|
||||||
|
{property.type === 'villa' ? 'فيلا' :
|
||||||
|
property.type === 'apartment' ? 'شقة' : 'بيت'}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500">نوع العقار</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.3 }}
|
||||||
|
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100"
|
||||||
|
>
|
||||||
|
<h2 className="text-xl font-bold mb-4 text-gray-900">وصف العقار</h2>
|
||||||
|
<p className="text-gray-600 whitespace-pre-line leading-relaxed">{property.description || 'لا يوجد وصف متاح.'}</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.4 }}
|
||||||
|
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100"
|
||||||
|
>
|
||||||
|
<h2 className="text-xl font-bold mb-4 text-gray-900">المميزات والخدمات</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{property.features.map((feature, idx) => (
|
||||||
|
<div key={idx} className="flex items-start gap-3 p-3 bg-gray-50 rounded-xl">
|
||||||
|
<div className={`w-6 h-6 rounded-full flex items-center justify-center flex-shrink-0 ${feature.available ? 'bg-gray-800 text-white' : 'bg-gray-200 text-gray-500'
|
||||||
|
}`}>
|
||||||
|
{feature.available ? (
|
||||||
|
<Check className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className={`font-medium ${feature.available ? 'text-gray-900' : 'text-gray-400'}`}>
|
||||||
|
{feature.name}
|
||||||
|
</span>
|
||||||
|
{feature.description && (
|
||||||
|
<p className={`text-sm mt-1 ${feature.available ? 'text-gray-500' : 'text-gray-400'}`}>
|
||||||
|
{feature.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{property.reviewList && property.reviewList.length > 0 && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.6 }}
|
||||||
|
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100"
|
||||||
|
>
|
||||||
|
<h2 className="text-xl font-bold mb-4 text-gray-900">تقييمات المستأجرين</h2>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{property.reviewList.map((review, idx) => (
|
||||||
|
<div key={idx} className="border-b border-gray-100 last:border-0 pb-4 last:pb-0">
|
||||||
|
<div className="flex justify-between items-start mb-2">
|
||||||
|
<div>
|
||||||
|
<span className="font-bold text-gray-900">{review.user}</span>
|
||||||
|
<div className="flex items-center gap-1 mt-1">
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<Star key={i} className={`w-4 h-4 ${i < review.rating ? 'fill-gray-800 text-gray-800' : 'text-gray-300'}`} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-gray-500">{review.date}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-600">{review.comment}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{property.rules && property.rules.length > 0 && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.7 }}
|
||||||
|
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100"
|
||||||
|
>
|
||||||
|
<h2 className="text-xl font-bold mb-4 text-gray-900">قوانين المنزل</h2>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{property.rules.map((rule, idx) => (
|
||||||
|
<li key={idx} className="flex items-center gap-2 text-gray-600">
|
||||||
|
<div className="w-1.5 h-1.5 bg-gray-400 rounded-full" />
|
||||||
|
{rule}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="sticky top-28">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: 20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100 mb-6"
|
||||||
|
>
|
||||||
|
<h2 className="text-xl font-bold mb-4 text-gray-900">احجز هذا العقار</h2>
|
||||||
|
|
||||||
|
{/* Selected dates display */}
|
||||||
|
<div className="mb-4 flex gap-2 text-sm">
|
||||||
|
<div className="flex-1 bg-gray-50 p-3 rounded-xl">
|
||||||
|
<span className="text-gray-500 block mb-1">من</span>
|
||||||
|
<span className="font-medium text-gray-900">{bookingDates.start || '—'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 bg-gray-50 p-3 rounded-xl">
|
||||||
|
<span className="text-gray-500 block mb-1">إلى</span>
|
||||||
|
<span className="font-medium text-gray-900">{bookingDates.end || '—'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{bookingDates.start && bookingDates.end && (() => {
|
||||||
|
const days = Math.ceil((new Date(bookingDates.end) - new Date(bookingDates.start)) / (1000 * 60 * 60 * 24));
|
||||||
|
return days > 0 ? (
|
||||||
|
<div className="mb-4 text-center text-sm text-amber-600 font-medium bg-amber-50 p-2 rounded-xl">
|
||||||
|
{days} يوم{days > 1 ? 'اً' : 'اً'} {selectingEnd ? '— اضغط على تاريخ النهاية' : '✓'}
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
})()}
|
||||||
|
|
||||||
|
{/* Calendar */}
|
||||||
|
<div className="mb-4 relative">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<button onClick={() => setCalendarMonth(new Date(calendarMonth.getFullYear(), calendarMonth.getMonth() - 1))} className="p-1 hover:bg-gray-100 rounded-lg">
|
||||||
|
<ChevronRight className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
<span className="font-bold text-gray-900">{monthNames[calendarMonth.getMonth()]} {calendarMonth.getFullYear()}</span>
|
||||||
|
<button onClick={() => setCalendarMonth(new Date(calendarMonth.getFullYear(), calendarMonth.getMonth() + 1))} className="p-1 hover:bg-gray-100 rounded-lg">
|
||||||
|
<ChevronLeft className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-7 gap-1 mb-1">
|
||||||
|
{dayNames.map((d) => (
|
||||||
|
<div key={d} className="text-center text-xs text-gray-500 font-medium py-1">{d}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-7 gap-1">
|
||||||
|
{(() => {
|
||||||
|
const year = calendarMonth.getFullYear();
|
||||||
|
const month = calendarMonth.getMonth();
|
||||||
|
const daysInMonth = getDaysInMonth(year, month);
|
||||||
|
const firstDay = getFirstDayOfMonth(year, month);
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
const cells = [];
|
||||||
|
|
||||||
|
// Empty cells before first day
|
||||||
|
for (let i = 0; i < firstDay; i++) {
|
||||||
|
cells.push(<div key={`empty-${i}`} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let day = 1; day <= daysInMonth; day++) {
|
||||||
|
const dateStr = formatDateStr(year, month, day);
|
||||||
|
const available = isDateAvailable(dateStr);
|
||||||
|
const isStart = bookingDates.start === dateStr;
|
||||||
|
const isEnd = bookingDates.end === dateStr;
|
||||||
|
const inRange = isInRange(dateStr);
|
||||||
|
const isPast = dateStr < today;
|
||||||
|
|
||||||
|
cells.push(
|
||||||
|
<button
|
||||||
|
key={dateStr}
|
||||||
|
onClick={() => !isPast && handleCalendarClick(dateStr)}
|
||||||
|
disabled={isPast || !available}
|
||||||
|
className={`w-full aspect-square rounded-lg text-sm font-medium transition-all ${
|
||||||
|
isStart || isEnd
|
||||||
|
? 'bg-amber-500 text-white'
|
||||||
|
: inRange
|
||||||
|
? 'bg-amber-100 text-amber-800'
|
||||||
|
: available && !isPast
|
||||||
|
? 'bg-green-50 text-green-700 hover:bg-green-100 cursor-pointer'
|
||||||
|
: 'text-gray-300 cursor-not-allowed'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{day}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return cells;
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 mt-3 text-xs text-gray-500">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="w-3 h-3 rounded bg-green-50 border border-green-200" />
|
||||||
|
<span>متاح</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="w-3 h-3 rounded bg-amber-500" />
|
||||||
|
<span>محدد</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="w-3 h-3 rounded bg-gray-100" />
|
||||||
|
<span>غير متاح</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-50 p-4 rounded-xl mb-6">
|
||||||
|
{(() => {
|
||||||
|
const days = bookingDates.start && bookingDates.end
|
||||||
|
? Math.ceil((new Date(bookingDates.end) - new Date(bookingDates.start)) / (1000 * 60 * 60 * 24))
|
||||||
|
: 0;
|
||||||
|
const effectiveDays = days > 0 ? days : 1;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex justify-between mb-2">
|
||||||
|
<span className="text-gray-600">السعر لـ {effectiveDays} يوم{effectiveDays > 1 ? 'اً' : 'اً'}</span>
|
||||||
|
<span className="font-bold text-gray-900">{formatCurrency(property.price * effectiveDays)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between mb-2">
|
||||||
|
<span className="text-gray-600">سلفة ضمان</span>
|
||||||
|
<span className="font-bold text-gray-900">{formatCurrency(property._raw?.deposit || 0)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between pt-2 border-t border-gray-200 font-bold">
|
||||||
|
<span className="text-gray-900">الإجمالي</span>
|
||||||
|
<span className="text-gray-900">{formatCurrency(property.price * effectiveDays + (property._raw?.deposit || 0))}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{bookingError && (
|
||||||
|
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-xl text-red-700 text-sm">
|
||||||
|
{bookingError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{bookingSuccess && (
|
||||||
|
<div className="mb-4 p-3 bg-green-50 border border-green-200 rounded-xl text-green-700 text-sm">
|
||||||
|
تم إرسال طلب الحجز بنجاح. سيتم التواصل معك قريباً.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleBooking}
|
||||||
|
disabled={bookingSuccess}
|
||||||
|
className="w-full bg-gray-800 text-white py-4 rounded-xl font-bold text-lg hover:bg-gray-900 transition-colors mb-4 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{bookingSuccess ? 'تم الإرسال ✓' : 'تأكيد الحجز'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||||
|
<Shield className="w-4 h-4 text-gray-600" />
|
||||||
|
<span>الدفع آمن ومضمون. سلفة الضمان قابلة للاسترداد.</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: 20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100"
|
||||||
|
>
|
||||||
|
<h3 className="font-bold mb-4 text-gray-900">معلومات المالك</h3>
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="w-12 h-12 bg-gray-100 rounded-full flex items-center justify-center">
|
||||||
|
<span className="text-xl font-bold text-gray-700">
|
||||||
|
{property.owner.name.charAt(0)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-bold text-gray-900">{property.owner.name}</div>
|
||||||
|
<div className="flex items-center gap-1 text-sm text-gray-500">
|
||||||
|
<Star className="w-3 h-3 fill-gray-600 text-gray-600" />
|
||||||
|
<span>{property.owner.rating}</span>
|
||||||
|
<span>· {property.owner.properties} عقارات</span>
|
||||||
|
</div>
|
||||||
|
{property.owner.responseRate && (
|
||||||
|
<div className="flex items-center gap-1 text-xs text-gray-500 mt-1">
|
||||||
|
<Clock className="w-3 h-3" />
|
||||||
|
<span>استجابة: {property.owner.responseRate}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showContact ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center gap-2 p-3 bg-gray-50 rounded-xl">
|
||||||
|
<Phone className="w-4 h-4 text-gray-600" />
|
||||||
|
<span className="font-medium text-gray-900">{property.owner.phone}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 p-3 bg-gray-50 rounded-xl">
|
||||||
|
<Mail className="w-4 h-4 text-gray-600" />
|
||||||
|
<span className="font-medium text-gray-900">{property.owner.email}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowContact(true)}
|
||||||
|
className="w-full bg-gray-800 text-white py-3 rounded-xl font-medium hover:bg-gray-900 transition-colors flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<Phone className="w-5 h-5" />
|
||||||
|
عرض معلومات الاتصال
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button className="w-full mt-3 bg-gray-100 text-gray-700 py-3 rounded-xl font-medium hover:bg-gray-200 transition-colors flex items-center justify-center gap-2">
|
||||||
|
<MessageCircle className="w-5 h-5" />
|
||||||
|
مراسلة المالك
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Login/Register Dialog */}
|
||||||
|
{showLoginDialog && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm" onClick={() => setShowLoginDialog(false)}>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="bg-white rounded-2xl p-8 max-w-md w-full mx-4 shadow-2xl text-center"
|
||||||
|
>
|
||||||
|
<div className="w-16 h-16 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<LogIn className="w-8 h-8 text-amber-600" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 mb-2">سجّل الدخول للمتابعة</h3>
|
||||||
|
<p className="text-gray-500 mb-6">يجب عليك إنشاء حساب أو تسجيل الدخول لحجز هذا العقار</p>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Link
|
||||||
|
href="/auth/choose-role"
|
||||||
|
className="block w-full bg-amber-500 text-white py-3 rounded-xl font-bold hover:bg-amber-600 transition-colors"
|
||||||
|
>
|
||||||
|
إنشاء حساب جديد
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/login"
|
||||||
|
className="block w-full bg-gray-100 text-gray-700 py-3 rounded-xl font-bold hover:bg-gray-200 transition-colors"
|
||||||
|
>
|
||||||
|
تسجيل الدخول
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowLoginDialog(false)}
|
||||||
|
className="block w-full text-gray-400 py-2 text-sm hover:text-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
إلغاء
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,908 +1,90 @@
|
|||||||
'use client';
|
import PropertyDetail from './PropertyDetail';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
// Server-side API fetch for metadata (runs at request time on server)
|
||||||
import { motion } from 'framer-motion';
|
async function fetchPropertyForMeta(id) {
|
||||||
import toast, { Toaster } from 'react-hot-toast';
|
try {
|
||||||
import Image from 'next/image';
|
const res = await fetch(`http://45.93.137.91/api/RentProperties/GetRentProperties`, {
|
||||||
import Link from 'next/link';
|
next: { revalidate: 60 },
|
||||||
import { useParams } from 'next/navigation';
|
});
|
||||||
import {
|
if (!res.ok) return null;
|
||||||
MapPin,
|
const text = await res.text();
|
||||||
Bed,
|
const json = JSON.parse(text);
|
||||||
Bath,
|
const items = Array.isArray(json?.data) ? json.data : Array.isArray(json) ? json : [];
|
||||||
Square,
|
return items.find(p => p.id == id) || items[0] || null;
|
||||||
DollarSign,
|
} catch {
|
||||||
Heart,
|
return null;
|
||||||
Share2,
|
}
|
||||||
Phone,
|
}
|
||||||
Mail,
|
|
||||||
MessageCircle,
|
|
||||||
Calendar,
|
|
||||||
Shield,
|
|
||||||
Star,
|
|
||||||
ChevronLeft,
|
|
||||||
ChevronRight,
|
|
||||||
Check,
|
|
||||||
X,
|
|
||||||
Wifi,
|
|
||||||
Car,
|
|
||||||
Coffee,
|
|
||||||
Wind,
|
|
||||||
Thermometer,
|
|
||||||
Lock,
|
|
||||||
Camera,
|
|
||||||
Home,
|
|
||||||
Building2,
|
|
||||||
Users,
|
|
||||||
Ruler,
|
|
||||||
CalendarDays,
|
|
||||||
Clock,
|
|
||||||
Award,
|
|
||||||
FileText,
|
|
||||||
Printer,
|
|
||||||
Download,
|
|
||||||
ArrowLeft,
|
|
||||||
LogIn
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { getRentProperty, getSaleProperty, bookReservation, checkAvailability, getAvailableDateRanges } from '../../utils/api';
|
|
||||||
import AuthService from '../../services/AuthService';
|
|
||||||
import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from '../../enums';
|
|
||||||
|
|
||||||
// Map API response to the UI format
|
function mapProperty(item) {
|
||||||
function mapApiDetail(item) {
|
const info = item.propertyInformation || item.PropertyInformation || {};
|
||||||
if (!item) return null;
|
let details = {};
|
||||||
|
try { details = JSON.parse(info.detailsJSON || info.DetailsJSON || '{}'); } catch {}
|
||||||
|
|
||||||
const info = item.propertyInformation || {};
|
const price = item.monthlyRent || item.MonthlyRent || item.dailyRent || item.DailyRent || 0;
|
||||||
|
const priceUnit = item.monthlyRent || item.MonthlyRent ? 'monthly' : 'daily';
|
||||||
const dailyPrice = item.dailyRent ?? item.monthlyRent ?? item.price ?? 0;
|
const buildingType = info.buildingType ?? info.BuildingType ?? 0;
|
||||||
const monthlyPrice = item.monthlyRent ?? 0;
|
const type = { 0: 'apartment', 1: 'villa', 2: 'house' }[buildingType] || 'apartment';
|
||||||
|
const typeLabel = { 0: 'شقة', 1: 'فيلا', 2: 'بيت' }[buildingType] || 'عقار';
|
||||||
const propType = BuildingTypeKeys[info.buildingType] ?? BuildingTypeKeys[item.type] ?? 'apartment';
|
const address = info.address || info.Address || '';
|
||||||
const status = PropertyStatusKeys[info.status] ?? PropertyStatusKeys[item.status] ?? 'available';
|
const bedrooms = info.numberOfBedRooms || info.NumberOfBedRooms || 0;
|
||||||
|
const bathrooms = info.numberOfBathRooms || info.NumberOfBathRooms || 0;
|
||||||
const features = [];
|
const area = info.space || info.Space || 0;
|
||||||
if (item.isSmokeAllow) features.push({ name: 'يسمح بالتدخين', available: true, description: '' });
|
const desc = info.description || info.Description || '';
|
||||||
if (item.isVisitorAllow) features.push({ name: 'يسمح بالزوار', available: true, description: '' });
|
const images = info.images || info.Images || [];
|
||||||
if (item.specializedFor) features.push({ name: 'متخصص', available: true, description: '' });
|
const firstImage = Array.isArray(images) && images[0] ? images[0] : '';
|
||||||
if (info.numberOfBedRooms) features.push({ name: 'غرف النوم', available: true, description: `${info.numberOfBedRooms} غرف` });
|
|
||||||
if (info.numberOfBathRooms) features.push({ name: 'الحمامات', available: true, description: `${info.numberOfBathRooms} حمامات` });
|
|
||||||
if (info.space) features.push({ name: 'المساحة', available: true, description: `${info.space} م²` });
|
|
||||||
|
|
||||||
const typeLabels = { 0: 'شقة', 1: 'فيلا', 2: 'بيت' };
|
|
||||||
|
|
||||||
// Extract images from API and build full URLs
|
|
||||||
const apiBase = typeof window !== 'undefined' ? (process.env.NEXT_PUBLIC_API_URL || 'http://45.93.137.91/api') : '';
|
|
||||||
const rawImages = Array.isArray(info.images) ? info.images : [];
|
|
||||||
const images = rawImages.length > 0
|
|
||||||
? rawImages.map(img => img.startsWith('http') ? img : `${apiBase}${img.startsWith('/') ? '' : '/'}${img}`)
|
|
||||||
: ['/property-placeholder.jpg', '/villa1.jpg', '/villa2.jpg'];
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: item.id,
|
title: `${typeLabel} في ${address}`,
|
||||||
title: info.address || `عقار #${item.id}`,
|
description: desc || `${typeLabel} في ${address} · ${bedrooms} غرف نوم · ${bathrooms} حمامات · ${area} م²`,
|
||||||
description: info.description || 'عقار سكني مميز في موقع استراتيجي.',
|
price,
|
||||||
type: propType,
|
priceUnit,
|
||||||
price: dailyPrice,
|
typeLabel,
|
||||||
priceUnit: 'daily',
|
address,
|
||||||
location: {
|
bedrooms,
|
||||||
city: extractCity(info.address) || 'دمشق',
|
bathrooms,
|
||||||
district: info.address || '',
|
area,
|
||||||
address: info.address || '',
|
image: firstImage,
|
||||||
lat: parseFloat(info.cordsX) || 0,
|
|
||||||
lng: parseFloat(info.cordsY) || 0,
|
|
||||||
},
|
|
||||||
bedrooms: info.numberOfBedRooms || 0,
|
|
||||||
bathrooms: info.numberOfBathRooms || 0,
|
|
||||||
area: info.space || 0,
|
|
||||||
features: features.length > 0 ? features : [
|
|
||||||
{ name: 'متاح للإيجار', available: true, description: '' },
|
|
||||||
],
|
|
||||||
images,
|
|
||||||
status,
|
|
||||||
rating: item.rating || 4.5,
|
|
||||||
reviews: 0,
|
|
||||||
reviewList: [],
|
|
||||||
owner: {
|
|
||||||
name: 'المالك',
|
|
||||||
phone: '—',
|
|
||||||
email: '—',
|
|
||||||
rating: 4.8,
|
|
||||||
properties: 1,
|
|
||||||
memberSince: '2024',
|
|
||||||
responseRate: '95%',
|
|
||||||
responseTime: 'خلال ساعات',
|
|
||||||
},
|
|
||||||
nearby: [],
|
|
||||||
specifications: {
|
|
||||||
constructionYear: null,
|
|
||||||
floor: '-',
|
|
||||||
parking: 0,
|
|
||||||
gardenArea: 0,
|
|
||||||
poolArea: 0,
|
|
||||||
furnished: false,
|
|
||||||
airConditioning: '-',
|
|
||||||
heating: '-',
|
|
||||||
electricity: '220V',
|
|
||||||
water: 'شبكة عامة',
|
|
||||||
},
|
|
||||||
rules: [],
|
|
||||||
_raw: item,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractCity is now imported from @/app/enums
|
export async function generateMetadata({ params }) {
|
||||||
|
const { id } = await params;
|
||||||
|
const raw = await fetchPropertyForMeta(id);
|
||||||
|
|
||||||
// API-only — no fallback data
|
if (!raw) {
|
||||||
|
return {
|
||||||
export default function PropertyDetailsPage() {
|
title: 'SweetHome - عقار',
|
||||||
const params = useParams();
|
description: 'اكتشف أفضل العقارات للإيجار',
|
||||||
const [currentImage, setCurrentImage] = useState(0);
|
|
||||||
const [showContact, setShowContact] = useState(false);
|
|
||||||
const [bookingDates, setBookingDates] = useState({ start: '', end: '' });
|
|
||||||
const [selectedDuration, setSelectedDuration] = useState(1);
|
|
||||||
const [property, setProperty] = useState(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [bookingError, setBookingError] = useState(null);
|
|
||||||
const [bookingSuccess, setBookingSuccess] = useState(false);
|
|
||||||
const [availableRanges, setAvailableRanges] = useState([]);
|
|
||||||
const [calendarMonth, setCalendarMonth] = useState(new Date());
|
|
||||||
const [selectingEnd, setSelectingEnd] = useState(false);
|
|
||||||
const [showLoginDialog, setShowLoginDialog] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const id = params.id;
|
|
||||||
setLoading(true);
|
|
||||||
setBookingError(null);
|
|
||||||
setBookingSuccess(false);
|
|
||||||
|
|
||||||
async function fetchProperty() {
|
|
||||||
try {
|
|
||||||
// Try RentProperties first, then SaleProperties
|
|
||||||
let data = null;
|
|
||||||
try {
|
|
||||||
data = await getRentProperty(id);
|
|
||||||
} catch {
|
|
||||||
try {
|
|
||||||
data = await getSaleProperty(id);
|
|
||||||
} catch {
|
|
||||||
// neither worked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
const mapped = mapApiDetail(data);
|
|
||||||
if (mapped) {
|
|
||||||
setProperty(mapped);
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setProperty(null);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[Property] Failed to fetch property:', err);
|
|
||||||
setProperty(null);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchProperty();
|
|
||||||
}, [params.id]);
|
|
||||||
|
|
||||||
// Fetch available date ranges
|
|
||||||
useEffect(() => {
|
|
||||||
if (!property) return;
|
|
||||||
const propId = property._raw?.id || params.id;
|
|
||||||
console.log('[Property] Fetching available dates for:', propId);
|
|
||||||
getAvailableDateRanges(propId)
|
|
||||||
.then((data) => {
|
|
||||||
const ranges = Array.isArray(data) ? data : [];
|
|
||||||
console.log('[Property] Available date ranges:', ranges);
|
|
||||||
setAvailableRanges(ranges);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.warn('[Property] Failed to fetch available dates:', err);
|
|
||||||
});
|
|
||||||
}, [property, params.id]);
|
|
||||||
|
|
||||||
// Set Open Graph meta tags dynamically for Facebook/Twitter sharing
|
|
||||||
useEffect(() => {
|
|
||||||
if (!property) return;
|
|
||||||
|
|
||||||
const typeLabel = property.type === 'villa' ? 'فيلا' : property.type === 'apartment' ? 'شقة' : 'بيت';
|
|
||||||
const priceLabel = `${formatCurrency(property.price)} / ${property.priceUnit === 'daily' ? 'يوم' : 'شهر'}`;
|
|
||||||
const desc = `${typeLabel} في ${property.location?.address || ''} · ${property.bedrooms} غرف نوم · ${property.bathrooms} حمامات · ${property.area} م²`;
|
|
||||||
const imageUrl = property.images?.[0]
|
|
||||||
? (property.images[0].startsWith('http') ? property.images[0] : `http://45.93.137.91${property.images[0]}`)
|
|
||||||
: '';
|
|
||||||
|
|
||||||
const setMeta = (prop, content) => {
|
|
||||||
let tag = document.querySelector(`meta[property="${prop}"]`);
|
|
||||||
if (!tag) {
|
|
||||||
tag = document.createElement('meta');
|
|
||||||
tag.setAttribute('property', prop);
|
|
||||||
document.head.appendChild(tag);
|
|
||||||
}
|
|
||||||
tag.setAttribute('content', content);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const setMetaName = (name, content) => {
|
|
||||||
let tag = document.querySelector(`meta[name="${name}"]`);
|
|
||||||
if (!tag) {
|
|
||||||
tag = document.createElement('meta');
|
|
||||||
tag.setAttribute('name', name);
|
|
||||||
document.head.appendChild(tag);
|
|
||||||
}
|
|
||||||
tag.setAttribute('content', content);
|
|
||||||
};
|
|
||||||
|
|
||||||
setMeta('og:title', `${property.title} - ${priceLabel}`);
|
|
||||||
setMeta('og:description', desc);
|
|
||||||
if (imageUrl) setMeta('og:image', imageUrl);
|
|
||||||
setMeta('og:url', window.location.href);
|
|
||||||
setMeta('og:type', 'website');
|
|
||||||
setMeta('og:site_name', 'SweetHome');
|
|
||||||
|
|
||||||
// Twitter cards
|
|
||||||
setMetaName('twitter:card', 'summary_large_image');
|
|
||||||
setMetaName('twitter:title', `${property.title} - ${priceLabel}`);
|
|
||||||
setMetaName('twitter:description', desc);
|
|
||||||
if (imageUrl) setMetaName('twitter:image', imageUrl);
|
|
||||||
}, [property]);
|
|
||||||
|
|
||||||
const formatCurrency = (amount) => {
|
|
||||||
return amount?.toLocaleString() + ' ل.س';
|
|
||||||
};
|
|
||||||
|
|
||||||
const calculateTotalPrice = () => {
|
|
||||||
if (!property) return 0;
|
|
||||||
const days = bookingDates.start && bookingDates.end
|
|
||||||
? Math.ceil((new Date(bookingDates.end) - new Date(bookingDates.start)) / (1000 * 60 * 60 * 24))
|
|
||||||
: selectedDuration;
|
|
||||||
return property.price * (days > 0 ? days : 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Calendar helpers
|
|
||||||
const isDateAvailable = (dateStr) => {
|
|
||||||
const d = new Date(dateStr + 'T00:00:00');
|
|
||||||
return availableRanges.some((range) => {
|
|
||||||
const start = new Date(range.startDate);
|
|
||||||
const end = new Date(range.endDate);
|
|
||||||
return d >= start && d <= end;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const isInRange = (dateStr) => {
|
|
||||||
if (!bookingDates.start) return false;
|
|
||||||
const d = new Date(dateStr + 'T00:00:00');
|
|
||||||
const start = new Date(bookingDates.start + 'T00:00:00');
|
|
||||||
const end = bookingDates.end ? new Date(bookingDates.end + 'T00:00:00') : start;
|
|
||||||
return d >= start && d <= end;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isRangeFullyAvailable = (startStr, endStr) => {
|
|
||||||
const start = new Date(startStr + 'T00:00:00');
|
|
||||||
const end = new Date(endStr + 'T00:00:00');
|
|
||||||
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
|
|
||||||
if (!isDateAvailable(d.toISOString().split('T')[0])) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCalendarClick = (dateStr) => {
|
|
||||||
if (!isDateAvailable(dateStr)) return;
|
|
||||||
|
|
||||||
if (!bookingDates.start || selectingEnd) {
|
|
||||||
if (!bookingDates.start) {
|
|
||||||
setBookingDates({ start: dateStr, end: '' });
|
|
||||||
setSelectingEnd(true);
|
|
||||||
} else {
|
|
||||||
const start = bookingDates.start;
|
|
||||||
const end = dateStr;
|
|
||||||
const [s, e] = end > start ? [start, end] : [end, start];
|
|
||||||
if (isRangeFullyAvailable(s, e)) {
|
|
||||||
setBookingDates({ start: s, end: e });
|
|
||||||
setSelectingEnd(false);
|
|
||||||
} else {
|
|
||||||
toast.error('بعض التواريخ في هذه الفترة غير متاحة');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setBookingDates({ start: dateStr, end: '' });
|
|
||||||
setSelectingEnd(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDaysInMonth = (year, month) => new Date(year, month + 1, 0).getDate();
|
|
||||||
const getFirstDayOfMonth = (year, month) => new Date(year, month, 1).getDay();
|
|
||||||
|
|
||||||
const formatDateStr = (year, month, day) => {
|
|
||||||
return `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const monthNames = ['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'];
|
|
||||||
const dayNames = ['أح', 'إث', 'ثل', 'أر', 'خم', 'جم', 'سب'];
|
|
||||||
|
|
||||||
const handleBooking = async () => {
|
|
||||||
if (!AuthService.isAuthenticated()) {
|
|
||||||
setShowLoginDialog(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setBookingError(null);
|
|
||||||
setBookingSuccess(false);
|
|
||||||
|
|
||||||
if (!bookingDates.start || !bookingDates.end) {
|
|
||||||
setBookingError('يرجى اختيار تاريخ البداية والنهاية');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const propId = property?._raw?.id || parseInt(params.id);
|
|
||||||
const startDate = new Date(bookingDates.start).toISOString();
|
|
||||||
const endDate = new Date(bookingDates.end).toISOString();
|
|
||||||
|
|
||||||
console.log('[Booking] Reserving:', { propertyId: propId, startDate, endDate });
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await bookReservation(propId, startDate, endDate);
|
|
||||||
console.log('[Booking] Success:', res);
|
|
||||||
setBookingSuccess(true);
|
|
||||||
toast.success('تم إرسال طلب الحجز بنجاح!');
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[Booking] Failed:', err);
|
|
||||||
setBookingError(err.message || 'فشل في إرسال طلب الحجز');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="w-16 h-16 border-4 border-gray-200 border-t-gray-800 rounded-full animate-spin mx-auto mb-4"></div>
|
|
||||||
<p className="text-gray-600">جاري تحميل تفاصيل العقار...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!property) {
|
const p = mapProperty(raw);
|
||||||
return (
|
const priceStr = `${p.price.toLocaleString()} ل.س / ${p.priceUnit === 'daily' ? 'يوم' : 'شهر'}`;
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
const imageUrl = p.image
|
||||||
<div className="text-center">
|
? (p.image.startsWith('http') ? p.image : `http://45.93.137.91${p.image}`)
|
||||||
<Home className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
: '';
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">العقار غير موجود</h2>
|
|
||||||
<p className="text-gray-600 mb-4">لم نتمكن من العثور على العقار المطلوب</p>
|
|
||||||
<Link href="/properties" className="bg-gray-800 text-white px-6 py-3 rounded-xl font-medium hover:bg-gray-900 transition-colors">
|
|
||||||
العودة إلى العقارات
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return {
|
||||||
<div className="min-h-screen bg-gray-50">
|
title: `${p.title} - ${priceStr}`,
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
description: p.description,
|
||||||
<div className="bg-white border-b sticky top-16 z-40 shadow-sm">
|
openGraph: {
|
||||||
<div className="container mx-auto px-4">
|
title: `${p.title} - ${priceStr}`,
|
||||||
<div className="flex items-center justify-between h-16">
|
description: p.description,
|
||||||
<Link href="/properties" className="flex items-center gap-2 text-gray-600 hover:text-gray-900 transition-colors">
|
images: imageUrl ? [imageUrl] : [],
|
||||||
<ArrowLeft className="w-5 h-5" />
|
url: `https://sweethome.example/property/${id}`,
|
||||||
<span>العودة إلى العقارات</span>
|
type: 'website',
|
||||||
</Link>
|
siteName: 'SweetHome',
|
||||||
<div className="flex gap-2">
|
},
|
||||||
<button className="p-2 hover:bg-gray-100 rounded-full transition-colors">
|
twitter: {
|
||||||
<Heart className="w-5 h-5 text-gray-600" />
|
card: 'summary_large_image',
|
||||||
</button>
|
title: `${p.title} - ${priceStr}`,
|
||||||
<button
|
description: p.description,
|
||||||
onClick={() => {
|
images: imageUrl ? [imageUrl] : [],
|
||||||
const typeLabel = property.type === 'villa' ? 'فيلا' : property.type === 'apartment' ? 'شقة' : 'بيت';
|
},
|
||||||
const priceLabel = `${formatCurrency(property.price)} / ${property.priceUnit === 'daily' ? 'يوم' : 'شهر'}`;
|
};
|
||||||
const lines = [
|
}
|
||||||
`🏠 ${typeLabel} مفروشة في ${property.location?.address || ''}`,
|
|
||||||
`💰 ${priceLabel}`,
|
export default function PropertyPage({ params }) {
|
||||||
`🛏️ ${property.bedrooms} غرف نوم | 🚿 ${property.bathrooms} حمامات | 📐 ${property.area} م²`,
|
return <PropertyDetail params={params} />;
|
||||||
property.description ? `📝 ${property.description.substring(0, 80)}...` : '',
|
|
||||||
'',
|
|
||||||
'🔥 احجز الآن على SweetHome',
|
|
||||||
].filter(Boolean);
|
|
||||||
const quote = encodeURIComponent(lines.join('\n'));
|
|
||||||
const url = encodeURIComponent(window.location.href);
|
|
||||||
window.open(`https://www.facebook.com/sharer/sharer.php?u=${url}"e=${quote}`, '_blank', 'width=600,height=400');
|
|
||||||
}}
|
|
||||||
className="p-2 hover:bg-blue-50 rounded-full transition-colors flex items-center gap-1"
|
|
||||||
title="مشاركة على فيسبوك"
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5 text-blue-600" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="container mx-auto px-4 py-8">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="mb-8"
|
|
||||||
>
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
||||||
<div className="relative h-[500px] rounded-2xl overflow-hidden group bg-gray-100">
|
|
||||||
<Image
|
|
||||||
src={property.images[currentImage] || '/property-placeholder.jpg'}
|
|
||||||
alt={property.title}
|
|
||||||
fill
|
|
||||||
className="object-cover"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{property.images.length > 1 && (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
onClick={() => setCurrentImage(prev => Math.max(0, prev - 1))}
|
|
||||||
className="absolute left-4 top-1/2 transform -translate-y-1/2 w-10 h-10 bg-white/90 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity shadow-lg hover:bg-white"
|
|
||||||
>
|
|
||||||
<ChevronLeft className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setCurrentImage(prev => Math.min(property.images.length - 1, prev + 1))}
|
|
||||||
className="absolute right-4 top-1/2 transform -translate-y-1/2 w-10 h-10 bg-white/90 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity shadow-lg hover:bg-white"
|
|
||||||
>
|
|
||||||
<ChevronRight className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex gap-2">
|
|
||||||
{property.images.map((_, idx) => (
|
|
||||||
<button
|
|
||||||
key={idx}
|
|
||||||
onClick={() => setCurrentImage(idx)}
|
|
||||||
className={`w-2 h-2 rounded-full transition-all ${idx === currentImage ? 'bg-gray-800 w-4' : 'bg-white/70 hover:bg-white'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="absolute bottom-4 right-4 bg-black/50 text-white px-3 py-1 rounded-full text-sm backdrop-blur-sm">
|
|
||||||
<Camera className="w-4 h-4 inline ml-1" />
|
|
||||||
{currentImage + 1} / {property.images.length}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
{property.images.slice(1, 5).map((img, idx) => (
|
|
||||||
<div
|
|
||||||
key={idx}
|
|
||||||
onClick={() => setCurrentImage(idx + 1)}
|
|
||||||
className="relative h-[240px] rounded-2xl overflow-hidden cursor-pointer hover:opacity-90 transition-opacity bg-gray-100"
|
|
||||||
>
|
|
||||||
<Image src={img} alt={`${property.title} ${idx + 2}`} fill className="object-cover" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
||||||
<div className="lg:col-span-2 space-y-6">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.1 }}
|
|
||||||
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100"
|
|
||||||
>
|
|
||||||
<div className="flex justify-between items-start mb-4">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">{property.title}</h1>
|
|
||||||
<div className="flex items-center gap-2 text-gray-500">
|
|
||||||
<MapPin className="w-5 h-5" />
|
|
||||||
<span>{property.location.address}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-left">
|
|
||||||
<div className="text-3xl font-bold text-gray-900">{formatCurrency(property.price)}</div>
|
|
||||||
<div className="text-sm text-gray-500">/{property.priceUnit === 'daily' ? 'يوم' : 'شهر'}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Star className="w-5 h-5 fill-gray-800 text-gray-800" />
|
|
||||||
<span className="font-bold text-gray-900">{property.rating}</span>
|
|
||||||
{property.reviews > 0 && <span className="text-gray-500">({property.reviews} تقييم)</span>}
|
|
||||||
</div>
|
|
||||||
<div className="w-px h-4 bg-gray-200" />
|
|
||||||
<span className={`font-medium ${property.status === 'available' ? 'text-gray-800' : 'text-gray-500'}`}>
|
|
||||||
{property.status === 'available' ? 'متاح للإيجار' : 'محجوز حالياً'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.2 }}
|
|
||||||
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100"
|
|
||||||
>
|
|
||||||
<h2 className="text-xl font-bold mb-4 text-gray-900">المواصفات الرئيسية</h2>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
||||||
<div className="text-center p-4 bg-gray-50 rounded-xl">
|
|
||||||
<Bed className="w-6 h-6 text-gray-700 mx-auto mb-2" />
|
|
||||||
<div className="font-bold text-gray-900">{property.bedrooms}</div>
|
|
||||||
<div className="text-sm text-gray-500">غرف نوم</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-center p-4 bg-gray-50 rounded-xl">
|
|
||||||
<Bath className="w-6 h-6 text-gray-700 mx-auto mb-2" />
|
|
||||||
<div className="font-bold text-gray-900">{property.bathrooms}</div>
|
|
||||||
<div className="text-sm text-gray-500">حمامات</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-center p-4 bg-gray-50 rounded-xl">
|
|
||||||
<Square className="w-6 h-6 text-gray-700 mx-auto mb-2" />
|
|
||||||
<div className="font-bold text-gray-900">{property.area}</div>
|
|
||||||
<div className="text-sm text-gray-500">م²</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-center p-4 bg-gray-50 rounded-xl">
|
|
||||||
<Home className="w-6 h-6 text-gray-700 mx-auto mb-2" />
|
|
||||||
<div className="font-bold text-gray-900">
|
|
||||||
{property.type === 'villa' ? 'فيلا' :
|
|
||||||
property.type === 'apartment' ? 'شقة' : 'بيت'}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-500">نوع العقار</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.3 }}
|
|
||||||
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100"
|
|
||||||
>
|
|
||||||
<h2 className="text-xl font-bold mb-4 text-gray-900">وصف العقار</h2>
|
|
||||||
<p className="text-gray-600 whitespace-pre-line leading-relaxed">{property.description || 'لا يوجد وصف متاح.'}</p>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.4 }}
|
|
||||||
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100"
|
|
||||||
>
|
|
||||||
<h2 className="text-xl font-bold mb-4 text-gray-900">المميزات والخدمات</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
{property.features.map((feature, idx) => (
|
|
||||||
<div key={idx} className="flex items-start gap-3 p-3 bg-gray-50 rounded-xl">
|
|
||||||
<div className={`w-6 h-6 rounded-full flex items-center justify-center flex-shrink-0 ${feature.available ? 'bg-gray-800 text-white' : 'bg-gray-200 text-gray-500'
|
|
||||||
}`}>
|
|
||||||
{feature.available ? (
|
|
||||||
<Check className="w-4 h-4" />
|
|
||||||
) : (
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className={`font-medium ${feature.available ? 'text-gray-900' : 'text-gray-400'}`}>
|
|
||||||
{feature.name}
|
|
||||||
</span>
|
|
||||||
{feature.description && (
|
|
||||||
<p className={`text-sm mt-1 ${feature.available ? 'text-gray-500' : 'text-gray-400'}`}>
|
|
||||||
{feature.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{property.reviewList && property.reviewList.length > 0 && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.6 }}
|
|
||||||
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100"
|
|
||||||
>
|
|
||||||
<h2 className="text-xl font-bold mb-4 text-gray-900">تقييمات المستأجرين</h2>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{property.reviewList.map((review, idx) => (
|
|
||||||
<div key={idx} className="border-b border-gray-100 last:border-0 pb-4 last:pb-0">
|
|
||||||
<div className="flex justify-between items-start mb-2">
|
|
||||||
<div>
|
|
||||||
<span className="font-bold text-gray-900">{review.user}</span>
|
|
||||||
<div className="flex items-center gap-1 mt-1">
|
|
||||||
{[...Array(5)].map((_, i) => (
|
|
||||||
<Star key={i} className={`w-4 h-4 ${i < review.rating ? 'fill-gray-800 text-gray-800' : 'text-gray-300'}`} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-gray-500">{review.date}</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-gray-600">{review.comment}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{property.rules && property.rules.length > 0 && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.7 }}
|
|
||||||
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100"
|
|
||||||
>
|
|
||||||
<h2 className="text-xl font-bold mb-4 text-gray-900">قوانين المنزل</h2>
|
|
||||||
<ul className="space-y-2">
|
|
||||||
{property.rules.map((rule, idx) => (
|
|
||||||
<li key={idx} className="flex items-center gap-2 text-gray-600">
|
|
||||||
<div className="w-1.5 h-1.5 bg-gray-400 rounded-full" />
|
|
||||||
{rule}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="sticky top-28">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, x: 20 }}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100 mb-6"
|
|
||||||
>
|
|
||||||
<h2 className="text-xl font-bold mb-4 text-gray-900">احجز هذا العقار</h2>
|
|
||||||
|
|
||||||
{/* Selected dates display */}
|
|
||||||
<div className="mb-4 flex gap-2 text-sm">
|
|
||||||
<div className="flex-1 bg-gray-50 p-3 rounded-xl">
|
|
||||||
<span className="text-gray-500 block mb-1">من</span>
|
|
||||||
<span className="font-medium text-gray-900">{bookingDates.start || '—'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 bg-gray-50 p-3 rounded-xl">
|
|
||||||
<span className="text-gray-500 block mb-1">إلى</span>
|
|
||||||
<span className="font-medium text-gray-900">{bookingDates.end || '—'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{bookingDates.start && bookingDates.end && (() => {
|
|
||||||
const days = Math.ceil((new Date(bookingDates.end) - new Date(bookingDates.start)) / (1000 * 60 * 60 * 24));
|
|
||||||
return days > 0 ? (
|
|
||||||
<div className="mb-4 text-center text-sm text-amber-600 font-medium bg-amber-50 p-2 rounded-xl">
|
|
||||||
{days} يوم{days > 1 ? 'اً' : 'اً'} {selectingEnd ? '— اضغط على تاريخ النهاية' : '✓'}
|
|
||||||
</div>
|
|
||||||
) : null;
|
|
||||||
})()}
|
|
||||||
|
|
||||||
{/* Calendar */}
|
|
||||||
<div className="mb-4 relative">
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
|
||||||
<button onClick={() => setCalendarMonth(new Date(calendarMonth.getFullYear(), calendarMonth.getMonth() - 1))} className="p-1 hover:bg-gray-100 rounded-lg">
|
|
||||||
<ChevronRight className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
<span className="font-bold text-gray-900">{monthNames[calendarMonth.getMonth()]} {calendarMonth.getFullYear()}</span>
|
|
||||||
<button onClick={() => setCalendarMonth(new Date(calendarMonth.getFullYear(), calendarMonth.getMonth() + 1))} className="p-1 hover:bg-gray-100 rounded-lg">
|
|
||||||
<ChevronLeft className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-7 gap-1 mb-1">
|
|
||||||
{dayNames.map((d) => (
|
|
||||||
<div key={d} className="text-center text-xs text-gray-500 font-medium py-1">{d}</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-7 gap-1">
|
|
||||||
{(() => {
|
|
||||||
const year = calendarMonth.getFullYear();
|
|
||||||
const month = calendarMonth.getMonth();
|
|
||||||
const daysInMonth = getDaysInMonth(year, month);
|
|
||||||
const firstDay = getFirstDayOfMonth(year, month);
|
|
||||||
const today = new Date().toISOString().split('T')[0];
|
|
||||||
const cells = [];
|
|
||||||
|
|
||||||
// Empty cells before first day
|
|
||||||
for (let i = 0; i < firstDay; i++) {
|
|
||||||
cells.push(<div key={`empty-${i}`} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let day = 1; day <= daysInMonth; day++) {
|
|
||||||
const dateStr = formatDateStr(year, month, day);
|
|
||||||
const available = isDateAvailable(dateStr);
|
|
||||||
const isStart = bookingDates.start === dateStr;
|
|
||||||
const isEnd = bookingDates.end === dateStr;
|
|
||||||
const inRange = isInRange(dateStr);
|
|
||||||
const isPast = dateStr < today;
|
|
||||||
|
|
||||||
cells.push(
|
|
||||||
<button
|
|
||||||
key={dateStr}
|
|
||||||
onClick={() => !isPast && handleCalendarClick(dateStr)}
|
|
||||||
disabled={isPast || !available}
|
|
||||||
className={`w-full aspect-square rounded-lg text-sm font-medium transition-all ${
|
|
||||||
isStart || isEnd
|
|
||||||
? 'bg-amber-500 text-white'
|
|
||||||
: inRange
|
|
||||||
? 'bg-amber-100 text-amber-800'
|
|
||||||
: available && !isPast
|
|
||||||
? 'bg-green-50 text-green-700 hover:bg-green-100 cursor-pointer'
|
|
||||||
: 'text-gray-300 cursor-not-allowed'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{day}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return cells;
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-4 mt-3 text-xs text-gray-500">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<div className="w-3 h-3 rounded bg-green-50 border border-green-200" />
|
|
||||||
<span>متاح</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<div className="w-3 h-3 rounded bg-amber-500" />
|
|
||||||
<span>محدد</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<div className="w-3 h-3 rounded bg-gray-100" />
|
|
||||||
<span>غير متاح</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-gray-50 p-4 rounded-xl mb-6">
|
|
||||||
{(() => {
|
|
||||||
const days = bookingDates.start && bookingDates.end
|
|
||||||
? Math.ceil((new Date(bookingDates.end) - new Date(bookingDates.start)) / (1000 * 60 * 60 * 24))
|
|
||||||
: 0;
|
|
||||||
const effectiveDays = days > 0 ? days : 1;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex justify-between mb-2">
|
|
||||||
<span className="text-gray-600">السعر لـ {effectiveDays} يوم{effectiveDays > 1 ? 'اً' : 'اً'}</span>
|
|
||||||
<span className="font-bold text-gray-900">{formatCurrency(property.price * effectiveDays)}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between mb-2">
|
|
||||||
<span className="text-gray-600">سلفة ضمان</span>
|
|
||||||
<span className="font-bold text-gray-900">{formatCurrency(property._raw?.deposit || 0)}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between pt-2 border-t border-gray-200 font-bold">
|
|
||||||
<span className="text-gray-900">الإجمالي</span>
|
|
||||||
<span className="text-gray-900">{formatCurrency(property.price * effectiveDays + (property._raw?.deposit || 0))}</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{bookingError && (
|
|
||||||
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-xl text-red-700 text-sm">
|
|
||||||
{bookingError}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{bookingSuccess && (
|
|
||||||
<div className="mb-4 p-3 bg-green-50 border border-green-200 rounded-xl text-green-700 text-sm">
|
|
||||||
تم إرسال طلب الحجز بنجاح. سيتم التواصل معك قريباً.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={handleBooking}
|
|
||||||
disabled={bookingSuccess}
|
|
||||||
className="w-full bg-gray-800 text-white py-4 rounded-xl font-bold text-lg hover:bg-gray-900 transition-colors mb-4 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
{bookingSuccess ? 'تم الإرسال ✓' : 'تأكيد الحجز'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
|
||||||
<Shield className="w-4 h-4 text-gray-600" />
|
|
||||||
<span>الدفع آمن ومضمون. سلفة الضمان قابلة للاسترداد.</span>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, x: 20 }}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
transition={{ delay: 0.2 }}
|
|
||||||
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100"
|
|
||||||
>
|
|
||||||
<h3 className="font-bold mb-4 text-gray-900">معلومات المالك</h3>
|
|
||||||
<div className="flex items-center gap-3 mb-4">
|
|
||||||
<div className="w-12 h-12 bg-gray-100 rounded-full flex items-center justify-center">
|
|
||||||
<span className="text-xl font-bold text-gray-700">
|
|
||||||
{property.owner.name.charAt(0)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="font-bold text-gray-900">{property.owner.name}</div>
|
|
||||||
<div className="flex items-center gap-1 text-sm text-gray-500">
|
|
||||||
<Star className="w-3 h-3 fill-gray-600 text-gray-600" />
|
|
||||||
<span>{property.owner.rating}</span>
|
|
||||||
<span>· {property.owner.properties} عقارات</span>
|
|
||||||
</div>
|
|
||||||
{property.owner.responseRate && (
|
|
||||||
<div className="flex items-center gap-1 text-xs text-gray-500 mt-1">
|
|
||||||
<Clock className="w-3 h-3" />
|
|
||||||
<span>استجابة: {property.owner.responseRate}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showContact ? (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center gap-2 p-3 bg-gray-50 rounded-xl">
|
|
||||||
<Phone className="w-4 h-4 text-gray-600" />
|
|
||||||
<span className="font-medium text-gray-900">{property.owner.phone}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 p-3 bg-gray-50 rounded-xl">
|
|
||||||
<Mail className="w-4 h-4 text-gray-600" />
|
|
||||||
<span className="font-medium text-gray-900">{property.owner.email}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
onClick={() => setShowContact(true)}
|
|
||||||
className="w-full bg-gray-800 text-white py-3 rounded-xl font-medium hover:bg-gray-900 transition-colors flex items-center justify-center gap-2"
|
|
||||||
>
|
|
||||||
<Phone className="w-5 h-5" />
|
|
||||||
عرض معلومات الاتصال
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button className="w-full mt-3 bg-gray-100 text-gray-700 py-3 rounded-xl font-medium hover:bg-gray-200 transition-colors flex items-center justify-center gap-2">
|
|
||||||
<MessageCircle className="w-5 h-5" />
|
|
||||||
مراسلة المالك
|
|
||||||
</button>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Login/Register Dialog */}
|
|
||||||
{showLoginDialog && (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm" onClick={() => setShowLoginDialog(false)}>
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
className="bg-white rounded-2xl p-8 max-w-md w-full mx-4 shadow-2xl text-center"
|
|
||||||
>
|
|
||||||
<div className="w-16 h-16 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
||||||
<LogIn className="w-8 h-8 text-amber-600" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-xl font-bold text-gray-900 mb-2">سجّل الدخول للمتابعة</h3>
|
|
||||||
<p className="text-gray-500 mb-6">يجب عليك إنشاء حساب أو تسجيل الدخول لحجز هذا العقار</p>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Link
|
|
||||||
href="/auth/choose-role"
|
|
||||||
className="block w-full bg-amber-500 text-white py-3 rounded-xl font-bold hover:bg-amber-600 transition-colors"
|
|
||||||
>
|
|
||||||
إنشاء حساب جديد
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href="/login"
|
|
||||||
className="block w-full bg-gray-100 text-gray-700 py-3 rounded-xl font-bold hover:bg-gray-200 transition-colors"
|
|
||||||
>
|
|
||||||
تسجيل الدخول
|
|
||||||
</Link>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowLoginDialog(false)}
|
|
||||||
className="block w-full text-gray-400 py-2 text-sm hover:text-gray-600 transition-colors"
|
|
||||||
>
|
|
||||||
إلغاء
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user