Files
SweetHome/app/page.js

562 lines
21 KiB
JavaScript
Raw Normal View History

'use client';
2026-03-07 07:34:31 +03:00
import { useState, useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
ShieldCheck,
Lock,
Zap,
Star,
Rocket,
Search,
MapPin,
Home,
DollarSign,
ChevronDown,
Shield,
Award,
Sparkles,
UserCircle,
LogOut,
Calendar,
Building,
PlusCircle,
Heart,
MessageCircle
2026-03-07 07:34:31 +03:00
} from 'lucide-react';
import HeroSearch from './components/home/HeroSearch';
import PropertyMap from './components/home/PropertyMap';
import Link from 'next/link';
import Image from 'next/image';
import { getRentProperties, getSaleProperties } from './utils/api';
import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from './enums';
import AuthService from './services/AuthService';
// Map API property data to the format the UI expects
// API returns { propertyInformationId, deposit, monthlyRent, dailyRent, rating, propertyInformation: {...}, ... }
function mapApiProperty(item, index) {
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('يسمح بالتدخين');
if (item.isVisitorAllow) features.push('يسمح بالزوار');
if (item.specializedFor) features.push('متخصص');
if (info.numberOfBedRooms) features.push(`${info.numberOfBedRooms} غرف نوم`);
if (info.numberOfBathRooms) features.push(`${info.numberOfBathRooms} حمامات`);
return {
id: item.id ?? index + 1,
title: info.address || `عقار #${item.id || index + 1}`,
description: info.description || '',
type: propType,
price: dailyPrice,
priceUSD: 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,
images: ['/property-placeholder.jpg'],
status,
rating: item.rating || 4.5,
isNew: false,
allowedIdentities: ['syrian', 'passport'],
priceDisplay: {
daily: dailyPrice,
monthly: monthlyPrice,
},
bookings: [],
_raw: item,
};
}
// extractCity is now imported from @/app/enums
// API-only — no fallback data
export default function HomePage() {
2026-03-07 07:34:31 +03:00
const mapSectionRef = useRef(null);
const [searchFilters, setSearchFilters] = useState(null);
const [showMap, setShowMap] = useState(false);
const [filteredProperties, setFilteredProperties] = useState([]);
const [isScrolling, setIsScrolling] = useState(false);
const [user, setUser] = useState(null);
const [showUserMenu, setShowUserMenu] = useState(false);
const menuRef = useRef(null);
const [allProperties, setAllProperties] = useState([]);
const [loading, setLoading] = useState(true);
// Fetch properties from API on mount
useEffect(() => {
const authUser = AuthService.getUser();
if (authUser) {
setUser({
name: authUser.name || authUser.email,
email: authUser.email,
role: AuthService.isOwner() ? 'owner' : 'customer',
});
}
async function fetchProperties() {
try {
const [rentData, saleData] = await Promise.all([
getRentProperties().catch(() => []),
getSaleProperties().catch(() => []),
]);
const rentList = Array.isArray(rentData) ? rentData : [];
const saleList = Array.isArray(saleData) ? saleData : [];
const mapped = [
...rentList.map((p, i) => mapApiProperty(p, i)),
...saleList.map((p, i) => mapApiProperty(p, rentList.length + i)),
];
if (mapped.length > 0) {
setAllProperties(mapped);
}
// If API returns empty, keep fallback
} catch (err) {
console.error('[Home] Failed to fetch properties:', err);
} finally {
setLoading(false);
}
}
fetchProperties();
}, []);
useEffect(() => {
const handleClickOutside = (event) => {
if (menuRef.current && !menuRef.current.contains(event.target)) {
setShowUserMenu(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const logout = () => {
AuthService.deleteToken();
setUser(null);
setShowUserMenu(false);
};
2026-03-07 07:34:31 +03:00
const applyFilters = (filters) => {
setSearchFilters(filters);
2026-03-07 07:34:31 +03:00
const filtered = allProperties.filter(property => {
if (filters.city && filters.city !== 'all' && property.location.city !== filters.city) {
return false;
}
2026-03-07 07:34:31 +03:00
if (filters.propertyType && filters.propertyType !== 'all' && property.type !== filters.propertyType) {
return false;
}
2026-03-07 07:34:31 +03:00
if (filters.priceRange && filters.priceRange !== 'all') {
const priceUSD = property.priceUSD;
switch (filters.priceRange) {
2026-03-07 07:34:31 +03:00
case '0-500': if (priceUSD > 50) return false; break;
case '500-1000': if (priceUSD < 51 || priceUSD > 100) return false; break;
case '1000-2000': if (priceUSD < 101 || priceUSD > 200) return false; break;
case '2000-3000': if (priceUSD < 201 || priceUSD > 300) return false; break;
case '3000+': if (priceUSD < 301) return false; break;
}
}
2026-03-07 07:34:31 +03:00
if (filters.identityType && property.allowedIdentities) {
if (!property.allowedIdentities.includes(filters.identityType)) {
return false;
}
}
2026-03-07 07:34:31 +03:00
return true;
});
2026-03-07 07:34:31 +03:00
setFilteredProperties(filtered);
2026-03-07 07:34:31 +03:00
if (!showMap) {
setShowMap(true);
2026-03-07 07:34:31 +03:00
setTimeout(() => {
if (mapSectionRef.current) {
setIsScrolling(true);
mapSectionRef.current.scrollIntoView({
behavior: 'smooth',
2026-03-07 07:34:31 +03:00
block: 'center'
});
2026-03-07 07:34:31 +03:00
setTimeout(() => setIsScrolling(false), 1000);
}
}, 300);
} else {
if (mapSectionRef.current) {
setIsScrolling(true);
mapSectionRef.current.scrollIntoView({
behavior: 'smooth',
2026-03-07 07:34:31 +03:00
block: 'center'
});
setTimeout(() => setIsScrolling(false), 1000);
}
}
};
2026-03-07 07:34:31 +03:00
const resetSearch = () => {
setShowMap(false);
setSearchFilters(null);
setFilteredProperties([]);
window.scrollTo({
top: 0,
behavior: 'smooth'
});
};
2026-01-28 17:32:36 +03:00
const getUserInitial = () => {
if (user?.name) {
return user.name.charAt(0).toUpperCase();
}
return null;
};
const isOwner = user?.role === 'owner';
2026-01-28 17:32:36 +03:00
return (
2026-03-07 07:34:31 +03:00
<div className="min-h-screen">
<section className="relative min-h-screen flex items-center justify-center overflow-hidden">
<div className="absolute inset-0 z-0">
<motion.div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{
2026-03-07 07:34:31 +03:00
backgroundImage: 'url(/hero.jpg)',
}}
initial={{ scale: 1.1 }}
animate={{ scale: 1 }}
transition={{ duration: 1.5, ease: "easeOut" }}
/>
<div className="absolute inset-0 bg-gradient-to-r from-black/70 via-black/60 to-black/50" />
</div>
<div className="relative z-10 container mx-auto px-4 sm:px-6 lg:px-8">
<div className="max-w-6xl mx-auto">
<motion.div
className="text-center mb-12"
initial="hidden"
animate="visible"
2026-03-07 07:34:31 +03:00
variants={{
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.2 }
}
}}
2026-01-28 17:32:36 +03:00
>
<motion.h1
className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-6 leading-tight tracking-tight"
2026-03-07 07:34:31 +03:00
variants={{
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 }
}}
>
2026-03-20 13:33:15 +03:00
إيجاد منزلك الجديد<br />
<motion.span
className="text-amber-400"
2026-03-07 07:34:31 +03:00
animate={{
y: [0, -10, 0],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut"
}}
>
2026-03-20 13:33:15 +03:00
أصبح سهلاً
</motion.span>
</motion.h1>
<motion.p
className="text-base sm:text-lg text-gray-200 max-w-2xl mx-auto leading-relaxed"
2026-03-07 07:34:31 +03:00
variants={{
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 }
}}
>
2026-03-20 13:33:15 +03:00
نوفر قوائم عقارات عالية الجودة لمساعدتك في إيجاد المنزل المثالي
</motion.p>
</motion.div>
{!isOwner && <HeroSearch onSearch={applyFilters} />}
{isOwner && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="bg-white/10 backdrop-blur-lg rounded-2xl p-8 text-center border border-white/20"
>
<h2 className="text-2xl font-bold text-white mb-2">
مرحباً {user?.name}!
</h2>
<p className="text-gray-200 mb-4">
يمكنك إدارة عقاراتك من خلال لوحة التحكم الخاصة بك
</p>
<Link
href="/owner/properties"
className="inline-flex items-center gap-2 bg-amber-500 text-white px-6 py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors"
>
<Building className="w-5 h-5" />
إدارة عقاراتي
</Link>
</motion.div>
)}
</div>
2026-01-28 17:32:36 +03:00
</div>
{!showMap && !isOwner && (
<motion.div
2026-03-07 07:34:31 +03:00
className="absolute bottom-8 left-1/2 transform -translate-x-1/2 cursor-pointer"
animate={{
y: [0, 10, 0],
}}
transition={{
duration: 1.5,
repeat: Infinity,
ease: "easeInOut"
}}
onClick={() => window.scrollTo({
top: window.innerHeight,
behavior: 'smooth'
})}
2026-01-28 17:32:36 +03:00
>
2026-03-07 07:34:31 +03:00
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
</svg>
</motion.div>
2026-03-07 07:34:31 +03:00
)}
</section>
{!isOwner && (
<AnimatePresence mode="wait">
{showMap && (
<motion.section
ref={mapSectionRef}
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -50 }}
transition={{
type: "spring",
damping: 20,
stiffness: 100,
duration: 0.6
}}
className="py-12 bg-gray-50 relative"
>
{isScrolling && (
<motion.div
className="absolute top-0 left-0 right-0 h-1 bg-amber-500 z-10"
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ duration: 1, ease: "easeInOut" }}
/>
)}
<div className="container mx-auto px-4">
2026-03-07 07:34:31 +03:00
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="text-center mb-8"
2026-03-07 07:34:31 +03:00
>
<div className="flex items-center justify-center gap-4 mb-2">
<h2 className="text-3xl font-bold text-gray-900">
{filteredProperties.length > 0 ? 'نتائج البحث' : 'لا توجد نتائج'}
</h2>
<motion.button
onClick={resetSearch}
className="px-4 py-2 bg-white border border-gray-300 rounded-full text-sm font-medium text-gray-700 hover:bg-gray-50 shadow-sm flex items-center gap-2"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
بحث جديد
</motion.button>
2026-03-07 07:34:31 +03:00
</div>
{filteredProperties.length > 0 ? (
<p className="text-gray-600">
تم العثور على {filteredProperties.length} عقار يطابق معايير البحث
</p>
) : (
<p className="text-gray-600">
لا توجد عقارات تطابق معايير البحث. جرب تغيير الفلاتر.
</p>
)}
2026-03-07 07:34:31 +03:00
</motion.div>
<motion.div
className="bg-white rounded-2xl shadow-xl overflow-hidden border border-gray-200"
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.3, type: "spring" }}
>
{filteredProperties.length > 0 ? (
<PropertyMap
properties={filteredProperties}
userIdentity={searchFilters?.identityType || 'syrian'}
/>
) : (
<div className="h-[400px] flex flex-col items-center justify-center bg-gray-50">
<div className="w-24 h-24 bg-gray-200 rounded-full flex items-center justify-center mb-4">
<svg className="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد نتائج</h3>
<p className="text-gray-500">حاول تغيير معايير البحث</p>
</div>
)}
</motion.div>
{filteredProperties.length > 0 && searchFilters && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="mt-6 flex flex-wrap gap-3 justify-center"
>
<div className="bg-white px-4 py-2 rounded-full shadow-sm border border-gray-200 text-sm">
<span className="text-gray-600">المدينة: </span>
<span className="font-bold text-gray-900">
{searchFilters.city === 'all' ? 'جميع المدن' : searchFilters.city}
</span>
</div>
<div className="bg-white px-4 py-2 rounded-full shadow-sm border border-gray-200 text-sm">
<span className="text-gray-600">نوع العقار: </span>
<span className="font-bold text-gray-900">
{searchFilters.propertyType === 'all' ? 'الكل' :
searchFilters.propertyType === 'apartment' ? 'شقة' :
searchFilters.propertyType === 'villa' ? 'فيلا' : 'بيت'}
</span>
</div>
<div className="bg-white px-4 py-2 rounded-full shadow-sm border border-gray-200 text-sm">
<span className="text-gray-600">نطاق السعر: </span>
<span className="font-bold text-gray-900">
{searchFilters.priceRange === 'all' ? 'جميع الأسعار' :
searchFilters.priceRange === '0-500' ? 'أقل من 50$' :
searchFilters.priceRange === '500-1000' ? '50$ - 100$' :
searchFilters.priceRange === '1000-2000' ? '100$ - 200$' :
searchFilters.priceRange === '2000-3000' ? '200$ - 300$' : 'أكثر من 300$'}
</span>
</div>
</motion.div>
)}
</div>
</motion.section>
)}
</AnimatePresence>
)}
<section className="py-20 bg-gradient-to-b from-white to-gray-50">
<div className="container mx-auto px-4">
<motion.div
className="text-center mb-12"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4 tracking-tight">
2026-03-20 13:33:15 +03:00
لماذا تختار سويت هوم؟
</h2>
<p className="text-gray-600 max-w-2xl mx-auto text-lg">
نجعل عملية إيجاد منزلك المثالي سهلة وسريعة
</p>
</motion.div>
2026-03-07 07:34:31 +03:00
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<motion.div
className="group bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.1 }}
whileHover={{ y: -4 }}
>
<div className="flex items-center gap-4 mb-4">
<div className="w-12 h-12 bg-amber-100 rounded-xl flex items-center justify-center group-hover:bg-amber-200 transition-colors duration-300">
<ShieldCheck className="w-6 h-6 text-amber-600" />
</div>
<h3 className="text-lg font-bold text-gray-900">
2026-03-20 13:33:15 +03:00
قوائم موثوقة
</h3>
</div>
<p className="text-gray-600 text-sm leading-relaxed">
كل عقار يتم التحقق منه بدقة لضمان الدقة والجودة.
</p>
</motion.div>
<motion.div
className="group bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.2 }}
whileHover={{ y: -4 }}
>
<div className="flex items-center gap-4 mb-4">
<div className="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center group-hover:bg-blue-200 transition-colors duration-300">
<Lock className="w-6 h-6 text-blue-600" />
</div>
<h3 className="text-lg font-bold text-gray-900">
2026-03-20 13:33:15 +03:00
عمليات آمنة
</h3>
</div>
<p className="text-gray-600 text-sm leading-relaxed">
2026-03-20 13:33:15 +03:00
سلامتك هي أولويتنا. نوفر معاملات آمنة ونحمي معلوماتك الشخصية.
</p>
</motion.div>
<motion.div
className="group bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.3 }}
whileHover={{ y: -4 }}
>
<div className="flex items-center gap-4 mb-4">
<div className="w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center group-hover:bg-green-200 transition-colors duration-300">
<Zap className="w-6 h-6 text-green-600" />
</div>
<h3 className="text-lg font-bold text-gray-900">
2026-03-20 13:33:15 +03:00
نتائج سريعة
</h3>
</div>
<p className="text-gray-600 text-sm leading-relaxed">
2026-03-20 13:33:15 +03:00
اعثر على منزلك المثالي في دقائق باستخدام خوارزميات البحث والمطابقة المتقدمة لدينا.
</p>
</motion.div>
2026-03-07 07:34:31 +03:00
</div>
</div>
</section>
2026-01-28 17:32:36 +03:00
</div>
);
}