diff --git a/app/globals.css b/app/globals.css index 0a7aa79..b51d745 100644 --- a/app/globals.css +++ b/app/globals.css @@ -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); diff --git a/app/layout.js b/app/layout.js index fc59121..e5bed49 100644 --- a/app/layout.js +++ b/app/layout.js @@ -25,7 +25,29 @@ export const metadata = { export default function Layout({ children }) { return ( - + + + + + 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 [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 [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 ( +
+
+
+

جاري تحميل تفاصيل العقار...

+
+
+ ); + } + + if (!property) { + return ( +
+
+ +

العقار غير موجود

+

لم نتمكن من العثور على العقار المطلوب

+ + العودة إلى العقارات + +
+
+ ); + } + + return ( +
+ +
+
+
+ + + العودة إلى العقارات + +
+ + {/* Share Dropdown */} +
+ + + {showShareMenu && ( + <> +
setShowShareMenu(false)} /> +
+
+ {/* Facebook */} + + + {/* WhatsApp */} + + + {/* Telegram */} + + + {/* Instagram (copy link) */} + + + {/* Copy Link */} + +
+
+ + )} +
+
+
+
+
+ +
+ +
+
+ {property.title} + + {property.images.length > 1 && ( + <> + + + + )} + +
+ {property.images.map((_, idx) => ( +
+ +
+ + {currentImage + 1} / {property.images.length} +
+
+ +
+ {property.images.slice(1, 5).map((img, idx) => ( +
setCurrentImage(idx + 1)} + className="relative h-[240px] rounded-2xl overflow-hidden cursor-pointer hover:opacity-90 transition-opacity bg-gray-100" + > + {`${property.title} +
+ ))} +
+
+
+ +
+
+ +
+
+

{property.title}

+
+ + {property.location.address} +
+
+
+
{formatCurrency(property.price)}
+
/{property.priceUnit === 'daily' ? 'يوم' : 'شهر'}
+
+
+ +
+
+ + {property.rating} + {property.reviews > 0 && ({property.reviews} تقييم)} +
+
+ + {property.status === 'available' ? 'متاح للإيجار' : 'محجوز حالياً'} + +
+ + + +

المواصفات الرئيسية

+
+
+ +
{property.bedrooms}
+
غرف نوم
+
+
+ +
{property.bathrooms}
+
حمامات
+
+
+ +
{property.area}
+
م²
+
+
+ +
+ {property.type === 'villa' ? 'فيلا' : + property.type === 'apartment' ? 'شقة' : 'بيت'} +
+
نوع العقار
+
+
+
+ + +

وصف العقار

+

{property.description || 'لا يوجد وصف متاح.'}

+
+ + +

المميزات والخدمات

+
+ {property.features.map((feature, idx) => ( +
+
+ {feature.available ? ( + + ) : ( + + )} +
+
+ + {feature.name} + + {feature.description && ( +

+ {feature.description} +

+ )} +
+
+ ))} +
+
+ + {property.reviewList && property.reviewList.length > 0 && ( + +

تقييمات المستأجرين

+
+ {property.reviewList.map((review, idx) => ( +
+
+
+ {review.user} +
+ {[...Array(5)].map((_, i) => ( + + ))} +
+
+ {review.date} +
+

{review.comment}

+
+ ))} +
+
+ )} + + {property.rules && property.rules.length > 0 && ( + +

قوانين المنزل

+
    + {property.rules.map((rule, idx) => ( +
  • +
    + {rule} +
  • + ))} +
+
+ )} +
+ +
+
+ +

احجز هذا العقار

+ + {/* Selected dates display */} +
+
+ من + {bookingDates.start || '—'} +
+
+ إلى + {bookingDates.end || '—'} +
+
+ + {bookingDates.start && bookingDates.end && (() => { + const days = Math.ceil((new Date(bookingDates.end) - new Date(bookingDates.start)) / (1000 * 60 * 60 * 24)); + return days > 0 ? ( +
+ {days} يوم{days > 1 ? 'اً' : 'اً'} {selectingEnd ? '— اضغط على تاريخ النهاية' : '✓'} +
+ ) : null; + })()} + + {/* Calendar */} +
+
+ + {monthNames[calendarMonth.getMonth()]} {calendarMonth.getFullYear()} + +
+ +
+ {dayNames.map((d) => ( +
{d}
+ ))} +
+ +
+ {(() => { + 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(
); + } + + 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( + + ); + } + return cells; + })()} +
+ +
+
+
+ متاح +
+
+
+ محدد +
+
+
+ غير متاح +
+
+
+ +
+ {(() => { + 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 ( + <> +
+ السعر لـ {effectiveDays} يوم{effectiveDays > 1 ? 'اً' : 'اً'} + {formatCurrency(property.price * effectiveDays)} +
+
+ سلفة ضمان + {formatCurrency(property._raw?.deposit || 0)} +
+
+ الإجمالي + {formatCurrency(property.price * effectiveDays + (property._raw?.deposit || 0))} +
+ + ); + })()} +
+ + {bookingError && ( +
+ {bookingError} +
+ )} + + {bookingSuccess && ( +
+ تم إرسال طلب الحجز بنجاح. سيتم التواصل معك قريباً. +
+ )} + + + +
+ + الدفع آمن ومضمون. سلفة الضمان قابلة للاسترداد. +
+ + + +

معلومات المالك

+
+
+ + {property.owner.name.charAt(0)} + +
+
+
{property.owner.name}
+
+ + {property.owner.rating} + · {property.owner.properties} عقارات +
+ {property.owner.responseRate && ( +
+ + استجابة: {property.owner.responseRate} +
+ )} +
+
+ + {showContact ? ( +
+
+ + {property.owner.phone} +
+
+ + {property.owner.email} +
+
+ ) : ( + + )} + + +
+
+
+
+
+ + {/* Login/Register Dialog */} + {showLoginDialog && ( +
setShowLoginDialog(false)}> + e.stopPropagation()} + className="bg-white rounded-2xl p-8 max-w-md w-full mx-4 shadow-2xl text-center" + > +
+ +
+

سجّل الدخول للمتابعة

+

يجب عليك إنشاء حساب أو تسجيل الدخول لحجز هذا العقار

+
+ + إنشاء حساب جديد + + + تسجيل الدخول + + +
+
+
+ )} +
+ ); +} diff --git a/app/property/[id]/page.js b/app/property/[id]/page.js index 22bfbe4..2c1191f 100644 --- a/app/property/[id]/page.js +++ b/app/property/[id]/page.js @@ -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, - }, - 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, + title: `${typeLabel} في ${address}`, + description: desc || `${typeLabel} في ${address} · ${bedrooms} غرف نوم · ${bathrooms} حمامات · ${area} م²`, + price, + priceUnit, + typeLabel, + address, + bedrooms, + bathrooms, + area, + image: firstImage, }; } -// 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 - -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 ( -
-
-
-

جاري تحميل تفاصيل العقار...

-
-
- ); + if (!raw) { + return { + title: 'SweetHome - عقار', + description: 'اكتشف أفضل العقارات للإيجار', + }; } - if (!property) { - return ( -
-
- -

العقار غير موجود

-

لم نتمكن من العثور على العقار المطلوب

- - العودة إلى العقارات - -
-
- ); - } + 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`; - return ( -
- -
-
-
- - - العودة إلى العقارات - -
- - -
-
-
-
+ // 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 }]; -
- -
-
- {property.title} - - {property.images.length > 1 && ( - <> - - - - )} - -
- {property.images.map((_, idx) => ( -
- -
- - {currentImage + 1} / {property.images.length} -
-
- -
- {property.images.slice(1, 5).map((img, idx) => ( -
setCurrentImage(idx + 1)} - className="relative h-[240px] rounded-2xl overflow-hidden cursor-pointer hover:opacity-90 transition-opacity bg-gray-100" - > - {`${property.title} -
- ))} -
-
-
- -
-
- -
-
-

{property.title}

-
- - {property.location.address} -
-
-
-
{formatCurrency(property.price)}
-
/{property.priceUnit === 'daily' ? 'يوم' : 'شهر'}
-
-
- -
-
- - {property.rating} - {property.reviews > 0 && ({property.reviews} تقييم)} -
-
- - {property.status === 'available' ? 'متاح للإيجار' : 'محجوز حالياً'} - -
- - - -

المواصفات الرئيسية

-
-
- -
{property.bedrooms}
-
غرف نوم
-
-
- -
{property.bathrooms}
-
حمامات
-
-
- -
{property.area}
-
م²
-
-
- -
- {property.type === 'villa' ? 'فيلا' : - property.type === 'apartment' ? 'شقة' : 'بيت'} -
-
نوع العقار
-
-
-
- - -

وصف العقار

-

{property.description || 'لا يوجد وصف متاح.'}

-
- - -

المميزات والخدمات

-
- {property.features.map((feature, idx) => ( -
-
- {feature.available ? ( - - ) : ( - - )} -
-
- - {feature.name} - - {feature.description && ( -

- {feature.description} -

- )} -
-
- ))} -
-
- - {property.reviewList && property.reviewList.length > 0 && ( - -

تقييمات المستأجرين

-
- {property.reviewList.map((review, idx) => ( -
-
-
- {review.user} -
- {[...Array(5)].map((_, i) => ( - - ))} -
-
- {review.date} -
-

{review.comment}

-
- ))} -
-
- )} - - {property.rules && property.rules.length > 0 && ( - -

قوانين المنزل

-
    - {property.rules.map((rule, idx) => ( -
  • -
    - {rule} -
  • - ))} -
-
- )} -
- -
-
- -

احجز هذا العقار

- - {/* Selected dates display */} -
-
- من - {bookingDates.start || '—'} -
-
- إلى - {bookingDates.end || '—'} -
-
- - {bookingDates.start && bookingDates.end && (() => { - const days = Math.ceil((new Date(bookingDates.end) - new Date(bookingDates.start)) / (1000 * 60 * 60 * 24)); - return days > 0 ? ( -
- {days} يوم{days > 1 ? 'اً' : 'اً'} {selectingEnd ? '— اضغط على تاريخ النهاية' : '✓'} -
- ) : null; - })()} - - {/* Calendar */} -
-
- - {monthNames[calendarMonth.getMonth()]} {calendarMonth.getFullYear()} - -
- -
- {dayNames.map((d) => ( -
{d}
- ))} -
- -
- {(() => { - 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(
); - } - - 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( - - ); - } - return cells; - })()} -
- -
-
-
- متاح -
-
-
- محدد -
-
-
- غير متاح -
-
-
- -
- {(() => { - 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 ( - <> -
- السعر لـ {effectiveDays} يوم{effectiveDays > 1 ? 'اً' : 'اً'} - {formatCurrency(property.price * effectiveDays)} -
-
- سلفة ضمان - {formatCurrency(property._raw?.deposit || 0)} -
-
- الإجمالي - {formatCurrency(property.price * effectiveDays + (property._raw?.deposit || 0))} -
- - ); - })()} -
- - {bookingError && ( -
- {bookingError} -
- )} - - {bookingSuccess && ( -
- تم إرسال طلب الحجز بنجاح. سيتم التواصل معك قريباً. -
- )} - - - -
- - الدفع آمن ومضمون. سلفة الضمان قابلة للاسترداد. -
- - - -

معلومات المالك

-
-
- - {property.owner.name.charAt(0)} - -
-
-
{property.owner.name}
-
- - {property.owner.rating} - · {property.owner.properties} عقارات -
- {property.owner.responseRate && ( -
- - استجابة: {property.owner.responseRate} -
- )} -
-
- - {showContact ? ( -
-
- - {property.owner.phone} -
-
- - {property.owner.email} -
-
- ) : ( - - )} - - -
-
-
-
-
- - {/* Login/Register Dialog */} - {showLoginDialog && ( -
setShowLoginDialog(false)}> - e.stopPropagation()} - className="bg-white rounded-2xl p-8 max-w-md w-full mx-4 shadow-2xl text-center" - > -
- -
-

سجّل الدخول للمتابعة

-

يجب عليك إنشاء حساب أو تسجيل الدخول لحجز هذا العقار

-
- - إنشاء حساب جديد - - - تسجيل الدخول - - -
-
-
- )} -
- ); + 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', + }, + twitter: { + card: 'summary_large_image', + title: `${p.title} - ${priceStr}`, + description: p.description, + images: ogImages.map(i => i.url), + }, + }; +} + +export default function PropertyPage({ params }) { + return ; } diff --git a/public/fonts/Madani Arabic Black.woff2 b/public/fonts/Madani Arabic Black.woff2 new file mode 100644 index 0000000..51d7e2a Binary files /dev/null and b/public/fonts/Madani Arabic Black.woff2 differ diff --git a/public/fonts/Madani Arabic Extra Bold.woff2 b/public/fonts/Madani Arabic Extra Bold.woff2 new file mode 100644 index 0000000..72a6def Binary files /dev/null and b/public/fonts/Madani Arabic Extra Bold.woff2 differ diff --git a/public/fonts/Madani Arabic Extra Light.woff2 b/public/fonts/Madani Arabic Extra Light.woff2 new file mode 100644 index 0000000..1a8985f Binary files /dev/null and b/public/fonts/Madani Arabic Extra Light.woff2 differ diff --git a/public/fonts/Madani Arabic Light.woff2 b/public/fonts/Madani Arabic Light.woff2 new file mode 100644 index 0000000..70bcfa8 Binary files /dev/null and b/public/fonts/Madani Arabic Light.woff2 differ diff --git a/public/fonts/Madani Arabic Medium.woff2 b/public/fonts/Madani Arabic Medium.woff2 new file mode 100644 index 0000000..8f9445d Binary files /dev/null and b/public/fonts/Madani Arabic Medium.woff2 differ diff --git a/public/fonts/Madani Arabic Semi Bold.woff2 b/public/fonts/Madani Arabic Semi Bold.woff2 new file mode 100644 index 0000000..e9e40a1 Binary files /dev/null and b/public/fonts/Madani Arabic Semi Bold.woff2 differ diff --git a/public/fonts/Madani Arabic Thin.woff2 b/public/fonts/Madani Arabic Thin.woff2 new file mode 100644 index 0000000..b0696ea Binary files /dev/null and b/public/fonts/Madani Arabic Thin.woff2 differ diff --git a/public/fonts/Madani Arabic Variable.woff2 b/public/fonts/Madani Arabic Variable.woff2 new file mode 100644 index 0000000..4113155 Binary files /dev/null and b/public/fonts/Madani Arabic Variable.woff2 differ diff --git a/public/fonts/Madani-Arabic-Bold.woff2 b/public/fonts/Madani-Arabic-Bold.woff2 new file mode 100644 index 0000000..32c032c Binary files /dev/null and b/public/fonts/Madani-Arabic-Bold.woff2 differ diff --git a/public/fonts/Madani-Arabic-Regular.woff2 b/public/fonts/Madani-Arabic-Regular.woff2 new file mode 100644 index 0000000..b2cae37 Binary files /dev/null and b/public/fonts/Madani-Arabic-Regular.woff2 differ