Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 47s

This commit is contained in:
Rahaf
2026-03-30 19:26:08 +03:00
14 changed files with 1129 additions and 859 deletions

View File

@ -3,74 +3,74 @@
/* ─── Madani Arabic Font ─── */
@font-face {
font-family: 'Madani Arabic';
src: url('/fonts/Madani Arabic Thin.ttf') format('truetype');
src: url('/fonts/Madani Arabic Thin.woff2') format('woff2');
font-weight: 100;
font-style: normal;
font-display: swap;
font-display: block;
}
@font-face {
font-family: 'Madani Arabic';
src: url('/fonts/Madani Arabic Extra Light.ttf') format('truetype');
src: url('/fonts/Madani Arabic Extra Light.woff2') format('woff2');
font-weight: 200;
font-style: normal;
font-display: swap;
font-display: block;
}
@font-face {
font-family: 'Madani Arabic';
src: url('/fonts/Madani Arabic Light.ttf') format('truetype');
src: url('/fonts/Madani Arabic Light.woff2') format('woff2');
font-weight: 300;
font-style: normal;
font-display: swap;
font-display: block;
}
@font-face {
font-family: 'Madani Arabic';
src: url('/fonts/Madani-Arabic-Regular.ttf') format('truetype');
src: url('/fonts/Madani-Arabic-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
font-display: block;
}
@font-face {
font-family: 'Madani Arabic';
src: url('/fonts/Madani Arabic Medium.ttf') format('truetype');
src: url('/fonts/Madani Arabic Medium.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
font-display: block;
}
@font-face {
font-family: 'Madani Arabic';
src: url('/fonts/Madani Arabic Semi Bold.ttf') format('truetype');
src: url('/fonts/Madani Arabic Semi Bold.woff2') format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
font-display: block;
}
@font-face {
font-family: 'Madani Arabic';
src: url('/fonts/Madani-Arabic-Bold.ttf') format('truetype');
src: url('/fonts/Madani-Arabic-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
font-display: block;
}
@font-face {
font-family: 'Madani Arabic';
src: url('/fonts/Madani Arabic Extra Bold.ttf') format('truetype');
src: url('/fonts/Madani Arabic Extra Bold.woff2') format('woff2');
font-weight: 800;
font-style: normal;
font-display: swap;
font-display: block;
}
@font-face {
font-family: 'Madani Arabic';
src: url('/fonts/Madani Arabic Black.ttf') format('truetype');
src: url('/fonts/Madani Arabic Black.woff2') format('woff2');
font-weight: 900;
font-style: normal;
font-display: swap;
font-display: block;
}
:root {
@ -92,6 +92,10 @@
}
}
html, body {
font-family: 'Madani Arabic', 'Noto Sans Arabic', 'Cairo', Arial, sans-serif;
}
body {
background: var(--background);
color: var(--foreground);

View File

@ -25,7 +25,29 @@ export const metadata = {
export default function Layout({ children }) {
return (
<html lang="ar" dir="rtl">
<head />
<head>
<link
rel="preload"
as="font"
href="/fonts/Madani-Arabic-Regular.woff2"
type="font/woff2"
crossOrigin="anonymous"
/>
<link
rel="preload"
as="font"
href="/fonts/Madani-Arabic-Bold.woff2"
type="font/woff2"
crossOrigin="anonymous"
/>
<link
rel="preload"
as="font"
href="/fonts/Madani Arabic Medium.woff2"
type="font/woff2"
crossOrigin="anonymous"
/>
</head>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
style={{ fontFamily: "'Madani Arabic', 'Noto Sans Arabic', 'Cairo', Arial, sans-serif" }}

File diff suppressed because it is too large Load Diff

View File

@ -1,854 +1,96 @@
'use client';
import PropertyDetail from './PropertyDetail';
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';
// Server-side API fetch for metadata (runs at request time on server)
async function fetchPropertyForMeta(id) {
try {
const res = await fetch(`http://45.93.137.91/api/RentProperties/GetRentProperties`, {
next: { revalidate: 60 },
});
if (!res.ok) return null;
const text = await res.text();
const json = JSON.parse(text);
const items = Array.isArray(json?.data) ? json.data : Array.isArray(json) ? json : [];
return items.find(p => p.id == id) || items[0] || null;
} catch {
return null;
}
}
// Map API response to the UI format
function mapApiDetail(item) {
if (!item) return null;
function mapProperty(item) {
const info = item.propertyInformation || item.PropertyInformation || {};
let details = {};
try { details = JSON.parse(info.detailsJSON || info.DetailsJSON || '{}'); } catch {}
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'];
const price = item.monthlyRent || item.MonthlyRent || item.dailyRent || item.DailyRent || 0;
const priceUnit = item.monthlyRent || item.MonthlyRent ? 'monthly' : 'daily';
const buildingType = info.buildingType ?? info.BuildingType ?? 0;
const type = { 0: 'apartment', 1: 'villa', 2: 'house' }[buildingType] || 'apartment';
const typeLabel = { 0: 'شقة', 1: 'فيلا', 2: 'بيت' }[buildingType] || 'عقار';
const address = info.address || info.Address || '';
const bedrooms = info.numberOfBedRooms || info.NumberOfBedRooms || 0;
const bathrooms = info.numberOfBathRooms || info.NumberOfBathRooms || 0;
const area = info.space || info.Space || 0;
const desc = info.description || info.Description || '';
const images = info.images || info.Images || [];
const firstImage = Array.isArray(images) && images[0] ? images[0] : '';
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,
title: `${typeLabel} في ${address}`,
description: desc || `${typeLabel} في ${address} · ${bedrooms} غرف نوم · ${bathrooms} حمامات · ${area} م²`,
price,
priceUnit,
typeLabel,
address,
bedrooms,
bathrooms,
area,
image: firstImage,
};
}
export async function generateMetadata({ params }) {
const { id } = await params;
const raw = await fetchPropertyForMeta(id);
if (!raw) {
return {
title: 'SweetHome - عقار',
description: 'اكتشف أفضل العقارات للإيجار',
};
}
const p = mapProperty(raw);
const priceStr = `${p.price.toLocaleString()} ل.س / ${p.priceUnit === 'daily' ? 'يوم' : 'شهر'}`;
const propertyImage = p.image
? (p.image.startsWith('http') ? p.image : `http://45.93.137.91${p.image}`)
: '';
const logoUrl = `http://45.93.137.91/logo.png`;
// Use property image if available, otherwise logo
const ogImages = propertyImage
? [{ url: propertyImage, width: 1200, height: 630 }, { url: logoUrl, width: 512, height: 512 }]
: [{ url: logoUrl, width: 512, height: 512 }];
return {
title: `${p.title} - ${priceStr}`,
description: p.description,
openGraph: {
title: `${p.title} - ${priceStr}`,
description: p.description,
images: ogImages,
url: `http://45.93.137.91/property/${id}`,
type: 'website',
siteName: 'SweetHome',
},
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: 'خلال ساعات',
twitter: {
card: 'summary_large_image',
title: `${p.title} - ${priceStr}`,
description: p.description,
images: ogImages.map(i => i.url),
},
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]);
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 title = encodeURIComponent(property?.title || 'عقار');
const url = encodeURIComponent(window.location.href);
const quote = encodeURIComponent(`${property?.title || 'عقار'} - SweetHome\n${property?.location?.address || ''}`);
window.open(`https://www.facebook.com/sharer/sharer.php?u=${url}&quote=${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>
);
export default function PropertyPage({ params }) {
return <PropertyDetail params={params} />;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.