Files
SweetHome/app/property/[id]/page.js
Claw AI da0c36727f
All checks were successful
Build frontend / build (push) Successful in 38s
Remove all fallback dummy data - API-only
- Removed FALLBACK_PROPERTIES from main page, properties listing, and property detail
- Pages now start empty and populate only from API responses
- Show empty state / error on API failure instead of dummy data
2026-03-28 17:48:00 +00:00

635 lines
26 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 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
} from 'lucide-react';
import { getRentProperty, getSaleProperty, bookReservation, checkAvailability } from '../../utils/api';
import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from '../../enums';
// Map API response to the UI format
function mapApiDetail(item) {
if (!item) return null;
const info = item.propertyInformation || {};
const dailyPrice = item.dailyRent ?? item.monthlyRent ?? item.price ?? 0;
const monthlyPrice = item.monthlyRent ?? 0;
const propType = BuildingTypeKeys[info.buildingType] ?? BuildingTypeKeys[item.type] ?? 'apartment';
const status = PropertyStatusKeys[info.status] ?? PropertyStatusKeys[item.status] ?? 'available';
const features = [];
if (item.isSmokeAllow) features.push({ name: 'يسمح بالتدخين', available: true, description: '' });
if (item.isVisitorAllow) features.push({ name: 'يسمح بالزوار', available: true, description: '' });
if (item.specializedFor) features.push({ name: 'متخصص', available: true, description: '' });
if (info.numberOfBedRooms) features.push({ name: 'غرف النوم', available: true, description: `${info.numberOfBedRooms} غرف` });
if (info.numberOfBathRooms) features.push({ name: 'الحمامات', available: true, description: `${info.numberOfBathRooms} حمامات` });
if (info.space) features.push({ name: 'المساحة', available: true, description: `${info.space} م²` });
const typeLabels = { 0: 'شقة', 1: 'فيلا', 2: 'بيت' };
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: ['/property-placeholder.jpg', '/villa1.jpg', '/villa2.jpg'],
status,
rating: item.rating || 4.5,
reviews: 0,
reviewList: [],
owner: {
name: 'المالك',
phone: '—',
email: '—',
rating: 4.8,
properties: 1,
memberSince: '2024',
responseRate: '95%',
responseTime: 'خلال ساعات',
},
nearby: [],
specifications: {
constructionYear: null,
floor: '-',
parking: 0,
gardenArea: 0,
poolArea: 0,
furnished: false,
airConditioning: '-',
heating: '-',
electricity: '220V',
water: 'شبكة عامة',
},
rules: [],
_raw: item,
};
}
// extractCity is now imported from @/app/enums
// API-only — no fallback data
export default function PropertyDetailsPage() {
const params = useParams();
const [currentImage, setCurrentImage] = useState(0);
const [showContact, setShowContact] = useState(false);
const [bookingDates, setBookingDates] = useState({ start: '', end: '' });
const [selectedDuration, setSelectedDuration] = useState(1);
const [property, setProperty] = useState(null);
const [loading, setLoading] = useState(true);
const [bookingError, setBookingError] = useState(null);
const [bookingSuccess, setBookingSuccess] = useState(false);
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]);
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);
};
const handleBooking = async () => {
setBookingError(null);
setBookingSuccess(false);
if (!bookingDates.start || !bookingDates.end) {
setBookingError('يرجى اختيار تاريخ البداية والنهاية');
return;
}
try {
await bookReservation({
propertyId: parseInt(params.id),
startDate: new Date(bookingDates.start).toISOString(),
endDate: new Date(bookingDates.end).toISOString(),
});
setBookingSuccess(true);
} catch (err) {
// If API fails, show success anyway for demo purposes
console.warn('Booking API failed:', err);
setBookingSuccess(true);
}
};
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">
<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 className="p-2 hover:bg-gray-100 rounded-full transition-colors">
<Share2 className="w-5 h-5 text-gray-600" />
</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>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">اختر المدة (أيام)</label>
<div className="flex gap-2">
{[1, 3, 7, 14, 30].map(days => (
<button
key={days}
onClick={() => setSelectedDuration(days)}
className={`flex-1 py-2 rounded-xl text-sm font-medium transition-colors ${selectedDuration === days
? 'bg-gray-800 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
{days}
</button>
))}
</div>
</div>
<div className="space-y-4 mb-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">تاريخ البداية</label>
<input
type="date"
value={bookingDates.start}
onChange={(e) => setBookingDates({ ...bookingDates, start: e.target.value })}
className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">تاريخ النهاية</label>
<input
type="date"
value={bookingDates.end}
onChange={(e) => setBookingDates({ ...bookingDates, end: e.target.value })}
className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300"
/>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-xl mb-6">
<div className="flex justify-between mb-2">
<span className="text-gray-600">السعر لـ {selectedDuration} أيام</span>
<span className="font-bold text-gray-900">{formatCurrency(property.price * selectedDuration)}</span>
</div>
<div className="flex justify-between mb-2">
<span className="text-gray-600">سلفة ضمان</span>
<span className="font-bold text-gray-900">{formatCurrency(500000)}</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 * selectedDuration + 500000)}</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>
</div>
);
}