Files
SweetHome/app/property/[id]/PropertyDetail.js
2026-04-14 14:23:17 +00:00

1084 lines
52 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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,
Copy
} from 'lucide-react';
import { getRentProperty, getSaleProperty, bookReservation, checkAvailability, getAvailableDateRanges } from '../../utils/api';
import AuthService from '../../services/AuthService';
import { useFavorites } from '@/app/contexts/FavoritesContext';
import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from '../../enums';
import RatingForm from '@/app/components/ratings/RatingForm.js';
import RatingList from '@/app/components/ratings/RatingList.js';
import StarRating from '@/app/components/ratings/StarRating.js';
// Copy to clipboard that works on HTTP too
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
// Fallback for HTTP / older browsers
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
return true;
} catch {
return false;
} finally {
document.body.removeChild(textarea);
}
}
}
// 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 || 'https://45.93.137.91.nip.io/api') : '';
const rawImages = Array.isArray(info.images) ? info.images : [];
const images = rawImages.length > 0
? rawImages.map(img => img.startsWith('http') ? img : `${apiBase}${img.startsWith('/') ? '' : '/Pictures/'}${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 { isFavorite, addFavorite, removeFavorite } = useFavorites();
const [currentImage, setCurrentImage] = useState(0);
const [showContact, setShowContact] = useState(false);
const [showShareMenu, setShowShareMenu] = 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 [favLoading, setFavLoading] = useState(false);
const [selectingEnd, setSelectingEnd] = useState(false);
const [showLoginDialog, setShowLoginDialog] = useState(false);
const [showRatingForm, setShowRatingForm] = useState(false);
const [currentRating, setCurrentRating] = useState(0);
const [currentComment, setCurrentComment] = useState('');
const [userRating, setUserRating] = useState(null);
const [canRate, setCanRate] = 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 user rating and check if they can rate
useEffect(() => {
async function fetchUserRatingAndCheck() {
if (!property || !AuthService.isAuthenticated()) return;
try {
// Check if user has already rated
const rating = await getUserPropertyRating(property._raw?.id || parseInt(params.id), AuthService.getUserId());
if (rating) {
setUserRating(rating);
setCurrentRating(rating.rating);
setCurrentComment(rating.comment || '');
}
// Check if user can rate (e.g., after renting)
const canRateProperty = await canRateProperty(property._raw?.id || parseInt(params.id), AuthService.getUserId());
setCanRate(canRateProperty);
} catch (error) {
console.error('[Property] Failed to fetch user rating:', error);
}
}
fetchUserRatingAndCheck();
}, [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
onClick={async () => {
if (!AuthService.isAuthenticated()) { setShowLoginDialog(true); return; }
const propId = property?._raw?.id || parseInt(params.id);
setFavLoading(true);
if (isFavorite(propId)) {
await removeFavorite(propId);
toast.success('تمت الإزالة من المفضلة');
} else {
await addFavorite(propId);
toast.success('تمت الإضافة إلى المفضلة');
}
setFavLoading(false);
}}
disabled={favLoading}
className="p-2 hover:bg-gray-100 rounded-full transition-colors"
>
<Heart className={`w-5 h-5 ${isFavorite(property?._raw?.id || parseInt(params.id)) ? 'fill-red-500 text-red-500' : 'text-gray-600'}`} />
</button>
{/* Share Dropdown */}
<div className="relative">
<button
onClick={() => setShowShareMenu(!showShareMenu)}
className="p-2 hover:bg-gray-100 rounded-full transition-colors flex items-center gap-1"
title="مشاركة"
>
<Share2 className="w-5 h-5 text-gray-500" />
</button>
{showShareMenu && (
<>
<div className="fixed inset-0 z-40" onClick={() => setShowShareMenu(false)} />
<div className="absolute left-0 top-full mt-2 w-56 bg-white rounded-xl shadow-xl border border-gray-200 overflow-hidden z-50">
<div className="p-2 space-y-1">
{/* Facebook */}
<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}&quote=${quote}`, '_blank', 'width=600,height=400');
setShowShareMenu(false);
}}
className="w-full flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-blue-50 transition-colors"
>
<div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center">
<svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 24 24"><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>
</div>
<span className="text-sm font-medium text-gray-700">فيسبوك</span>
</button>
{/* WhatsApp */}
<button
onClick={() => {
const typeLabel = property.type === 'villa' ? 'فيلا' : property.type === 'apartment' ? 'شقة' : 'بيت';
const priceLabel = `${formatCurrency(property.price)} / ${property.priceUnit === 'daily' ? 'يوم' : 'شهر'}`;
const text = encodeURIComponent(
`🏠 ${typeLabel} في ${property.location?.address || ''}\n💰 ${priceLabel}\n🛏️ ${property.bedrooms} غرف | 📐 ${property.area} م²\n\n🔗 ${window.location.href}\n\n🔥 احجز على SweetHome`
);
window.open(`https://wa.me/?text=${text}`, '_blank');
setShowShareMenu(false);
}}
className="w-full flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-green-50 transition-colors"
>
<div className="w-8 h-8 bg-green-500 rounded-lg flex items-center justify-center">
<svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 24 24"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z"/></svg>
</div>
<span className="text-sm font-medium text-gray-700">واتساب</span>
</button>
{/* Telegram */}
<button
onClick={() => {
const typeLabel = property.type === 'villa' ? 'فيلا' : property.type === 'apartment' ? 'شقة' : 'بيت';
const priceLabel = `${formatCurrency(property.price)} / ${property.priceUnit === 'daily' ? 'يوم' : 'شهر'}`;
const text = encodeURIComponent(
`🏠 ${typeLabel} في ${property.location?.address || ''}\n💰 ${priceLabel}\n🛏️ ${property.bedrooms} غرف | 📐 ${property.area} م²\n\n🔥 احجز على SweetHome`
);
const url = encodeURIComponent(window.location.href);
window.open(`https://t.me/share/url?url=${url}&text=${text}`, '_blank');
setShowShareMenu(false);
}}
className="w-full flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-sky-50 transition-colors"
>
<div className="w-8 h-8 bg-sky-500 rounded-lg flex items-center justify-center">
<svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 24 24"><path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.479.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/></svg>
</div>
<span className="text-sm font-medium text-gray-700">تيليجرام</span>
</button>
{/* Instagram (copy link) */}
<button
onClick={async () => {
const ok = await copyToClipboard(window.location.href);
if (ok) {
toast.success('تم نسخ الرابط! الصقه في انستغرام');
} else {
toast.error('فشل نسخ الرابط');
}
window.open('https://www.instagram.com', '_blank');
setShowShareMenu(false);
}}
className="w-full flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-pink-50 transition-colors"
>
<div className="w-8 h-8 bg-gradient-to-br from-purple-600 via-pink-500 to-orange-400 rounded-lg flex items-center justify-center">
<svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>
</div>
<span className="text-sm font-medium text-gray-700">انستغرام</span>
</button>
{/* Copy Link */}
<button
onClick={async () => {
const ok = await copyToClipboard(window.location.href);
if (ok) {
toast.success('✅ تم نسخ الرابط');
} else {
toast.error('فشل نسخ الرابط');
}
setShowShareMenu(false);
}}
className="w-full flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-gray-100 transition-colors"
>
<div className="w-8 h-8 bg-gray-500 rounded-lg flex items-center justify-center">
<Copy className="w-4 h-4 text-white" />
</div>
<span className="text-sm font-medium text-gray-700">نسخ الرابط</span>
</button>
</div>
</div>
</>
)}
</div>
</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 className="flex items-start gap-2">
<div className="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0">
<User className="w-6 h-6 text-gray-600" />
</div>
<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>
</div>
<span className="text-sm text-gray-500">{review.date}</span>
</div>
<p className="text-gray-600">{review.comment}</p>
</div>
))}
</div>
</motion.div>
)}
{/* New Rating Components */}
{AuthService.isAuthenticated() && canRate && !userRating && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.65 }}
className="bg-amber-50 border-2 border-amber-200 rounded-xl p-4 text-center cursor-pointer hover:border-amber-300 transition-all"
onClick={() => setShowRatingForm(true)}
>
<Star className="w-8 h-8 text-amber-500 mx-auto mb-2" />
<h3 className="font-bold text-amber-700 mb-2">قيّم هذا العقار</h3>
<p className="text-sm text-amber-600">شارك تجربتك مع المستأجرين الآخرين</p>
</motion.div>
)}
<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-200"
>
<RatingList
propertyId={property._raw?.id || parseInt(params.id)}
userId={AuthService.getUserId()}
/>
</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>
);
}