Compare commits

...

2 Commits

Author SHA1 Message Date
1c8e888ea3 Added Admin page, Login, forgot password, register and owner with profile 2026-03-17 20:36:59 +03:00
8c75c7c659 Added map for home 2026-03-07 07:34:31 +03:00
21 changed files with 6818 additions and 645 deletions

651
app/ClientLayout.js Normal file
View File

@ -0,0 +1,651 @@
'use client';
import { usePathname } from 'next/navigation';
import { useTranslation } from 'react-i18next';
import Link from "next/link";
import Image from "next/image";
import { NavLink, MobileNavLink } from "./components/NavLinks";
import {
Globe,
LogIn,
UserPlus,
UserCircle,
LogOut,
Calendar,
Building,
PlusCircle,
Heart,
MessageCircle,
Settings,
Edit,
Phone,
Mail,
MapPin,
Camera,
Shield,
Bell,
Home,
ChevronDown,
Menu,
X,
TrendingUp,
CalendarDays,
Clock,
Users,
DollarSign
} from 'lucide-react';
import { useState, useEffect, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import './i18n/config';
export default function ClientLayout({ children }) {
const { t, i18n } = useTranslation();
const pathname = usePathname();
const [currentLanguage, setCurrentLanguage] = useState('en');
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [showUserMenu, setShowUserMenu] = useState(false);
const [user, setUser] = useState(null);
const [isMounted, setIsMounted] = useState(false);
const currentYear = new Date().getFullYear();
const menuRef = useRef(null);
useEffect(() => {
setIsMounted(true);
const savedLanguage = localStorage.getItem('language') || 'en';
setCurrentLanguage(savedLanguage);
i18n.changeLanguage(savedLanguage);
const storedUser = localStorage.getItem('user');
if (storedUser) {
const userData = JSON.parse(storedUser);
console.log('User data loaded:', userData);
setUser(userData);
}
if (savedLanguage === 'ar') {
document.documentElement.dir = 'rtl';
document.documentElement.lang = 'ar';
} else {
document.documentElement.dir = 'ltr';
document.documentElement.lang = 'en';
}
}, [i18n]);
useEffect(() => {
const handleClickOutside = (event) => {
if (menuRef.current && !menuRef.current.contains(event.target)) {
setShowUserMenu(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const changeLanguage = (lng) => {
i18n.changeLanguage(lng);
setCurrentLanguage(lng);
localStorage.setItem('language', lng);
if (lng === 'ar') {
document.documentElement.dir = 'rtl';
document.documentElement.lang = 'ar';
} else {
document.documentElement.dir = 'ltr';
document.documentElement.lang = 'en';
}
};
const toggleMobileMenu = () => {
setIsMobileMenuOpen(!isMobileMenuOpen);
};
const closeMobileMenu = () => {
setIsMobileMenuOpen(false);
};
const logout = () => {
localStorage.removeItem('user');
setUser(null);
setShowUserMenu(false);
window.location.href = '/';
};
const isAuthPage = ['/login', '/register', '/forgot-password', '/auth/choose-role'].includes(pathname);
const isProfilePage = pathname === '/profile';
const isOwner = user?.role === 'owner';
const isAdmin = user?.role === 'admin';
console.log('User role:', user?.role);
console.log('Is Admin:', isAdmin);
const getUserInitial = () => {
if (user?.name) {
return user.name.charAt(0).toUpperCase();
}
return <UserCircle className="w-5 h-5" />;
};
if (!isMounted) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
<p className="text-gray-600">جاري التحميل...</p>
</div>
</div>
);
}
return (
<>
{!isAuthPage && (
<nav className="fixed top-0 left-0 right-0 bg-white/95 backdrop-blur-sm border-b border-gray-200 z-50 transition-all duration-300 shadow-sm">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className={`flex justify-between items-center h-20 ${currentLanguage === 'ar' ? 'flex-row-reverse' : ''}`}>
<div className="flex items-center">
<Link href="/" className="flex items-center space-x-3 group">
<div className="relative w-10 h-10">
<Image
src="/logo.png"
alt={t("logoAlt")}
fill
className="object-contain group-hover:scale-105 transition-transform duration-300"
priority
sizes="40px"
/>
</div>
<span className="text-3xl font-bold text-gray-800 hidden md:block">
{t("brandNamePart1")}<span className="text-amber-600">{t("brandNamePart2")}</span>
</span>
</Link>
</div>
<div className="hidden md:flex items-center space-x-4">
<div className={`flex items-center space-x-1 ${currentLanguage === 'ar' ? 'flex-row-reverse space-x-reverse' : ''}`}>
<NavLink href="/">
{t("home")}
</NavLink>
<NavLink href="/properties">
{t("ourProducts")}
</NavLink>
{isAdmin && (
<NavLink href="/admin">
<span className="flex items-center gap-2">
<Shield className="w-4 h-4" />
الإدارة
</span>
</NavLink>
)}
{isOwner && (
<>
<NavLink href="/owner/properties">
<span className="flex items-center gap-2">
<Building className="w-4 h-4" />
عقاراتي
</span>
</NavLink>
<NavLink href="/owner/bookings">
<span className="flex items-center gap-2">
<Calendar className="w-4 h-4" />
الحجوزات
</span>
</NavLink>
<NavLink href="/owner/calendar">
<span className="flex items-center gap-2">
<CalendarDays className="w-4 h-4" />
التقويم
</span>
</NavLink>
<NavLink href="/owner/profits">
<span className="flex items-center gap-2">
<TrendingUp className="w-4 h-4" />
الأرباح
</span>
</NavLink>
</>
)}
{!user && !isAuthPage && (
<>
<NavLink href="/login">
<span className="flex items-center gap-2">
<LogIn className="w-4 h-4" />
تسجيل الدخول
</span>
</NavLink>
<NavLink href="/auth/choose-role">
<span className="flex items-center gap-2">
<UserPlus className="w-4 h-4" />
إنشاء حساب
</span>
</NavLink>
</>
)}
</div>
<motion.button
whileHover={{ scale: 1.1, rotate: 360 }}
whileTap={{ scale: 0.9 }}
onClick={() => changeLanguage(currentLanguage === 'en' ? 'ar' : 'en')}
className="flex items-center justify-center w-10 h-10 bg-gray-100 hover:bg-gray-200 rounded-full transition-all duration-200 ml-4"
>
<Globe className="w-5 h-5 text-gray-700" />
</motion.button>
{user && (
<div className="relative" ref={menuRef}>
<motion.button
onClick={() => setShowUserMenu(!showUserMenu)}
className="w-10 h-10 bg-gradient-to-br from-amber-500 to-amber-600 rounded-full flex items-center justify-center text-white font-bold text-lg shadow-lg hover:shadow-xl transition-all hover:scale-105 focus:outline-none"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
>
{getUserInitial()}
</motion.button>
<AnimatePresence>
{showUserMenu && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="absolute left-0 mt-2 w-72 bg-white rounded-xl shadow-xl border border-gray-200 overflow-hidden z-50"
>
<div className="bg-gradient-to-r from-amber-500 to-amber-600 p-4 text-white">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-white/20 rounded-full flex items-center justify-center text-xl font-bold">
{getUserInitial()}
</div>
<div>
<p className="font-bold">{user?.name || 'مستخدم'}</p>
<p className="text-xs text-amber-100">{user?.email || ''}</p>
<p className="text-xs text-amber-100 mt-1">
{isOwner ? 'مالك عقار' : isAdmin ? 'مدير النظام' : 'مستأجر'}
</p>
</div>
</div>
</div>
<div className="p-2">
<Link
href="/profile"
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
onClick={() => setShowUserMenu(false)}
>
<UserCircle className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">الملف الشخصي</p>
<p className="text-xs text-gray-500">عرض وتعديل معلوماتك</p>
</div>
</Link>
<Link
href="/favorites"
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
onClick={() => setShowUserMenu(false)}
>
<Heart className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">المفضلة</p>
<p className="text-xs text-gray-500">العقارات المحفوظة</p>
</div>
</Link>
{isOwner && (
<>
<div className="border-t border-gray-100 my-2"></div>
<Link
href="/owner/properties"
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
onClick={() => setShowUserMenu(false)}
>
<Building className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">عقاراتي</p>
<p className="text-xs text-gray-500">إدارة عقاراتك</p>
</div>
</Link>
<Link
href="/owner/properties/add"
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
onClick={() => setShowUserMenu(false)}
>
<PlusCircle className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">إضافة عقار</p>
<p className="text-xs text-gray-500">أضف عقاراً جديداً</p>
</div>
</Link>
<Link
href="/owner/bookings"
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
onClick={() => setShowUserMenu(false)}
>
<Calendar className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">الحجوزات</p>
<p className="text-xs text-gray-500">إدارة حجوزاتك</p>
</div>
</Link>
<Link
href="/owner/calendar"
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
onClick={() => setShowUserMenu(false)}
>
<CalendarDays className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">التقويم</p>
<p className="text-xs text-gray-500">جدول توفر العقارات</p>
</div>
</Link>
<Link
href="/owner/profits"
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
onClick={() => setShowUserMenu(false)}
>
<TrendingUp className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">الأرباح</p>
<p className="text-xs text-gray-500">إحصائيات وأرباح</p>
</div>
</Link>
</>
)}
{isAdmin && (
<>
<div className="border-t border-gray-100 my-2"></div>
<Link
href="/admin"
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
onClick={() => setShowUserMenu(false)}
>
<Shield className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">لوحة التحكم</p>
<p className="text-xs text-gray-500">إدارة المنصة</p>
</div>
</Link>
<Link
href="/admin/users"
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
onClick={() => setShowUserMenu(false)}
>
<Users className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">المستخدمين</p>
<p className="text-xs text-gray-500">إدارة المستخدمين</p>
</div>
</Link>
<Link
href="/admin/properties"
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
onClick={() => setShowUserMenu(false)}
>
<Building className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">العقارات</p>
<p className="text-xs text-gray-500">إدارة جميع العقارات</p>
</div>
</Link>
<Link
href="/admin/bookings"
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
onClick={() => setShowUserMenu(false)}
>
<Calendar className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">الحجوزات</p>
<p className="text-xs text-gray-500">إدارة الحجوزات</p>
</div>
</Link>
<Link
href="/admin/ledger"
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
onClick={() => setShowUserMenu(false)}
>
<DollarSign className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">دفتر الحسابات</p>
<p className="text-xs text-gray-500">إدارة المعاملات المالية</p>
</div>
</Link>
</>
)}
{!isOwner && !isAdmin && user && (
<>
<div className="border-t border-gray-100 my-2"></div>
<Link
href="/tenant/bookings"
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
onClick={() => setShowUserMenu(false)}
>
<Calendar className="w-5 h-5 text-amber-500" />
<div>
<p className="font-medium">حجوزاتي</p>
<p className="text-xs text-gray-500">عرض حجوزاتك</p>
</div>
</Link>
</>
)}
<div className="border-t border-gray-100 my-2"></div>
<button
onClick={logout}
className="w-full flex items-center gap-3 px-4 py-3 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
>
<LogOut className="w-5 h-5" />
<div>
<p className="font-medium">تسجيل الخروج</p>
<p className="text-xs text-red-400">إنهاء الجلسة الحالية</p>
</div>
</button>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
)}
</div>
<div className="md:hidden flex items-center gap-3">
<motion.button
whileHover={{ scale: 1.1, rotate: 360 }}
whileTap={{ scale: 0.9 }}
onClick={() => changeLanguage(currentLanguage === 'en' ? 'ar' : 'en')}
className="flex items-center justify-center w-10 h-10 bg-gray-100 hover:bg-gray-200 rounded-full transition-colors"
>
<Globe className="w-5 h-5 text-gray-700" />
</motion.button>
{user && (
<button
onClick={() => setShowUserMenu(!showUserMenu)}
className="w-10 h-10 bg-gradient-to-br from-amber-500 to-amber-600 rounded-full flex items-center justify-center text-white font-bold shadow-lg"
>
{getUserInitial()}
</button>
)}
<button
type="button"
onClick={toggleMobileMenu}
className="inline-flex items-center justify-center p-2 rounded-md text-gray-700 hover:text-amber-600 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-amber-500"
>
<span className="sr-only">{t("openMainMenu")}</span>
{isMobileMenuOpen ? (
<X className="w-6 h-6" />
) : (
<Menu className="w-6 h-6" />
)}
</button>
</div>
</div>
</div>
{isMobileMenuOpen && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="md:hidden bg-white border-t border-gray-200 shadow-lg"
>
<div className="px-2 pt-2 pb-3 space-y-1">
<MobileNavLink href="/" onClick={closeMobileMenu}>
{t("home")}
</MobileNavLink>
<MobileNavLink href="/properties" onClick={closeMobileMenu}>
{t("ourProducts")}
</MobileNavLink>
{isAdmin && (
<MobileNavLink href="/admin" onClick={closeMobileMenu}>
<span className="flex items-center gap-2">
<Shield className="w-4 h-4" />
الإدارة
</span>
</MobileNavLink>
)}
{isOwner && (
<>
<MobileNavLink href="/owner/properties" onClick={closeMobileMenu}>
<span className="flex items-center gap-2">
<Building className="w-4 h-4" />
عقاراتي
</span>
</MobileNavLink>
<MobileNavLink href="/owner/bookings" onClick={closeMobileMenu}>
<span className="flex items-center gap-2">
<Calendar className="w-4 h-4" />
الحجوزات
</span>
</MobileNavLink>
<MobileNavLink href="/owner/calendar" onClick={closeMobileMenu}>
<span className="flex items-center gap-2">
<CalendarDays className="w-4 h-4" />
التقويم
</span>
</MobileNavLink>
<MobileNavLink href="/owner/profits" onClick={closeMobileMenu}>
<span className="flex items-center gap-2">
<TrendingUp className="w-4 h-4" />
الأرباح
</span>
</MobileNavLink>
</>
)}
{!user && (
<>
<div className="border-t border-gray-200 my-2"></div>
<MobileNavLink href="/login" onClick={closeMobileMenu}>
<span className="flex items-center gap-2">
<LogIn className="w-4 h-4" />
تسجيل الدخول
</span>
</MobileNavLink>
<MobileNavLink href="/auth/choose-role" onClick={closeMobileMenu}>
<span className="flex items-center gap-2">
<UserPlus className="w-4 h-4" />
إنشاء حساب
</span>
</MobileNavLink>
</>
)}
</div>
</motion.div>
)}
</nav>
)}
<main className={`${!isAuthPage && !isProfilePage ? 'pt-20' : ''} min-h-screen bg-gradient-to-b from-gray-50 to-white ${currentLanguage === 'ar' ? 'text-right' : 'text-left'}`}>
{children}
</main>
{!isAuthPage && !isProfilePage && (
<footer className="bg-gray-900 text-white py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className={`grid grid-cols-1 md:grid-cols-4 gap-8 ${currentLanguage === 'ar' ? 'text-right' : 'text-left'}`}>
<div className="space-y-4">
<div className={`flex items-center ${currentLanguage === 'ar' ? 'flex-row-reverse' : 'space-x-3'}`}>
<div className="relative w-10 h-10">
<Image
src="/logo.png"
alt={t("logoAlt")}
fill
className="object-contain"
sizes="40px"
/>
</div>
<span className="text-3xl font-bold">
{t("brandNamePart1")}<span className="text-amber-400">{t("brandNamePart2")}</span>
</span>
</div>
<p className="text-gray-400 text-sm">
{t("footerDescription")}
</p>
</div>
<div>
<h3 className="text-lg font-semibold mb-4">{t("quickLinks")}</h3>
<ul className="space-y-2">
<li>
<Link href="/" className="text-gray-400 hover:text-white transition-colors block py-1">
{t("home")}
</Link>
</li>
<li>
<Link href="/properties" className="text-gray-400 hover:text-white transition-colors block py-1">
{t("ourProducts")}
</Link>
</li>
{isAdmin && (
<li>
<Link href="/admin" className="text-gray-400 hover:text-white transition-colors block py-1">
الإدارة
</Link>
</li>
)}
</ul>
</div>
<div>
<h3 className="text-lg font-semibold mb-4">{t("contactUs")}</h3>
<ul className="space-y-3 text-gray-400">
<li className="flex items-center gap-2">
<Phone className="w-5 h-5" />
<span>{t("phone")}</span>
</li>
<li className="flex items-center gap-2">
<Mail className="w-5 h-5" />
<span>{t("footerEmail")}</span>
</li>
</ul>
</div>
</div>
<div className="mt-8 pt-8 border-t border-gray-800 text-center text-gray-400 text-sm">
<p>&copy; {currentYear} {t("copyright")}. {t("allRightsReserved")}</p>
</div>
</div>
</footer>
)}
</>
);
}

View File

@ -1,3 +1,5 @@
// app/admin/page.js (محدث)
'use client'; 'use client';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';

View File

@ -0,0 +1,232 @@
'use client';
import { motion } from 'framer-motion';
import Link from 'next/link';
import { Home, Building, ArrowLeft } from 'lucide-react';
export default function ChooseRolePage() {
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.2,
delayChildren: 0.3
}
}
};
const itemVariants = {
hidden: { y: 20, opacity: 0 },
visible: {
y: 0,
opacity: 1,
transition: { type: 'spring', stiffness: 100 }
}
};
const floatingAnimation = {
y: [0, -10, 0],
transition: {
duration: 3,
repeat: Infinity,
ease: "easeInOut"
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center p-4 relative overflow-hidden">
<div className="absolute inset-0 overflow-hidden">
{[...Array(20)].map((_, i) => (
<motion.div
key={i}
className="absolute rounded-full bg-amber-500/10"
style={{
left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`,
width: Math.random() * 100 + 50,
height: Math.random() * 100 + 50,
}}
animate={{
x: [0, Math.random() * 50 - 25, 0],
y: [0, Math.random() * 50 - 25, 0],
}}
transition={{
duration: Math.random() * 10 + 10,
repeat: Infinity,
ease: "linear"
}}
/>
))}
</div>
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="relative z-10 w-full max-w-4xl"
>
<motion.div
variants={itemVariants}
className="absolute -top-16 left-0"
>
<Link
href="/"
className="group flex items-center gap-2 text-gray-400 hover:text-white transition-colors"
>
<motion.div whileHover={{ x: -5 }}>
<ArrowLeft className="w-4 h-4" />
</motion.div>
<span>العودة للرئيسية</span>
</Link>
</motion.div>
<motion.div
variants={itemVariants}
className="text-center mb-12"
>
<motion.h1
className="text-4xl md:text-5xl font-bold text-white mb-4"
animate={floatingAnimation}
>
مرحباً بك في SweetHome
</motion.h1>
<p className="text-xl text-gray-400">
اختر نوع الحساب المناسب لك
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<motion.div
variants={itemVariants}
whileHover={{ scale: 1.05, y: -5 }}
whileTap={{ scale: 0.95 }}
className="group cursor-pointer"
>
<Link href="/register/owner">
<div className="bg-gradient-to-br from-amber-500 to-amber-600 rounded-3xl p-8 text-white shadow-2xl shadow-amber-500/20 relative overflow-hidden">
<motion.div
className="absolute -top-20 -right-20 w-40 h-40 bg-white/10 rounded-full"
animate={{
scale: [1, 1.2, 1],
rotate: [0, 90, 0],
}}
transition={{ duration: 8, repeat: Infinity }}
/>
<div className="relative z-10">
<motion.div
className="w-20 h-20 bg-white/20 rounded-2xl flex items-center justify-center mb-6 backdrop-blur-sm"
whileHover={{ rotate: 360 }}
transition={{ duration: 0.5 }}
>
<Building className="w-10 h-10 text-white" />
</motion.div>
<h2 className="text-3xl font-bold mb-3">صاحب عقار</h2>
<p className="text-amber-100 mb-6">
تريد عرض عقار للإيجار أو البيع؟ انضم كمالك عقار وابدأ الآن
</p>
<ul className="space-y-2 text-sm text-amber-100">
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" />
إضافة عقارات غير محدودة
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" />
إدارة الحجوزات بسهولة
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" />
تواصل مباشر مع المستأجرين
</li>
</ul>
<motion.button
className="mt-8 w-full bg-white text-amber-600 py-3 rounded-xl font-bold hover:bg-amber-50 transition-colors"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
إنشاء حساب مالك
</motion.button>
</div>
</div>
</Link>
</motion.div>
<motion.div
variants={itemVariants}
whileHover={{ scale: 1.05, y: -5 }}
whileTap={{ scale: 0.95 }}
className="group cursor-pointer"
>
<Link href="/register/tenant">
<div className="bg-gradient-to-br from-blue-500 to-blue-600 rounded-3xl p-8 text-white shadow-2xl shadow-blue-500/20 relative overflow-hidden">
<motion.div
className="absolute -bottom-20 -left-20 w-40 h-40 bg-white/10 rounded-full"
animate={{
scale: [1, 1.2, 1],
rotate: [0, -90, 0],
}}
transition={{ duration: 8, repeat: Infinity }}
/>
<div className="relative z-10">
<motion.div
className="w-20 h-20 bg-white/20 rounded-2xl flex items-center justify-center mb-6 backdrop-blur-sm"
whileHover={{ rotate: 360 }}
transition={{ duration: 0.5 }}
>
<Home className="w-10 h-10 text-white" />
</motion.div>
<h2 className="text-3xl font-bold mb-3">مستأجر / مشتري</h2>
<p className="text-blue-100 mb-6">
تبحث عن عقار للإيجار أو الشراء؟ انضم كعميل وابحث عن منزل أحلامك
</p>
<ul className="space-y-2 text-sm text-blue-100">
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" />
آلاف العقارات المتاحة
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" />
بحث متقدم بفلاتر ذكية
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-white rounded-full" />
حجز وإستئجار فوري
</li>
</ul>
<motion.button
className="mt-8 w-full bg-white text-blue-600 py-3 rounded-xl font-bold hover:bg-blue-50 transition-colors"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
إنشاء حساب مستأجر
</motion.button>
</div>
</div>
</Link>
</motion.div>
</div>
<motion.p
variants={itemVariants}
className="text-center text-gray-400 mt-8"
>
لديك حساب بالفعل؟{' '}
<Link
href="/login"
className="text-amber-400 hover:text-amber-300 font-medium transition-colors"
>
تسجيل الدخول
</Link>
</motion.p>
</motion.div>
</div>
);
}

View File

@ -1,6 +1,7 @@
'use client'; 'use client';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import { LogIn, UserPlus } from 'lucide-react';
export function NavLink({ href, children }) { export function NavLink({ href, children }) {
const pathname = usePathname(); const pathname = usePathname();

View File

@ -0,0 +1,192 @@
'use client';
import { useState } from 'react';
import { motion } from 'framer-motion';
import { useTranslation } from 'react-i18next';
import { Search, MapPin, Home, DollarSign } from 'lucide-react';
export default function HeroSearch({ onSearch }) {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState('rent');
const [filters, setFilters] = useState({
city: '',
propertyType: '',
priceRange: '',
identityType: 'syrian'
});
const cities = [
{ id: 'all', label: 'جميع المدن' },
{ id: 'دمشق', label: 'دمشق' },
{ id: 'حلب', label: 'حلب' },
{ id: 'حمص', label: 'حمص' },
{ id: 'اللاذقية', label: 'اللاذقية' },
{ id: 'درعا', label: 'درعا' }
];
const propertyTypes = [
{ id: 'all', label: 'الكل' },
{ id: 'apartment', label: 'شقة' },
{ id: 'villa', label: 'فيلا' },
{ id: 'house', label: 'بيت' },
{ id: 'studio', label: 'استوديو' }
];
const priceRanges = [
{ id: 'all', label: 'جميع الأسعار' },
{ id: '0-500', label: 'أقل من 50$' },
{ id: '500-1000', label: '50$ - 100$' },
{ id: '1000-2000', label: '100$ - 200$' },
{ id: '2000-3000', label: '200$ - 300$' },
{ id: '3000+', label: 'أكثر من 300$' }
];
const identityTypes = [
{ id: 'syrian', label: 'هوية سورية' },
{ id: 'passport', label: 'جواز سفر' }
];
const handleSearch = () => {
onSearch({
...filters,
propertyType: filters.propertyType || 'all',
city: filters.city || 'all',
priceRange: filters.priceRange || 'all'
});
};
return (
<motion.div
className="bg-white/10 backdrop-blur-lg rounded-2xl p-6 sm:p-8 border border-white/20 shadow-2xl"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.5 }}
>
<div className="flex flex-wrap gap-2 mb-8">
{['rent', 'buy', 'sell'].map((tab) => (
<motion.button
key={tab}
onClick={() => setActiveTab(tab)}
className={`px-4 py-2 rounded-lg font-medium text-sm transition-all ${
activeTab === tab
? 'bg-amber-500 text-white'
: 'bg-white/20 text-white hover:bg-white/30'
}`}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
{t(`${tab}Tab`)}
</motion.button>
))}
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium text-white mb-2">
<div className="flex items-center gap-1">
<MapPin className="w-4 h-4" />
{t("cityStreetLabel")}
</div>
</label>
<select
value={filters.city}
onChange={(e) => setFilters({...filters, city: e.target.value})}
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'left 1rem center',
backgroundSize: '1rem',
paddingLeft: '2.5rem'
}}
>
{cities.map(city => (
<option key={city.id} value={city.id}>{city.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-white mb-2">
<div className="flex items-center gap-1">
<Home className="w-4 h-4" />
{t("rentTypeLabel")}
</div>
</label>
<select
value={filters.propertyType}
onChange={(e) => setFilters({...filters, propertyType: e.target.value})}
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'left 1rem center',
backgroundSize: '1rem',
paddingLeft: '2.5rem'
}}
>
{propertyTypes.map(type => (
<option key={type.id} value={type.id}>{type.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-white mb-2">
<div className="flex items-center gap-1">
<DollarSign className="w-4 h-4" />
{t("priceLabel")}
</div>
</label>
<select
value={filters.priceRange}
onChange={(e) => setFilters({...filters, priceRange: e.target.value})}
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'left 1rem center',
backgroundSize: '1rem',
paddingLeft: '2.5rem'
}}
>
{priceRanges.map(range => (
<option key={range.id} value={range.id}>{range.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-white mb-2">
<div className="flex items-center gap-1">
نوع الوثيقة
</div>
</label>
<select
value={filters.identityType}
onChange={(e) => setFilters({...filters, identityType: e.target.value})}
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'left 1rem center',
backgroundSize: '1rem',
paddingLeft: '2.5rem'
}}
>
{identityTypes.map(type => (
<option key={type.id} value={type.id}>{type.label}</option>
))}
</select>
</div>
</div>
<div className="mt-6">
<motion.button
onClick={handleSearch}
className="w-full bg-amber-500 hover:bg-amber-600 text-white font-bold py-4 px-6 rounded-xl transition-all duration-300 flex items-center justify-center text-base gap-3 shadow-lg hover:shadow-xl"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Search className="w-5 h-5" />
{t("searchButton")}
</motion.button>
</div>
</motion.div>
);
}

View File

@ -0,0 +1,287 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { MapPin, DollarSign, X, Navigation, Bed, Bath, Square, Star, Calendar, Heart, ChevronLeft, ChevronRight } from 'lucide-react';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import Image from 'next/image';
import Link from 'next/link';
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
});
const PropertyPopup = ({ property, onClose, onBook }) => {
const [currentImage, setCurrentImage] = useState(0);
const [isFavorite, setIsFavorite] = useState(false);
const [selectedDays, setSelectedDays] = useState(1);
const formatCurrency = (amount) => {
return amount?.toLocaleString() + ' ل.س';
};
const quickDays = [1, 3, 7, 14];
return (
<motion.div
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 20, scale: 0.95 }}
transition={{ type: "spring", damping: 25, stiffness: 300 }}
className="absolute bottom-6 left-1/2 transform -translate-x-1/2 w-[90%] max-w-md bg-white/95 backdrop-blur-sm rounded-2xl shadow-2xl overflow-hidden z-20 border border-gray-200/50"
style={{ maxHeight: '70vh' }}
>
<div className="relative h-32 bg-gradient-to-r from-gray-900 to-gray-700">
<Image
src={property.images[currentImage] || '/property-placeholder.jpg'}
alt={property.title}
fill
className="object-cover opacity-80"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent" />
<button
onClick={onClose}
className="absolute top-2 right-2 w-8 h-8 bg-black/50 backdrop-blur-sm rounded-full flex items-center justify-center hover:bg-black/70 transition-colors z-10"
>
<X className="w-4 h-4 text-white" />
</button>
<button
onClick={() => setIsFavorite(!isFavorite)}
className="absolute top-2 left-2 w-8 h-8 bg-black/50 backdrop-blur-sm rounded-full flex items-center justify-center hover:bg-black/70 transition-colors z-10"
>
<Heart className={`w-4 h-4 ${isFavorite ? 'fill-red-500 text-red-500' : 'text-white'}`} />
</button>
<div className="absolute bottom-2 left-3">
<div className="text-2xl font-bold text-white">{formatCurrency(property.price)}</div>
<div className="text-xs text-white/80">/ليلة</div>
</div>
<div className="absolute bottom-2 right-3 flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg">
<Star className="w-3 h-3 fill-amber-400 text-amber-400" />
<span className="text-sm font-medium text-white">{property.rating}</span>
</div>
{property.images.length > 1 && (
<>
<button
onClick={() => setCurrentImage(prev => Math.max(0, prev - 1))}
className="absolute left-2 top-1/2 transform -translate-y-1/2 w-6 h-6 bg-black/30 rounded-full flex items-center justify-center hover:bg-black/50 transition-colors"
>
<ChevronLeft className="w-4 h-4 text-white" />
</button>
<button
onClick={() => setCurrentImage(prev => Math.min(property.images.length - 1, prev + 1))}
className="absolute right-2 top-1/2 transform -translate-y-1/2 w-6 h-6 bg-black/30 rounded-full flex items-center justify-center hover:bg-black/50 transition-colors"
>
<ChevronRight className="w-4 h-4 text-white" />
</button>
</>
)}
{property.images.length > 1 && (
<div className="absolute bottom-2 left-1/2 transform -translate-x-1/2 flex gap-1">
{property.images.map((_, idx) => (
<div
key={idx}
className={`w-1 h-1 rounded-full transition-all ${
idx === currentImage ? 'w-3 bg-white' : 'bg-white/50'
}`}
/>
))}
</div>
)}
</div>
<div className="p-4 space-y-3">
<div>
<h3 className="font-bold text-gray-900 text-base line-clamp-1">{property.title}</h3>
<div className="flex items-center gap-1 text-gray-500 text-xs mt-1">
<MapPin className="w-3 h-3 flex-shrink-0" />
<span className="line-clamp-1">{property.location.address || `${property.location.city}، ${property.location.district}`}</span>
</div>
</div>
<div className="flex items-center justify-between bg-gray-50 p-2 rounded-xl">
<div className="flex items-center gap-1 text-xs">
<Bed className="w-3 h-3 text-gray-600" />
<span className="text-gray-700">{property.bedrooms} غرف</span>
</div>
<div className="w-px h-4 bg-gray-200" />
<div className="flex items-center gap-1 text-xs">
<Bath className="w-3 h-3 text-gray-600" />
<span className="text-gray-700">{property.bathrooms} حمامات</span>
</div>
<div className="w-px h-4 bg-gray-200" />
<div className="flex items-center gap-1 text-xs">
<Square className="w-3 h-3 text-gray-600" />
<span className="text-gray-700">{property.area}م²</span>
</div>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-2">اختر المدة</label>
<div className="flex gap-2">
{quickDays.map(days => (
<button
key={days}
onClick={() => setSelectedDays(days)}
className={`flex-1 py-2 rounded-lg text-xs font-medium transition-all ${
selectedDays === days
? 'bg-amber-500 text-white shadow-sm'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
{days} {days === 1 ? 'يوم' : 'أيام'}
</button>
))}
</div>
</div>
<div className="flex justify-between items-center p-3 bg-amber-50 rounded-xl">
<span className="text-sm text-gray-700">الإجمالي</span>
<div>
<span className="text-lg font-bold text-amber-600">{formatCurrency(property.price * selectedDays)}</span>
<span className="text-xs text-gray-500 mr-1">لـ {selectedDays} {selectedDays === 1 ? 'يوم' : 'أيام'}</span>
</div>
</div>
<div className="overflow-x-auto scrollbar-hide">
<div className="flex gap-2 pb-1" style={{ minWidth: 'min-content' }}>
{property.features.slice(0, 6).map((feature, idx) => (
<span key={idx} className="px-2 py-1 bg-gray-100 text-gray-700 rounded-lg text-xs whitespace-nowrap">
{feature}
</span>
))}
</div>
</div>
<div className="flex gap-2 pt-2">
<Link
href={`/property/${property.id}`}
className="flex-1 bg-white border border-gray-300 text-gray-700 py-2.5 rounded-xl font-medium hover:bg-gray-50 transition-colors text-center text-xs"
>
التفاصيل
</Link>
<button
onClick={() => {
alert('تم إرسال طلب الحجز بنجاح');
}}
className="flex-1 bg-amber-500 text-white py-2.5 rounded-xl font-medium hover:bg-amber-600 transition-colors text-xs shadow-sm"
>
حجز سريع
</button>
</div>
</div>
<div className="text-center pb-2">
<div className="w-10 h-1 bg-gray-300 rounded-full mx-auto"></div>
</div>
</motion.div>
);
};
export default function PropertyMap({ properties, userIdentity = 'syrian' }) {
const [selectedProperty, setSelectedProperty] = useState(null);
const [mapLoaded, setMapLoaded] = useState(false);
const mapRef = useRef(null);
const mapInstanceRef = useRef(null);
const markersRef = useRef([]);
const filteredProperties = properties.filter(property => {
if (!property.allowedIdentities) return true;
return property.allowedIdentities.includes(userIdentity);
});
useEffect(() => {
if (!mapRef.current || mapInstanceRef.current) return;
const map = L.map(mapRef.current).setView([34.8021, 38.9968], 7);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
maxZoom: 19,
}).addTo(map);
mapInstanceRef.current = map;
setMapLoaded(true);
return () => {
if (mapInstanceRef.current) {
mapInstanceRef.current.remove();
mapInstanceRef.current = null;
}
};
}, []);
useEffect(() => {
if (!mapInstanceRef.current || !filteredProperties.length) return;
markersRef.current.forEach(marker => marker.remove());
markersRef.current = [];
filteredProperties.forEach(property => {
if (property.location?.lat && property.location?.lng) {
const customIcon = L.divIcon({
className: 'custom-marker',
html: `
<div class="relative group cursor-pointer">
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-1">
<div class="w-px h-6 bg-gradient-to-t from-amber-400 to-transparent"></div>
</div>
<div class="w-9 h-9 bg-amber-500 rounded-full flex items-center justify-center text-white font-bold shadow-lg border-2 border-white hover:bg-amber-600 transition-colors transform group-hover:scale-110">
<span class="text-xs">$${property.priceUSD}</span>
</div>
</div>
`,
iconSize: [36, 45],
iconAnchor: [18, 45],
popupAnchor: [0, -45],
});
const marker = L.marker([property.location.lat, property.location.lng], { icon: customIcon })
.addTo(mapInstanceRef.current)
.on('click', () => {
setSelectedProperty(property);
mapInstanceRef.current.panBy([0, -100], { animate: true, duration: 0.5 });
});
markersRef.current.push(marker);
}
});
if (markersRef.current.length > 0) {
const group = L.featureGroup(markersRef.current);
mapInstanceRef.current.fitBounds(group.getBounds(), { padding: [50, 100] });
}
}, [filteredProperties, mapLoaded]);
return (
<div className="relative w-full h-[600px] rounded-xl overflow-hidden">
<div ref={mapRef} className="w-full h-full z-0" />
<AnimatePresence>
{selectedProperty && (
<PropertyPopup
property={selectedProperty}
onClose={() => setSelectedProperty(null)}
/>
)}
</AnimatePresence>
<button
onClick={() => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
mapInstanceRef.current?.setView([position.coords.latitude, position.coords.longitude], 13);
},
(error) => console.error('Error getting location:', error)
);
}
}}
className="absolute top-4 right-4 bg-white p-2.5 rounded-full shadow-md hover:bg-gray-50 transition-colors z-10 border border-gray-200"
title="الموقع الحالي"
>
<Navigation className="w-4 h-4 text-gray-700" />
</button>
<div className="absolute top-4 left-4 bg-white px-3 py-1.5 rounded-full shadow-md z-10 border border-gray-200">
<span className="font-bold text-gray-900 text-sm">{filteredProperties.length}</span>
<span className="text-gray-600 text-xs mr-1">عقار</span>
</div>
</div>
);
}

View File

@ -4,10 +4,9 @@ import { useState } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Calendar as CalendarIcon, ChevronLeft, ChevronRight } from 'lucide-react'; import { Calendar as CalendarIcon, ChevronLeft, ChevronRight } from 'lucide-react';
export default function BookingCalendar({ property, onDateSelect }) { export default function BookingCalendar({ property }) {
const [currentMonth, setCurrentMonth] = useState(new Date()); const [currentMonth, setCurrentMonth] = useState(new Date());
const [selectedRange, setSelectedRange] = useState({ start: null, end: null }); const [selectedRange, setSelectedRange] = useState({ start: null, end: null });
const [bookedDates, setBookedDates] = useState(property.bookings || []);
const daysInMonth = new Date( const daysInMonth = new Date(
currentMonth.getFullYear(), currentMonth.getFullYear(),
@ -27,8 +26,9 @@ export default function BookingCalendar({ property, onDateSelect }) {
]; ];
const isDateBooked = (date) => { const isDateBooked = (date) => {
if (!property.bookings) return false;
const dateStr = date.toISOString().split('T')[0]; const dateStr = date.toISOString().split('T')[0];
return bookedDates.some(booking => { return property.bookings.some(booking => {
const start = new Date(booking.startDate); const start = new Date(booking.startDate);
const end = new Date(booking.endDate); const end = new Date(booking.endDate);
const current = new Date(date); const current = new Date(date);
@ -36,35 +36,8 @@ export default function BookingCalendar({ property, onDateSelect }) {
}); });
}; };
const isInSelectedRange = (date) => {
if (!selectedRange.start || !selectedRange.end) return false;
const dateStr = date.toISOString().split('T')[0];
return dateStr >= selectedRange.start && dateStr <= selectedRange.end;
};
const handleDateClick = (date) => { const handleDateClick = (date) => {
if (isDateBooked(date)) return; if (isDateBooked(date)) return;
const dateStr = date.toISOString().split('T')[0];
if (!selectedRange.start || (selectedRange.start && selectedRange.end)) {
setSelectedRange({ start: dateStr, end: null });
} else {
if (dateStr > selectedRange.start) {
setSelectedRange({ ...selectedRange, end: dateStr });
onDateSelect?.({ start: selectedRange.start, end: dateStr });
} else {
setSelectedRange({ start: dateStr, end: null });
}
}
};
const changeMonth = (direction) => {
setCurrentMonth(new Date(
currentMonth.getFullYear(),
currentMonth.getMonth() + direction,
1
));
}; };
const renderDays = () => { const renderDays = () => {
@ -83,24 +56,19 @@ export default function BookingCalendar({ property, onDateSelect }) {
); );
const isBooked = isDateBooked(date); const isBooked = isDateBooked(date);
const isSelected = isInSelectedRange(date);
const isToday = date.toDateString() === new Date().toDateString();
days.push( days.push(
<motion.button <button
key={dayNumber} key={dayNumber}
whileHover={!isBooked ? { scale: 1.1 } : {}}
onClick={() => handleDateClick(date)} onClick={() => handleDateClick(date)}
disabled={isBooked} disabled={isBooked}
className={` className={`
p-2 rounded-lg text-center transition-all p-2 rounded-lg text-center text-sm transition-all
${isBooked ? 'bg-gray-200 text-gray-400 cursor-not-allowed line-through' : 'hover:bg-amber-100 cursor-pointer'} ${isBooked ? 'bg-gray-200 text-gray-400 cursor-not-allowed line-through' : 'hover:bg-amber-100 cursor-pointer'}
${isSelected ? 'bg-amber-500 text-white hover:bg-amber-600' : ''}
${isToday && !isSelected && !isBooked ? 'border-2 border-amber-500' : ''}
`} `}
> >
{dayNumber} {dayNumber}
</motion.button> </button>
); );
} }
} }
@ -108,53 +76,47 @@ export default function BookingCalendar({ property, onDateSelect }) {
}; };
return ( return (
<div className="bg-white rounded-xl p-6 shadow-lg"> <div className="bg-white rounded-xl p-4 border border-gray-200">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-4">
<button <button
onClick={() => changeMonth(-1)} onClick={() => setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1))}
className="p-2 hover:bg-gray-100 rounded-lg" className="p-1 hover:bg-gray-100 rounded"
> >
<ChevronRight className="w-5 h-5" /> <ChevronRight className="w-4 h-4" />
</button> </button>
<h3 className="text-lg font-bold flex items-center gap-2"> <h4 className="font-medium text-sm flex items-center gap-1">
<CalendarIcon className="w-5 h-5 text-amber-500" /> <CalendarIcon className="w-4 h-4 text-amber-500" />
{monthNames[currentMonth.getMonth()]} {currentMonth.getFullYear()} {monthNames[currentMonth.getMonth()]} {currentMonth.getFullYear()}
</h3> </h4>
<button <button
onClick={() => changeMonth(1)} onClick={() => setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1))}
className="p-2 hover:bg-gray-100 rounded-lg" className="p-1 hover:bg-gray-100 rounded"
> >
<ChevronLeft className="w-5 h-5" /> <ChevronLeft className="w-4 h-4" />
</button> </button>
</div> </div>
<div className="grid grid-cols-7 gap-1 mb-2 text-center text-xs font-medium text-gray-500">
<div className="grid grid-cols-7 gap-1 mb-2 text-center text-sm font-semibold text-gray-600">
<div>جمعة</div>
<div>سبت</div>
<div>أحد</div> <div>أحد</div>
<div>إثنين</div> <div>إثنين</div>
<div>ثلاثاء</div> <div>ثلاثاء</div>
<div>أربعاء</div> <div>أربعاء</div>
<div>خميس</div> <div>خميس</div>
<div>جمعة</div>
<div>سبت</div>
</div> </div>
<div className="grid grid-cols-7 gap-1"> <div className="grid grid-cols-7 gap-1">
{renderDays()} {renderDays()}
</div> </div>
<div className="flex gap-3 mt-3 pt-3 border-t text-xs">
<div className="flex gap-4 mt-6 pt-4 border-t text-sm"> <div className="flex items-center gap-1">
<div className="flex items-center gap-2"> <div className="w-3 h-3 bg-gray-200 rounded" />
<div className="w-4 h-4 bg-amber-500 rounded" /> <span className="text-gray-500">محجوز</span>
<span>محدد</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-1">
<div className="w-4 h-4 bg-gray-200 rounded line-through" /> <div className="w-3 h-3 bg-white border border-gray-300 rounded" />
<span>محجوز</span> <span className="text-gray-500">متاح</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-amber-500 rounded" />
<span>اليوم</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,166 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { MapPin, DollarSign, X, Navigation } from 'lucide-react';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
});
export default function PropertyMap({ properties, onPropertySelect }) {
const [selectedProperty, setSelectedProperty] = useState(null);
const [mapLoaded, setMapLoaded] = useState(false);
const mapRef = useRef(null);
const mapInstanceRef = useRef(null);
const markersRef = useRef([]);
useEffect(() => {
if (!mapRef.current || mapInstanceRef.current) return;
const map = L.map(mapRef.current).setView([34.8021, 38.9968], 7);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 19,
}).addTo(map);
mapInstanceRef.current = map;
setMapLoaded(true);
return () => {
if (mapInstanceRef.current) {
mapInstanceRef.current.remove();
mapInstanceRef.current = null;
}
};
}, []);
useEffect(() => {
if (!mapInstanceRef.current || !properties.length) return;
markersRef.current.forEach(marker => marker.remove());
markersRef.current = [];
const customIcon = L.divIcon({
className: 'custom-marker',
html: `<div class="w-10 h-10 bg-amber-500 rounded-full flex items-center justify-center text-white font-bold shadow-lg border-2 border-white">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
</div>`,
iconSize: [40, 40],
iconAnchor: [20, 40],
popupAnchor: [0, -40],
});
properties.forEach(property => {
if (property.location?.lat && property.location?.lng) {
const marker = L.marker([property.location.lat, property.location.lng], { icon: customIcon })
.addTo(mapInstanceRef.current)
.on('click', () => {
setSelectedProperty(property);
onPropertySelect?.(property);
});
marker.bindTooltip(property.title, {
permanent: false,
direction: 'top',
offset: [0, -40],
className: 'property-tooltip'
});
markersRef.current.push(marker);
}
});
if (markersRef.current.length > 0) {
const group = L.featureGroup(markersRef.current);
mapInstanceRef.current.fitBounds(group.getBounds(), { padding: [50, 50] });
}
}, [properties, mapLoaded, onPropertySelect]);
return (
<div className="relative w-full h-[600px] rounded-xl overflow-hidden">
<div ref={mapRef} className="w-full h-full z-0" />
<AnimatePresence>
{selectedProperty && (
<motion.div
initial={{ opacity: 0, y: 20, scale: 0.9 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 20, scale: 0.9 }}
className="absolute bottom-4 left-4 right-4 md:left-auto md:right-4 md:w-80 bg-white rounded-xl shadow-xl p-4 z-10 border border-gray-200"
>
<button
onClick={() => setSelectedProperty(null)}
className="absolute top-2 right-2 p-1 hover:bg-gray-100 rounded-full transition-colors"
>
<X className="w-4 h-4 text-gray-600" />
</button>
<h3 className="font-bold text-lg mb-2 text-gray-900">{selectedProperty.title}</h3>
<div className="flex items-center gap-1 text-gray-600 text-sm mb-3">
<MapPin className="w-4 h-4 flex-shrink-0" />
<span className="line-clamp-1">{selectedProperty.location?.address || `${selectedProperty.location.city}، ${selectedProperty.location.district}`}</span>
</div>
<div className="grid grid-cols-2 gap-2 mb-3">
<div className="bg-gray-50 p-2 rounded-lg text-center">
<div className="text-xs text-gray-500">يومي</div>
<div className="font-bold text-gray-900">
{selectedProperty.priceDisplay?.daily?.toLocaleString() || selectedProperty.price?.toLocaleString()} ل.س
</div>
</div>
<div className="bg-gray-50 p-2 rounded-lg text-center">
<div className="text-xs text-gray-500">شهري</div>
<div className="font-bold text-gray-900">
{selectedProperty.priceDisplay?.monthly?.toLocaleString() || (selectedProperty.price * 30)?.toLocaleString()} ل.س
</div>
</div>
</div>
<div className="flex gap-2">
<button
onClick={() => {
if (selectedProperty.location?.lat && selectedProperty.location?.lng) {
window.open(`https://www.openstreetmap.org/directions?from=&to=${selectedProperty.location.lat},${selectedProperty.location.lng}`, '_blank');
}
}}
className="flex-1 bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700 transition-colors flex items-center justify-center gap-2 text-sm"
>
<Navigation className="w-4 h-4" />
الاتجاهات
</button>
<button
onClick={() => window.location.href = `/property/${selectedProperty.id}`}
className="flex-1 bg-gray-800 text-white py-2 rounded-lg hover:bg-gray-900 transition-colors text-sm"
>
عرض التفاصيل
</button>
</div>
</motion.div>
)}
</AnimatePresence>
<button
onClick={() => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
mapInstanceRef.current?.setView([position.coords.latitude, position.coords.longitude], 13);
},
(error) => {
console.error('Error getting location:', error);
}
);
}
}}
className="absolute top-4 right-4 bg-white p-3 rounded-full shadow-lg hover:bg-gray-50 transition-colors z-10 border border-gray-200"
title="الموقع الحالي"
>
<Navigation className="w-5 h-5 text-gray-700" />
</button>
</div>
);
}

129
app/forgot-password/page.js Normal file
View File

@ -0,0 +1,129 @@
'use client';
import { useState } from 'react';
import { motion } from 'framer-motion';
import Link from 'next/link';
import { Mail, ArrowLeft, CheckCircle } from 'lucide-react';
export default function ForgotPasswordPage() {
const [email, setEmail] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
setIsSubmitted(true);
}, 1500);
};
return (
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 flex items-center justify-center p-4 relative overflow-hidden">
<div className="absolute inset-0 overflow-hidden">
<div className="absolute -top-40 -right-40 w-80 h-80 bg-purple-500 rounded-full opacity-20 blur-3xl animate-pulse"></div>
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-pink-500 rounded-full opacity-20 blur-3xl animate-pulse delay-1000"></div>
</div>
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5 }}
className="relative w-full max-w-md"
>
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
className="absolute -top-16 left-0"
>
<Link href="/login" className="flex items-center gap-2 text-gray-300 hover:text-white transition-colors group">
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
<span>العودة لتسجيل الدخول</span>
</Link>
</motion.div>
<div className="bg-white/10 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/20 overflow-hidden">
<div className="bg-gradient-to-r from-purple-500 to-pink-500 p-8 text-center">
<motion.h1
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
className="text-3xl font-bold text-white mb-2"
>
استعادة كلمة المرور
</motion.h1>
<motion.p
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.1 }}
className="text-purple-100"
>
سنرسل لك رابط لإعادة تعيين كلمة المرور
</motion.p>
</div>
<div className="p-8">
{!isSubmitted ? (
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-200 mb-2">
البريد الإلكتروني
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Mail className="w-5 h-5 text-gray-400 group-focus-within:text-purple-500 transition-colors" />
</div>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full pr-12 pl-4 py-3 bg-white/5 border border-gray-600 rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent text-white placeholder-gray-400 transition-all"
placeholder="أدخل بريدك الإلكتروني"
required
/>
</div>
</div>
<button
type="submit"
disabled={isLoading}
className="w-full bg-gradient-to-r from-purple-500 to-pink-500 text-white py-3 rounded-xl font-bold text-lg hover:from-purple-600 hover:to-pink-600 transition-all transform hover:scale-[1.02] active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-purple-500/25"
>
{isLoading ? (
<div className="flex items-center justify-center gap-2">
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
<span>جاري الإرسال...</span>
</div>
) : (
'إرسال رابط الاستعادة'
)}
</button>
</form>
) : (
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="text-center py-6"
>
<div className="w-20 h-20 bg-green-500/20 rounded-full flex items-center justify-center mx-auto mb-4">
<CheckCircle className="w-10 h-10 text-green-500" />
</div>
<h3 className="text-xl font-bold text-white mb-2">تم الإرسال بنجاح!</h3>
<p className="text-gray-300 mb-6">
تم إرسال رابط استعادة كلمة المرور إلى {email}
</p>
<Link
href="/login"
className="inline-block px-6 py-3 bg-white/10 text-white rounded-xl hover:bg-white/20 transition-colors"
>
العودة لتسجيل الدخول
</Link>
</motion.div>
)}
</div>
</div>
</motion.div>
</div>
);
}

View File

@ -24,3 +24,51 @@ body {
color: var(--foreground); color: var(--foreground);
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
} }
.leaflet-container {
font-family: inherit;
width: 100%;
height: 100%;
}
.leaflet-popup-content-wrapper {
border-radius: 1rem;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
}
.leaflet-popup-content {
margin: 0;
min-width: 200px;
}
.leaflet-popup-tip {
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
}
.custom-marker {
background: transparent;
border: none;
}
.custom-marker div {
transition: transform 0.2s ease;
}
.custom-marker:hover div {
transform: scale(1.1);
}
.property-tooltip {
background: white !important;
color: #1f2937 !important;
font-weight: 500 !important;
font-size: 0.875rem !important;
padding: 0.5rem 1rem !important;
border-radius: 0.75rem !important;
border: 1px solid #e5e7eb !important;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
}
.property-tooltip::before {
border-top-color: white !important;
}

View File

@ -1,15 +1,6 @@
'use client';
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
import "./i18n/config"; import ClientLayout from "./ClientLayout";
import Link from "next/link";
import Image from "next/image";
import { NavLink, MobileNavLink } from "./components/NavLinks";
import { useTranslation } from 'react-i18next';
import { Globe } from 'lucide-react';
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
const geistSans = Geist({ const geistSans = Geist({
variable: "--font-geist-sans", variable: "--font-geist-sans",
@ -21,264 +12,19 @@ const geistMono = Geist_Mono({
subsets: ["latin"], subsets: ["latin"],
}); });
export default function RootLayout({ children }) { export const metadata = {
const { t, i18n } = useTranslation(); title: "SweetHome",
const [currentLanguage, setCurrentLanguage] = useState('en'); description: "Discover premium furniture and home decor",
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); };
const currentYear = new Date().getFullYear();
const changeLanguage = (lng) => {
i18n.changeLanguage(lng);
setCurrentLanguage(lng);
localStorage.setItem('language', lng);
if (lng === 'ar') {
document.documentElement.dir = 'rtl';
document.documentElement.lang = 'ar';
document.documentElement.classList.add('rtl');
document.documentElement.classList.remove('ltr');
} else {
document.documentElement.dir = 'ltr';
document.documentElement.lang = 'en';
document.documentElement.classList.add('ltr');
document.documentElement.classList.remove('rtl');
}
};
useEffect(() => {
const savedLanguage = localStorage.getItem('language') || 'en';
const lng = savedLanguage;
setCurrentLanguage(lng);
i18n.changeLanguage(lng);
if (lng === 'ar') {
document.documentElement.dir = 'rtl';
document.documentElement.lang = 'ar';
document.documentElement.classList.add('rtl');
} else {
document.documentElement.dir = 'ltr';
document.documentElement.lang = 'en';
document.documentElement.classList.add('ltr');
}
}, [i18n]);
const toggleMobileMenu = () => {
setIsMobileMenuOpen(!isMobileMenuOpen);
};
const closeMobileMenu = () => {
setIsMobileMenuOpen(false);
};
export default function Layout({ children }) {
return ( return (
<html lang={currentLanguage}> <html lang="en">
<head> <head />
<title>SweetHome</title> <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<meta name="description" content={currentLanguage === 'ar' ? 'اكتشف أثاث وديكور منزلي فاخر' : 'Discover premium furniture and home decor'} /> <ClientLayout>
</head>
<body className={`${geistSans.variable} ${geistMono.variable} antialiased ${currentLanguage === 'ar' ? 'font-arabic' : ''}`}>
<nav className="fixed top-0 left-0 right-0 bg-white/95 backdrop-blur-sm border-b border-gray-200 z-50 transition-all duration-300 shadow-sm">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className={`flex justify-between items-center h-20 ${currentLanguage === 'ar' ? 'flex-row-reverse' : ''}`}>
<div className="flex items-center">
<Link href="/" className="flex items-center space-x-3 group">
<div className="relative w-10 h-10">
<Image
src="/logo.png"
alt={t("logoAlt")}
fill
className="object-contain group-hover:scale-105 transition-transform duration-300"
priority
sizes="40px"
/>
</div>
<span className="text-3xl font-bold text-gray-800 hidden md:block">
{t("brandNamePart1")}<span className="text-amber-600">{t("brandNamePart2")}</span>
</span>
</Link>
</div>
<div className="hidden md:flex items-center space-x-4">
<div className={`flex items-center space-x-1 ${currentLanguage === 'ar' ? 'flex-row-reverse space-x-reverse' : ''}`}>
<NavLink href="/">
{t("home")}
</NavLink>
<NavLink href="/properties">
{t("ourProducts")}
</NavLink>
<NavLink href="/admin">
{t("admin")}
</NavLink>
</div>
<motion.button
whileHover={{ scale: 1.1, rotate: 360 }}
whileTap={{ scale: 0.9 }}
onClick={() => changeLanguage(currentLanguage === 'en' ? 'ar' : 'en')}
className="flex items-center justify-center w-10 h-10 bg-gray-100 hover:bg-gray-200 rounded-full transition-all duration-200 ml-4"
aria-label={currentLanguage === 'en' ? t("switchToArabic") : t("switchToEnglish")}
title={currentLanguage === 'en' ? t("switchToArabic") : t("switchToEnglish")}
>
<Globe className="w-5 h-5 text-gray-700" />
</motion.button>
</div>
<div className="md:hidden flex items-center gap-3">
<motion.button
whileHover={{ scale: 1.1, rotate: 360 }}
whileTap={{ scale: 0.9 }}
onClick={() => changeLanguage(currentLanguage === 'en' ? 'ar' : 'en')}
className="flex items-center justify-center w-10 h-10 bg-gray-100 hover:bg-gray-200 rounded-full transition-colors"
aria-label={currentLanguage === 'en' ? t("switchToArabic") : t("switchToEnglish")}
title={currentLanguage === 'en' ? t("switchToArabic") : t("switchToEnglish")}
>
<Globe className="w-5 h-5 text-gray-700" />
</motion.button>
<button
type="button"
onClick={toggleMobileMenu}
className="inline-flex items-center justify-center p-2 rounded-md text-gray-700 hover:text-amber-600 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-amber-500 transition-colors"
aria-expanded={isMobileMenuOpen}
aria-label={t("openMainMenu")}
>
<span className="sr-only">{t("openMainMenu")}</span>
{isMobileMenuOpen ? (
<svg
className="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
) : (
<svg
className="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
)}
</button>
</div>
</div>
</div>
{isMobileMenuOpen && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="md:hidden bg-white border-t border-gray-200 shadow-lg"
>
<div className="px-2 pt-2 pb-3 space-y-1">
<MobileNavLink href="/" onClick={closeMobileMenu}>
{t("home")}
</MobileNavLink>
<MobileNavLink href="/properties" onClick={closeMobileMenu}>
{t("ourProducts")}
</MobileNavLink>
<MobileNavLink href="/admin" onClick={closeMobileMenu}>
{t("admin")}
</MobileNavLink>
</div>
</motion.div>
)}
</nav>
<main className={`pt-16 min-h-screen bg-gradient-to-b from-gray-50 to-white ${currentLanguage === 'ar' ? 'text-right' : 'text-left'}`}>
{children} {children}
</main> </ClientLayout>
<footer className="bg-gray-900 text-white py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className={`grid grid-cols-1 md:grid-cols-4 gap-8 ${currentLanguage === 'ar' ? 'text-right' : 'text-left'}`}>
<div className="space-y-4">
<div className={`flex items-center ${currentLanguage === 'ar' ? 'flex-row-reverse' : 'space-x-3'}`}>
<div className="relative w-10 h-10">
<Image
src="/logo.png"
alt={t("logoAlt")}
fill
className="object-contain"
sizes="40px"
/>
</div>
<span className="text-3xl font-bold">
{t("brandNamePart1")}<span className="text-amber-400">{t("brandNamePart2")}</span>
</span>
</div>
<p className="text-gray-400 text-sm">
{t("footerDescription")}
</p>
</div>
<div>
<h3 className="text-lg font-semibold mb-4">{t("quickLinks")}</h3>
<ul className="space-y-2">
<li>
<Link href="/" className="text-gray-400 hover:text-white transition-colors block py-1">
{t("home")}
</Link>
</li>
<li>
<Link href="/properties" className="text-gray-400 hover:text-white transition-colors block py-1">
{t("ourProducts")}
</Link>
</li>
<li>
<Link href="/admin" className="text-gray-400 hover:text-white transition-colors block py-1">
{t("admin")}
</Link>
</li>
</ul>
</div>
<div>
<h3 className="text-lg font-semibold mb-4">{t("contactUs")}</h3>
<ul className="space-y-3 text-gray-400">
<li className={`flex items-center ${currentLanguage === 'ar' ? 'flex-row-reverse' : 'space-x-2'}`}>
<svg className="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
<span>{t("phone")}</span>
</li>
<li className={`flex items-center ${currentLanguage === 'ar' ? 'flex-row-reverse' : 'space-x-2'}`}>
<svg className="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
<span>{t("footerEmail")}</span>
</li>
</ul>
</div>
<div>
<h3 className="text-lg font-semibold mb-4">{t("stayUpdated")}</h3>
<div className="space-y-3">
<input
type="email"
placeholder={t("yourEmail")}
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent"
/>
<button className="w-full bg-amber-600 hover:bg-amber-700 text-white px-4 py-2 rounded-lg transition-colors font-medium">
{t("subscribe")}
</button>
</div>
</div>
</div>
<div className="mt-8 pt-8 border-t border-gray-800 text-center text-gray-400 text-sm">
<p>&copy; {currentYear} {t("copyright")}. {t("allRightsReserved")}</p>
</div>
</div>
</footer>
</body> </body>
</html> </html>
); );

424
app/login/page.js Normal file
View File

@ -0,0 +1,424 @@
'use client';
import { useState } from 'react';
import { motion } from 'framer-motion';
import toast, { Toaster } from 'react-hot-toast';
import Link from 'next/link';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import {
Mail,
Lock,
Eye,
EyeOff,
ArrowLeft,
LogIn,
CheckCircle,
Loader2,
Home,
Shield
} from 'lucide-react';
export default function LoginPage() {
const router = useRouter();
const [showPassword, setShowPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [formData, setFormData] = useState({
email: '',
password: '',
rememberMe: false
});
const [errors, setErrors] = useState({});
const ADMIN_EMAIL = 'admin@gmail.com';
const ADMIN_PASSWORD = '123';
const validateEmail = (email) => {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
};
const validateForm = () => {
const newErrors = {};
if (!formData.email) {
newErrors.email = 'البريد الإلكتروني مطلوب';
} else if (!validateEmail(formData.email)) {
newErrors.email = 'البريد الإلكتروني غير صالح';
}
if (!formData.password) {
newErrors.password = 'كلمة المرور مطلوبة';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) {
toast.error('يرجى تصحيح الأخطاء في النموذج', {
style: { background: '#fee2e2', color: '#991b1b' }
});
return;
}
setIsLoading(true);
setTimeout(() => {
if (formData.email.toLowerCase() === ADMIN_EMAIL && formData.password === ADMIN_PASSWORD) {
setIsLoading(false);
setIsSuccess(true);
toast.success('تم تسجيل الدخول كأدمن!', {
style: { background: '#dcfce7', color: '#166534' },
duration: 3000
});
localStorage.setItem('user', JSON.stringify({
name: 'مدير النظام',
email: ADMIN_EMAIL,
role: 'admin',
avatar: 'أ'
}));
setTimeout(() => {
router.push('/admin');
}, 1500);
} else {
setIsLoading(false);
toast.error('بيانات الدخول غير صحيحة. حاول مع admin@gmail.com / 123', {
style: { background: '#fee2e2', color: '#991b1b' },
duration: 4000
});
}
}, 1500);
};
const particles = Array.from({ length: 30 }, (_, i) => ({
id: i,
x: Math.random() * 100,
y: Math.random() * 100,
size: Math.random() * 3 + 1,
duration: Math.random() * 15 + 10,
delay: Math.random() * 5
}));
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
delayChildren: 0.2
}
}
};
const itemVariants = {
hidden: { y: 20, opacity: 0 },
visible: {
y: 0,
opacity: 1,
transition: { type: 'spring', stiffness: 100 }
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center p-4 relative overflow-hidden">
<Toaster position="top-center" reverseOrder={false} />
<div className="absolute inset-0 overflow-hidden">
{particles.map((particle) => (
<motion.div
key={particle.id}
className="absolute rounded-full bg-amber-500/20"
style={{
left: `${particle.x}%`,
top: `${particle.y}%`,
width: particle.size,
height: particle.size,
}}
animate={{
y: [0, -20, 0],
x: [0, 10, -10, 0],
opacity: [0.2, 0.4, 0.2],
}}
transition={{
duration: particle.duration,
repeat: Infinity,
delay: particle.delay,
ease: "linear"
}}
/>
))}
</div>
<motion.div
className="absolute top-20 left-20 w-64 h-64 bg-amber-500/10 rounded-full blur-3xl"
animate={{
scale: [1, 1.2, 1],
x: [0, 30, 0],
y: [0, -20, 0],
}}
transition={{ duration: 12, repeat: Infinity }}
/>
<motion.div
className="absolute bottom-20 right-20 w-80 h-80 bg-blue-500/10 rounded-full blur-3xl"
animate={{
scale: [1, 1.3, 1],
x: [0, -30, 0],
y: [0, 20, 0],
}}
transition={{ duration: 15, repeat: Infinity }}
/>
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="relative w-full max-w-md z-10"
>
<motion.div
variants={itemVariants}
className="absolute -top-16 left-0"
>
<Link
href="/"
className="group flex items-center gap-2 text-gray-400 hover:text-white transition-colors"
>
<motion.div
whileHover={{ x: -5 }}
transition={{ type: 'spring', stiffness: 400 }}
>
<ArrowLeft className="w-4 h-4" />
</motion.div>
<span>العودة للرئيسية</span>
</Link>
</motion.div>
<motion.div
variants={itemVariants}
className="bg-white/10 backdrop-blur-2xl rounded-3xl shadow-2xl border border-white/10 overflow-hidden"
>
<div className="bg-gradient-to-r from-amber-500 to-amber-600 p-8 text-center relative overflow-hidden">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: "spring" }}
className="absolute -top-10 -right-10 w-40 h-40 bg-white/10 rounded-full"
/>
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.3, type: "spring" }}
className="absolute -bottom-10 -left-10 w-40 h-40 bg-white/10 rounded-full"
/>
<motion.div
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.2 }}
className="relative z-10"
>
<motion.div
animate={{ rotate: [0, 10, -10, 0] }}
transition={{ duration: 2, repeat: Infinity }}
className="w-20 h-20 mx-auto mb-4 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-sm"
>
<Home className="w-10 h-10 text-white" />
</motion.div>
<h1 className="text-3xl font-bold text-white mb-2">SweetHome</h1>
<p className="text-amber-100">مرحباً بعودتك!</p>
</motion.div>
</div>
<div className="p-8">
<motion.form
variants={itemVariants}
onSubmit={handleSubmit}
className="space-y-6"
>
<motion.div variants={itemVariants}>
<label className="block text-sm font-medium text-gray-300 mb-2">
البريد الإلكتروني
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Mail className={`w-5 h-5 transition-colors ${
errors.email ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'
}`} />
</div>
<input
type="email"
value={formData.email}
onChange={(e) => {
setFormData({...formData, email: e.target.value});
if (errors.email) setErrors({...errors, email: null});
}}
className={`w-full pr-12 pl-4 py-4 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.email ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أدخل بريدك الإلكتروني"
/>
{formData.email && validateEmail(formData.email) && (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="absolute inset-y-0 left-0 pl-3 flex items-center"
>
<CheckCircle className="w-5 h-5 text-green-500" />
</motion.div>
)}
</div>
{errors.email && (
<motion.p
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="text-red-500 text-sm mt-1"
>
{errors.email}
</motion.p>
)}
</motion.div>
<motion.div variants={itemVariants}>
<label className="block text-sm font-medium text-gray-300 mb-2">
كلمة المرور
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className={`w-5 h-5 transition-colors ${
errors.password ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'
}`} />
</div>
<input
type={showPassword ? "text" : "password"}
value={formData.password}
onChange={(e) => {
setFormData({...formData, password: e.target.value});
if (errors.password) setErrors({...errors, password: null});
}}
className={`w-full pr-12 pl-12 py-4 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.password ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أدخل كلمة المرور"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute inset-y-0 left-0 pl-3 flex items-center"
>
{showPassword ? (
<EyeOff className="w-5 h-5 text-gray-400 hover:text-gray-300 transition-colors" />
) : (
<Eye className="w-5 h-5 text-gray-400 hover:text-gray-300 transition-colors" />
)}
</button>
</div>
{errors.password && (
<motion.p
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="text-red-500 text-sm mt-1"
>
{errors.password}
</motion.p>
)}
</motion.div>
<motion.div
variants={itemVariants}
className="flex items-center justify-between"
>
<label className="flex items-center gap-2 cursor-pointer group">
<input
type="checkbox"
checked={formData.rememberMe}
onChange={(e) => setFormData({...formData, rememberMe: e.target.checked})}
className="w-4 h-4 rounded border-gray-600 bg-white/5 text-amber-500 focus:ring-amber-500 focus:ring-offset-0"
/>
<span className="text-sm text-gray-400 group-hover:text-white transition-colors">
تذكرني
</span>
</label>
<Link
href="/forgot-password"
className="text-sm text-amber-400 hover:text-amber-300 transition-colors"
>
نسيت كلمة المرور؟
</Link>
</motion.div>
<motion.button
variants={itemVariants}
type="submit"
disabled={isLoading || isSuccess}
className="relative w-full bg-gradient-to-r from-amber-500 to-amber-600 text-white py-4 rounded-xl font-bold text-lg overflow-hidden group disabled:opacity-50 disabled:cursor-not-allowed"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<motion.div
className="absolute inset-0 bg-gradient-to-r from-amber-600 to-amber-700"
initial={{ x: '100%' }}
whileHover={{ x: 0 }}
transition={{ duration: 0.3 }}
/>
<span className="relative z-10 flex items-center justify-center gap-2">
{isLoading ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
جاري تسجيل الدخول...
</>
) : isSuccess ? (
<>
<CheckCircle className="w-5 h-5" />
تم بنجاح!
</>
) : (
<>
<LogIn className="w-5 h-5" />
تسجيل الدخول
</>
)}
</span>
</motion.button>
</motion.form>
<motion.p
variants={itemVariants}
className="text-center text-gray-400 mt-6"
>
ليس لديك حساب؟{' '}
<Link
href="/auth/choose-role"
className="text-amber-400 hover:text-amber-300 font-medium transition-colors"
>
إنشاء حساب جديد
</Link>
</motion.p>
</div>
</motion.div>
<motion.p
variants={itemVariants}
className="text-center text-gray-500 text-xs mt-4"
>
بتسجيل الدخول، أنت توافق على{' '}
<Link href="/terms" className="text-amber-400 hover:text-amber-300 transition-colors">
شروط الاستخدام
</Link>
{' '}و{' '}
<Link href="/privacy" className="text-amber-400 hover:text-amber-300 transition-colors">
سياسة الخصوصية
</Link>
</motion.p>
</motion.div>
</div>
);
}

File diff suppressed because it is too large Load Diff

1022
app/owner/properties/page.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,67 +1,315 @@
'use client'; 'use client';
import { motion } from 'framer-motion'; import { useState, useRef, useEffect } from 'react';
import { useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import {
ShieldCheck,
Lock,
Zap,
Star,
Rocket,
Search,
MapPin,
Home,
DollarSign,
ChevronDown,
Shield,
Award,
Sparkles,
UserCircle,
LogOut,
Calendar,
Building,
PlusCircle,
Heart,
MessageCircle
} from 'lucide-react';
import './i18n/config'; import './i18n/config';
import HeroSearch from './components/home/HeroSearch';
import PropertyMap from './components/home/PropertyMap';
import Link from 'next/link';
import Image from 'next/image';
export default function HomePage() { export default function HomePage() {
const { t } = useTranslation(); const { t } = useTranslation();
const constraintsRef = useRef(null); 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 fadeInUp = { useEffect(() => {
hidden: { opacity: 0, y: 20 }, const storedUser = localStorage.getItem('user');
visible: { if (storedUser) {
opacity: 1, setUser(JSON.parse(storedUser));
y: 0,
transition: {
duration: 0.6,
ease: "easeOut"
} }
}, []);
useEffect(() => {
const handleClickOutside = (event) => {
if (menuRef.current && !menuRef.current.contains(event.target)) {
setShowUserMenu(false);
} }
}; };
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const staggerContainer = { const logout = () => {
hidden: { opacity: 0 }, localStorage.removeItem('user');
visible: { setUser(null);
opacity: 1, setShowUserMenu(false);
transition: {
staggerChildren: 0.2,
delayChildren: 0.3
}
}
}; };
const buttonHover = { const [allProperties] = useState([
rest: { scale: 1 }, {
hover: { id: 1,
scale: 1.05, title: 'فيلا فاخرة في المزة',
transition: { description: 'فيلا فاخرة مع حديقة خاصة ومسبح في أفضل أحياء دمشق.',
type: "spring", type: 'villa',
stiffness: 400, price: 500000,
damping: 10 priceUSD: 50,
} priceUnit: 'daily',
location: {
city: 'دمشق',
district: 'المزة',
address: 'شارع المزة - فيلات غربية',
lat: 33.5138,
lng: 36.2765
}, },
tap: { scale: 0.95 } bedrooms: 5,
}; bathrooms: 4,
area: 450,
features: ['مسبح', 'حديقة خاصة', 'موقف سيارات', 'أمن 24/7', 'تدفئة مركزية', 'تكييف مركزي'],
images: ['/villa1.jpg', '/villa2.jpg', '/villa3.jpg'],
status: 'available',
rating: 4.8,
isNew: true,
allowedIdentities: ['syrian', 'passport'],
priceDisplay: {
daily: 500000,
monthly: 15000000
},
bookings: [
{ startDate: '2024-03-10', endDate: '2024-03-15' },
{ startDate: '2024-03-20', endDate: '2024-03-25' }
]
},
{
id: 2,
title: 'شقة حديثة في الشهباء',
description: 'شقة عصرية في حي الشهباء الراقي بحلب.',
type: 'apartment',
price: 250000,
priceUSD: 25,
priceUnit: 'daily',
location: {
city: 'حلب',
district: 'الشهباء',
address: 'شارع النيل - بناء الرحاب',
lat: 36.2021,
lng: 37.1347
},
bedrooms: 3,
bathrooms: 2,
area: 180,
features: ['مطبخ مجهز', 'بلكونة', 'موقف سيارات', 'مصعد'],
images: ['/apartment1.jpg', '/apartment2.jpg'],
status: 'available',
rating: 4.5,
isNew: false,
allowedIdentities: ['syrian'],
priceDisplay: {
daily: 250000,
monthly: 7500000
},
bookings: [
{ startDate: '2024-03-05', endDate: '2024-03-08' }
]
},
{
id: 3,
title: 'بيت عائلي في بابا عمرو',
description: 'بيت واسع مناسب للعائلات في حمص.',
type: 'house',
price: 350000,
priceUSD: 35,
priceUnit: 'daily',
location: {
city: 'حمص',
district: 'بابا عمرو',
address: 'حي الزهور',
lat: 34.7265,
lng: 36.7186
},
bedrooms: 4,
bathrooms: 3,
area: 300,
features: ['حديقة كبيرة', 'موقف سيارات', 'مدفأة', 'كراج'],
images: ['/house1.jpg'],
status: 'booked',
rating: 4.3,
isNew: false,
allowedIdentities: ['syrian', 'passport'],
priceDisplay: {
daily: 350000,
monthly: 10500000
},
bookings: []
},
{
id: 4,
title: 'شقة بجانب البحر',
description: 'شقة رائعة مع إطلالة بحرية في اللاذقية.',
type: 'apartment',
price: 300000,
priceUSD: 30,
priceUnit: 'daily',
location: {
city: 'اللاذقية',
district: 'الشاطئ الأزرق',
address: 'الكورنيش الغربي',
lat: 35.5306,
lng: 35.7801
},
bedrooms: 3,
bathrooms: 2,
area: 200,
features: ['إطلالة بحرية', 'شرفة', 'تكييف', 'أمن'],
images: ['/seaside1.jpg', '/seaside2.jpg', '/seaside3.jpg'],
status: 'available',
rating: 4.9,
isNew: true,
allowedIdentities: ['passport'],
priceDisplay: {
daily: 300000,
monthly: 9000000
},
bookings: []
},
{
id: 5,
title: 'فيلا في درعا',
description: 'فيلا فاخرة في حي الأطباء بدرعا.',
type: 'villa',
price: 400000,
priceUSD: 40,
priceUnit: 'daily',
location: {
city: 'درعا',
district: 'حي الأطباء',
address: 'شارع الشفاء',
lat: 32.6237,
lng: 36.1016
},
bedrooms: 4,
bathrooms: 3,
area: 350,
features: ['حديقة مثمرة', 'أنظمة أمن', 'مسبح', 'كراج'],
images: ['/villa4.jpg', '/villa5.jpg'],
status: 'available',
rating: 4.6,
isNew: false,
allowedIdentities: ['syrian', 'passport'],
priceDisplay: {
daily: 400000,
monthly: 12000000
},
bookings: []
}
]);
const floatingAnimation = { const applyFilters = (filters) => {
y: [0, -10, 0], setSearchFilters(filters);
transition: {
duration: 2, const filtered = allProperties.filter(property => {
repeat: Infinity, if (filters.city && filters.city !== 'all' && property.location.city !== filters.city) {
ease: "easeInOut" return false;
}
if (filters.propertyType && filters.propertyType !== 'all' && property.type !== filters.propertyType) {
return false;
}
if (filters.priceRange && filters.priceRange !== 'all') {
const priceUSD = property.priceUSD;
switch(filters.priceRange) {
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;
}
}
if (filters.identityType && property.allowedIdentities) {
if (!property.allowedIdentities.includes(filters.identityType)) {
return false;
}
}
return true;
});
setFilteredProperties(filtered);
if (!showMap) {
setShowMap(true);
setTimeout(() => {
if (mapSectionRef.current) {
setIsScrolling(true);
mapSectionRef.current.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
setTimeout(() => setIsScrolling(false), 1000);
}
}, 300);
} else {
if (mapSectionRef.current) {
setIsScrolling(true);
mapSectionRef.current.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
setTimeout(() => setIsScrolling(false), 1000);
}
} }
}; };
const resetSearch = () => {
setShowMap(false);
setSearchFilters(null);
setFilteredProperties([]);
window.scrollTo({
top: 0,
behavior: 'smooth'
});
};
const getUserInitial = () => {
if (user?.name) {
return user.name.charAt(0).toUpperCase();
}
return null;
};
const isOwner = user?.role === 'owner';
return ( return (
<div className="min-h-screen" ref={constraintsRef}> <div className="min-h-screen">
<section className="relative min-h-screen flex items-center justify-center overflow-hidden"> <section className="relative min-h-screen flex items-center justify-center overflow-hidden">
<div className="absolute inset-0 z-0"> <div className="absolute inset-0 z-0">
<motion.div <motion.div
className="absolute inset-0 bg-cover bg-center bg-no-repeat" className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{ style={{
backgroundImage: 'url(/Home.jpg)', backgroundImage: 'url(/hero.jpg)',
}} }}
initial={{ scale: 1.1 }} initial={{ scale: 1.1 }}
animate={{ scale: 1 }} animate={{ scale: 1 }}
@ -69,172 +317,83 @@ export default function HomePage() {
/> />
<div className="absolute inset-0 bg-gradient-to-r from-black/70 via-black/60 to-black/50" /> <div className="absolute inset-0 bg-gradient-to-r from-black/70 via-black/60 to-black/50" />
</div> </div>
<div className="relative z-10 container mx-auto px-4 sm:px-6 lg:px-8"> <div className="relative z-10 container mx-auto px-4 sm:px-6 lg:px-8">
<div className="max-w-6xl mx-auto"> <div className="max-w-6xl mx-auto">
<motion.div <motion.div
className="text-center mb-12" className="text-center mb-12"
initial="hidden" initial="hidden"
animate="visible" animate="visible"
variants={staggerContainer} variants={{
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.2 }
}
}}
> >
<motion.h1 <motion.h1
className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-6 leading-tight tracking-tight" className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-6 leading-tight tracking-tight"
variants={fadeInUp} variants={{
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 }
}}
> >
{t("heroTitleLine1")}<br /> {t("heroTitleLine1")}<br />
<motion.span <motion.span
className="text-amber-400" className="text-amber-400"
animate={floatingAnimation} animate={{
y: [0, -10, 0],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut"
}}
> >
{t("heroTitleLine2")} {t("heroTitleLine2")}
</motion.span> </motion.span>
</motion.h1> </motion.h1>
<motion.p <motion.p
className="text-base sm:text-lg text-gray-200 max-w-2xl mx-auto leading-relaxed" className="text-base sm:text-lg text-gray-200 max-w-2xl mx-auto leading-relaxed"
variants={fadeInUp} variants={{
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 }
}}
> >
{t("heroSubtitle")} {t("heroSubtitle")}
</motion.p> </motion.p>
</motion.div> </motion.div>
{!isOwner && <HeroSearch onSearch={applyFilters} />}
{isOwner && (
<motion.div <motion.div
className="bg-white/10 backdrop-blur-lg rounded-2xl p-6 sm:p-8 border border-white/20 shadow-2xl" initial={{ opacity: 0, y: 20 }}
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.5 }} transition={{ delay: 0.5 }}
whileHover={{ className="bg-white/10 backdrop-blur-lg rounded-2xl p-8 text-center border border-white/20"
boxShadow: "0 20px 40px rgba(0,0,0,0.3)",
transition: { duration: 0.3 }
}}
> >
<div className="flex flex-wrap gap-2 mb-8"> <h2 className="text-2xl font-bold text-white mb-2">
<motion.button مرحباً {user?.name}!
className="px-4 py-2 bg-amber-500 text-white font-medium rounded-lg text-sm" </h2>
variants={buttonHover} <p className="text-gray-200 mb-4">
initial="rest" يمكنك إدارة عقاراتك من خلال لوحة التحكم الخاصة بك
whileHover="hover" </p>
whileTap="tap" {/* <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"
> >
{t("rentTab")} <Building className="w-5 h-5" />
</motion.button> إدارة عقاراتي
<motion.button </Link> */}
className="px-4 py-2 bg-white/20 text-white font-medium rounded-lg hover:bg-white/30 transition-colors text-sm"
variants={buttonHover}
initial="rest"
whileHover="hover"
whileTap="tap"
>
{t("buyTab")}
</motion.button>
<motion.button
className="px-4 py-2 bg-white/20 text-white font-medium rounded-lg hover:bg-white/30 transition-colors text-sm"
variants={buttonHover}
initial="rest"
whileHover="hover"
whileTap="tap"
>
{t("sellTab")}
</motion.button>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium text-white mb-2">
{t("cityStreetLabel")}
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<input
type="text"
className="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-lg bg-white/90 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-sm"
placeholder={t("cityStreetPlaceholder")}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-white mb-2">
{t("rentTypeLabel")}
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</div>
<select
className="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-lg bg-white/90 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent appearance-none text-sm"
>
<option value="">{t("selectType")}</option>
<option value="apartment">{t("apartment")}</option>
<option value="house">{t("house")}</option>
<option value="villa">{t("villa")}</option>
<option value="studio">{t("studio")}</option>
{/* <option value="penthouse">{t("penthouse")}</option> */}
</select>
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
</div>
<div>
<label className="block text-sm font-medium text-white mb-2">
{t("priceLabel")}
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<select
className="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-lg bg-white/90 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent appearance-none text-sm"
>
<option value="">{t("selectPriceRange")}</option>
<option value="0-500">{t("priceRange1")}</option>
<option value="500-1000">{t("priceRange2")}</option>
<option value="1000-2000">{t("priceRange3")}</option>
<option value="2000-3000">{t("priceRange4")}</option>
<option value="3000+">{t("priceRange5")}</option>
</select>
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
</div>
<div className="flex items-end">
<motion.button
className="w-full bg-amber-500 hover:bg-amber-600 text-white font-bold py-3 px-6 rounded-lg transition-all duration-300 flex items-center justify-center text-sm"
variants={buttonHover}
initial="rest"
whileHover="hover"
whileTap="tap"
>
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
{t("searchButton")}
</motion.button>
</div>
</div>
</motion.div> </motion.div>
)}
</div> </div>
</div> </div>
{!showMap && !isOwner && (
<motion.div <motion.div
className="absolute bottom-8 left-1/2 transform -translate-x-1/2" className="absolute bottom-8 left-1/2 transform -translate-x-1/2 cursor-pointer"
animate={{ animate={{
y: [0, 10, 0], y: [0, 10, 0],
}} }}
@ -243,139 +402,228 @@ export default function HomePage() {
repeat: Infinity, repeat: Infinity,
ease: "easeInOut" ease: "easeInOut"
}} }}
onClick={() => window.scrollTo({
top: window.innerHeight,
behavior: 'smooth'
})}
> >
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
</svg> </svg>
</motion.div> </motion.div>
)}
</section> </section>
<section className="py-20 bg-gray-50"> {!isOwner && (
<div className="container mx-auto px-4 sm:px-6 lg:px-8"> <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 <motion.div
className="text-center mb-16" 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">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="text-center mb-8"
>
<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>
</div>
{filteredProperties.length > 0 ? (
<p className="text-gray-600">
تم العثور على {filteredProperties.length} عقار يطابق معايير البحث
</p>
) : (
<p className="text-gray-600">
لا توجد عقارات تطابق معايير البحث. جرب تغيير الفلاتر.
</p>
)}
</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 }} initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.5 }} viewport={{ once: true }}
transition={{ duration: 0.6 }} transition={{ duration: 0.6 }}
> >
<h2 className="text-2xl md:text-3xl font-bold text-gray-900 mb-4 tracking-tight"> <div className="inline-block px-4 py-1 bg-amber-100 text-amber-700 rounded-full text-sm font-medium mb-4">
لماذا نحن؟
</div>
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4 tracking-tight">
{t("whyChooseUsTitle")} {t("whyChooseUsTitle")}
</h2> </h2>
<p className="text-gray-600 max-w-2xl mx-auto text-base"> <p className="text-gray-600 max-w-2xl mx-auto text-lg">
{t("whyChooseUsSubtitle")} {t("whyChooseUsSubtitle")}
</p> </p>
</motion.div> </motion.div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<motion.div <motion.div
className="grid grid-cols-1 md:grid-cols-3 gap-8" className="group bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100"
variants={staggerContainer} initial={{ opacity: 0, y: 20 }}
initial="hidden" whileInView={{ opacity: 1, y: 0 }}
whileInView="visible" viewport={{ once: true }}
viewport={{ once: true, amount: 0.2 }} transition={{ duration: 0.5, delay: 0.1 }}
whileHover={{ y: -4 }}
> >
<motion.div <div className="flex items-center gap-4 mb-4">
className="bg-white p-6 rounded-xl shadow-lg hover:shadow-xl transition-shadow duration-300 cursor-pointer overflow-hidden relative group" <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">
variants={fadeInUp} <ShieldCheck className="w-6 h-6 text-amber-600" />
initial="rest"
whileHover={{ y: -5 }}
animate="rest"
>
<motion.div
className="absolute inset-0 bg-gradient-to-br from-amber-500/5 to-amber-600/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"
initial={{ scale: 0.5, opacity: 0 }}
whileHover={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.4 }}
/>
<div
className="w-12 h-12 bg-amber-100 rounded-lg flex items-center justify-center mb-4 relative z-10"
>
<svg className="w-6 h-6 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div> </div>
<h3 className="text-lg font-bold text-gray-900">
<h3
className="text-lg font-bold text-gray-900 mb-3 relative z-10"
>
{t("feature1Title")} {t("feature1Title")}
</h3> </h3>
<p </div>
className="text-gray-600 relative z-10 text-sm leading-relaxed"
> <p className="text-gray-600 text-sm leading-relaxed">
{t("feature1Description")} {t("feature1Description")}
</p> </p>
</motion.div> </motion.div>
<motion.div <motion.div
className="bg-white p-6 rounded-xl shadow-lg hover:shadow-xl transition-shadow duration-300 cursor-pointer overflow-hidden relative group" className="group bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100"
variants={fadeInUp} initial={{ opacity: 0, y: 20 }}
initial="rest" whileInView={{ opacity: 1, y: 0 }}
whileHover={{ y: -5 }} viewport={{ once: true }}
animate="rest" transition={{ duration: 0.5, delay: 0.2 }}
whileHover={{ y: -4 }}
> >
<motion.div <div className="flex items-center gap-4 mb-4">
className="absolute inset-0 bg-gradient-to-br from-amber-500/5 to-amber-600/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300" <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">
initial={{ scale: 0.5, opacity: 0 }} <Lock className="w-6 h-6 text-blue-600" />
whileHover={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.4, delay: 0.1 }}
/>
<div
className="w-12 h-12 bg-amber-100 rounded-lg flex items-center justify-center mb-4 relative z-10"
>
<svg className="w-6 h-6 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div> </div>
<h3 className="text-lg font-bold text-gray-900">
<h3
className="text-lg font-bold text-gray-900 mb-3 relative z-10"
>
{t("feature2Title")} {t("feature2Title")}
</h3> </h3>
<p </div>
className="text-gray-600 relative z-10 text-sm leading-relaxed"
> <p className="text-gray-600 text-sm leading-relaxed">
{t("feature2Description")} {t("feature2Description")}
</p> </p>
</motion.div> </motion.div>
<motion.div <motion.div
className="bg-white p-6 rounded-xl shadow-lg hover:shadow-xl transition-shadow duration-300 cursor-pointer overflow-hidden relative group" className="group bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100"
variants={fadeInUp} initial={{ opacity: 0, y: 20 }}
initial="rest" whileInView={{ opacity: 1, y: 0 }}
whileHover={{ y: -5 }} viewport={{ once: true }}
animate="rest" transition={{ duration: 0.5, delay: 0.3 }}
whileHover={{ y: -4 }}
> >
<motion.div <div className="flex items-center gap-4 mb-4">
className="absolute inset-0 bg-gradient-to-br from-amber-500/5 to-amber-600/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300" <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">
initial={{ scale: 0.5, opacity: 0 }} <Zap className="w-6 h-6 text-green-600" />
whileHover={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.4, delay: 0.2 }}
/>
<div
className="w-12 h-12 bg-amber-100 rounded-lg flex items-center justify-center mb-4 relative z-10"
>
<svg className="w-6 h-6 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div> </div>
<h3 className="text-lg font-bold text-gray-900">
<h3
className="text-lg font-bold text-gray-900 mb-3 relative z-10"
>
{t("feature3Title")} {t("feature3Title")}
</h3> </h3>
<p </div>
className="text-gray-600 relative z-10 text-sm leading-relaxed"
> <p className="text-gray-600 text-sm leading-relaxed">
{t("feature3Description")} {t("feature3Description")}
</p> </p>
</motion.div> </motion.div>
</motion.div> </div>
</div> </div>
</section> </section>
</div> </div>

641
app/profile/page.js Normal file
View File

@ -0,0 +1,641 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import Image from 'next/image';
import {
User,
Mail,
Phone,
MessageCircle,
Camera,
Save,
X,
CheckCircle,
AlertCircle,
ArrowLeft,
Building,
Home,
Calendar,
MapPin,
Edit,
Loader2,
Upload,
Check,
Pencil
} from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
export default function ProfilePage() {
const router = useRouter();
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const [editingField, setEditingField] = useState(null);
const [formData, setFormData] = useState({
name: '',
email: '',
phone: '',
whatsapp: '',
bio: '',
location: '',
joinedDate: ''
});
const [tempValues, setTempValues] = useState({});
const [avatar, setAvatar] = useState(null);
const [avatarPreview, setAvatarPreview] = useState('');
const [errors, setErrors] = useState({});
const fileInputRef = useRef(null);
const inputRefs = {
name: useRef(null),
email: useRef(null),
phone: useRef(null),
whatsapp: useRef(null),
location: useRef(null),
bio: useRef(null)
};
useEffect(() => {
const storedUser = localStorage.getItem('user');
if (storedUser) {
const userData = JSON.parse(storedUser);
setUser(userData);
const savedProfile = localStorage.getItem('userProfile');
let profileData;
if (savedProfile) {
profileData = JSON.parse(savedProfile);
} else {
profileData = {
name: userData.name || '',
email: userData.email || '',
phone: '',
whatsapp: '',
bio: '',
location: '',
joinedDate: new Date().toLocaleDateString('ar-SA', { month: 'long', year: 'numeric' })
};
}
setFormData(profileData);
setTempValues(profileData);
const savedAvatar = localStorage.getItem('userAvatar');
if (savedAvatar) {
setAvatarPreview(savedAvatar);
}
setIsLoading(false);
} else {
router.push('/login');
}
}, [router]);
useEffect(() => {
if (editingField && inputRefs[editingField]?.current) {
inputRefs[editingField].current.focus();
}
}, [editingField]);
const handleImageUpload = (file) => {
if (!file) return;
if (!file.type.startsWith('image/')) {
toast.error('الرجاء اختيار صورة صالحة');
return;
}
if (file.size > 2 * 1024 * 1024) {
toast.error('حجم الصورة يجب أن يكون أقل من 2 ميجابايت');
return;
}
const reader = new FileReader();
reader.onloadend = () => {
setAvatarPreview(reader.result);
localStorage.setItem('userAvatar', reader.result);
toast.success('تم تغيير الصورة بنجاح');
};
reader.readAsDataURL(file);
};
const validateField = (field, value) => {
if (field === 'email' && value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
return 'البريد الإلكتروني غير صالح';
}
if ((field === 'phone' || field === 'whatsapp') && value && !/^(09|05)[0-9]{8}$/.test(value)) {
return 'رقم الهاتف غير صالح (يجب أن يبدأ 09 أو 05)';
}
if (field === 'name' && !value?.trim()) {
return 'الاسم مطلوب';
}
return null;
};
const startEditing = (field) => {
setEditingField(field);
setTempValues(prev => ({ ...prev, [field]: formData[field] }));
setErrors(prev => ({ ...prev, [field]: null }));
};
const cancelEditing = () => {
setEditingField(null);
setTempValues({});
setErrors({});
};
const saveField = (field) => {
const value = tempValues[field];
const error = validateField(field, value);
if (error) {
setErrors(prev => ({ ...prev, [field]: error }));
return;
}
const updatedData = { ...formData, [field]: value };
setFormData(updatedData);
localStorage.setItem('userProfile', JSON.stringify(updatedData));
if (field === 'name') {
const updatedUser = { ...user, name: value };
localStorage.setItem('user', JSON.stringify(updatedUser));
setUser(updatedUser);
}
setEditingField(null);
setTempValues({});
toast.success(`تم تحديث ${getFieldLabel(field)} بنجاح`);
};
const handleKeyDown = (e, field) => {
if (e.key === 'Enter') {
saveField(field);
} else if (e.key === 'Escape') {
cancelEditing();
}
};
const getFieldLabel = (field) => {
const labels = {
name: 'الاسم',
email: 'البريد الإلكتروني',
phone: 'رقم الهاتف',
whatsapp: 'رقم الواتساب',
location: 'الموقع',
bio: 'نبذة عني'
};
return labels[field] || field;
};
const getFieldIcon = (field) => {
const icons = {
name: User,
email: Mail,
phone: Phone,
whatsapp: MessageCircle,
location: MapPin,
bio: User
};
const Icon = icons[field];
return Icon ? <Icon className="w-5 h-5 text-gray-400" /> : null;
};
const fadeInUp = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.5 }
};
if (isLoading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<Loader2 className="w-12 h-12 text-amber-500 animate-spin mx-auto mb-4" />
<p className="text-gray-600">جاري تحميل الملف الشخصي...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 py-8">
<Toaster position="top-center" reverseOrder={false} />
<div className="container mx-auto px-4 max-w-4xl">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="flex justify-between items-center mb-6"
>
<Link
href="/"
className="flex items-center gap-2 text-gray-600 hover:text-amber-600 transition-colors"
>
<ArrowLeft className="w-5 h-5" />
<span>العودة للرئيسية</span>
</Link>
</motion.div>
<motion.div
variants={fadeInUp}
initial="initial"
animate="animate"
className="bg-white rounded-3xl shadow-xl overflow-hidden"
>
<div className="h-48 bg-gradient-to-r from-amber-500 to-amber-600 relative overflow-hidden">
<motion.div
className="absolute inset-0 bg-white/10"
animate={{
x: ['-100%', '100%'],
}}
transition={{
duration: 3,
repeat: Infinity,
ease: 'linear',
}}
/>
</div>
<div className="px-8 pb-8">
<div className="flex justify-center -mt-16 mb-6">
<div className="relative group">
<div className="w-32 h-32 rounded-full border-4 border-white bg-gradient-to-br from-amber-500 to-amber-600 flex items-center justify-center text-white text-4xl font-bold shadow-xl overflow-hidden cursor-pointer"
onClick={() => fileInputRef.current?.click()}>
{avatarPreview ? (
<img
src={avatarPreview}
alt={formData.name}
className="w-full h-full object-cover"
/>
) : (
formData.name?.charAt(0).toUpperCase() || 'U'
)}
</div>
<button
onClick={() => fileInputRef.current?.click()}
className="absolute bottom-0 right-0 w-8 h-8 bg-amber-500 rounded-full flex items-center justify-center text-white hover:bg-amber-600 transition-colors shadow-lg"
title="تغيير الصورة"
>
<Camera className="w-4 h-4" />
</button>
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={(e) => handleImageUpload(e.target.files?.[0])}
className="hidden"
/>
</div>
</div>
<div className="text-center mb-6">
<div className="flex items-center justify-center gap-2">
{editingField === 'name' ? (
<div className="flex items-center gap-2">
<input
ref={inputRefs.name}
type="text"
value={tempValues.name || ''}
onChange={(e) => setTempValues({...tempValues, name: e.target.value})}
onKeyDown={(e) => handleKeyDown(e, 'name')}
className="text-3xl font-bold text-center border-b-2 border-amber-500 outline-none px-2 py-1 w-64"
placeholder="الاسم الكامل"
/>
<button
onClick={() => saveField('name')}
className="p-2 bg-green-500 text-white rounded-full hover:bg-green-600 transition-colors"
title="حفظ"
>
<Check className="w-4 h-4" />
</button>
<button
onClick={cancelEditing}
className="p-2 bg-red-500 text-white rounded-full hover:bg-red-600 transition-colors"
title="إلغاء"
>
<X className="w-4 h-4" />
</button>
</div>
) : (
<>
<h1 className="text-3xl font-bold text-gray-900">{formData.name}</h1>
<button
onClick={() => startEditing('name')}
className="p-2 text-gray-400 hover:text-amber-500 transition-colors"
title="تعديل الاسم"
>
<Pencil className="w-4 h-4" />
</button>
</>
)}
</div>
{errors.name && (
<p className="text-red-500 text-sm mt-1">{errors.name}</p>
)}
<div className="flex items-center justify-center gap-2 text-gray-500 mt-2">
<MapPin className="w-4 h-4" />
{editingField === 'location' ? (
<div className="flex items-center gap-2">
<input
ref={inputRefs.location}
type="text"
value={tempValues.location || ''}
onChange={(e) => setTempValues({...tempValues, location: e.target.value})}
onKeyDown={(e) => handleKeyDown(e, 'location')}
className="border-b border-amber-500 outline-none px-2 py-1"
placeholder="الموقع"
/>
<button
onClick={() => saveField('location')}
className="text-green-500 hover:text-green-600"
>
<Check className="w-4 h-4" />
</button>
<button
onClick={cancelEditing}
className="text-red-500 hover:text-red-600"
>
<X className="w-4 h-4" />
</button>
</div>
) : (
<>
<span>{formData.location || 'الموقع غير محدد'}</span>
<button
onClick={() => startEditing('location')}
className="text-gray-400 hover:text-amber-500 transition-colors"
>
<Pencil className="w-3 h-3" />
</button>
</>
)}
</div>
<div className="flex items-center justify-center gap-2 text-gray-500 mt-1">
<Calendar className="w-4 h-4" />
<span>عضو منذ {formData.joinedDate}</span>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-gray-50 p-4 rounded-xl group">
<div className="flex justify-between items-start mb-2">
<label className="text-sm font-medium text-gray-600">
البريد الإلكتروني
</label>
{editingField !== 'email' && (
<button
onClick={() => startEditing('email')}
className="opacity-0 group-hover:opacity-100 transition-opacity text-gray-400 hover:text-amber-500"
>
<Pencil className="w-4 h-4" />
</button>
)}
</div>
{editingField === 'email' ? (
<div className="space-y-2">
<div className="flex gap-2">
<input
ref={inputRefs.email}
type="email"
value={tempValues.email || ''}
onChange={(e) => setTempValues({...tempValues, email: e.target.value})}
onKeyDown={(e) => handleKeyDown(e, 'email')}
className="flex-1 px-3 py-2 border rounded-lg focus:ring-2 focus:ring-amber-500 focus:border-transparent"
placeholder="example@domain.com"
/>
<button
onClick={() => saveField('email')}
className="p-2 bg-green-500 text-white rounded-lg hover:bg-green-600"
title="حفظ"
>
<Check className="w-4 h-4" />
</button>
<button
onClick={cancelEditing}
className="p-2 bg-red-500 text-white rounded-lg hover:bg-red-600"
title="إلغاء"
>
<X className="w-4 h-4" />
</button>
</div>
{errors.email && (
<p className="text-red-500 text-xs">{errors.email}</p>
)}
</div>
) : (
<div className="flex items-center gap-2 text-gray-900">
<Mail className="w-5 h-5 text-gray-400" />
<span>{formData.email}</span>
</div>
)}
</div>
<div className="bg-gray-50 p-4 rounded-xl group">
<div className="flex justify-between items-start mb-2">
<label className="text-sm font-medium text-gray-600">
رقم الهاتف
</label>
{editingField !== 'phone' && (
<button
onClick={() => startEditing('phone')}
className="opacity-0 group-hover:opacity-100 transition-opacity text-gray-400 hover:text-amber-500"
>
<Pencil className="w-4 h-4" />
</button>
)}
</div>
{editingField === 'phone' ? (
<div className="space-y-2">
<div className="flex gap-2">
<input
ref={inputRefs.phone}
type="tel"
value={tempValues.phone || ''}
onChange={(e) => setTempValues({...tempValues, phone: e.target.value})}
onKeyDown={(e) => handleKeyDown(e, 'phone')}
className="flex-1 px-3 py-2 border rounded-lg focus:ring-2 focus:ring-amber-500 focus:border-transparent"
placeholder="09XXXXXXXX"
/>
<button
onClick={() => saveField('phone')}
className="p-2 bg-green-500 text-white rounded-lg hover:bg-green-600"
>
<Check className="w-4 h-4" />
</button>
<button
onClick={cancelEditing}
className="p-2 bg-red-500 text-white rounded-lg hover:bg-red-600"
>
<X className="w-4 h-4" />
</button>
</div>
{errors.phone && (
<p className="text-red-500 text-xs">{errors.phone}</p>
)}
</div>
) : (
<div className="flex items-center gap-2 text-gray-900">
<Phone className="w-5 h-5 text-gray-400" />
<span>{formData.phone || 'غير محدد'}</span>
</div>
)}
</div>
<div className="bg-gray-50 p-4 rounded-xl group">
<div className="flex justify-between items-start mb-2">
<label className="text-sm font-medium text-gray-600">
رقم الواتساب
</label>
{editingField !== 'whatsapp' && (
<button
onClick={() => startEditing('whatsapp')}
className="opacity-0 group-hover:opacity-100 transition-opacity text-gray-400 hover:text-amber-500"
>
<Pencil className="w-4 h-4" />
</button>
)}
</div>
{editingField === 'whatsapp' ? (
<div className="space-y-2">
<div className="flex gap-2">
<input
ref={inputRefs.whatsapp}
type="tel"
value={tempValues.whatsapp || ''}
onChange={(e) => setTempValues({...tempValues, whatsapp: e.target.value})}
onKeyDown={(e) => handleKeyDown(e, 'whatsapp')}
className="flex-1 px-3 py-2 border rounded-lg focus:ring-2 focus:ring-amber-500 focus:border-transparent"
placeholder="09XXXXXXXX"
/>
<button
onClick={() => saveField('whatsapp')}
className="p-2 bg-green-500 text-white rounded-lg hover:bg-green-600"
>
<Check className="w-4 h-4" />
</button>
<button
onClick={cancelEditing}
className="p-2 bg-red-500 text-white rounded-lg hover:bg-red-600"
>
<X className="w-4 h-4" />
</button>
</div>
{errors.whatsapp && (
<p className="text-red-500 text-xs">{errors.whatsapp}</p>
)}
</div>
) : (
<div className="flex items-center gap-2 text-gray-900">
<MessageCircle className="w-5 h-5 text-gray-400" />
<span>{formData.whatsapp || 'غير محدد'}</span>
</div>
)}
</div>
<div className="bg-gray-50 p-4 rounded-xl">
<label className="block text-sm font-medium text-gray-600 mb-2">
نوع الحساب
</label>
<div className="flex items-center gap-2">
{user?.role === 'owner' ? (
<>
<Building className="w-5 h-5 text-amber-500" />
<span className="text-gray-900">مالك عقار</span>
</>
) : (
<>
<Home className="w-5 h-5 text-blue-500" />
<span className="text-gray-900">مستأجر</span>
</>
)}
</div>
</div>
</div>
<div className="mt-6 bg-gray-50 p-4 rounded-xl group">
<div className="flex justify-between items-start mb-2">
<label className="text-sm font-medium text-gray-600">
نبذة عني
</label>
{editingField !== 'bio' && (
<button
onClick={() => startEditing('bio')}
className="opacity-0 group-hover:opacity-100 transition-opacity text-gray-400 hover:text-amber-500"
>
<Pencil className="w-4 h-4" />
</button>
)}
</div>
{editingField === 'bio' ? (
<div className="space-y-2">
<textarea
ref={inputRefs.bio}
value={tempValues.bio || ''}
onChange={(e) => setTempValues({...tempValues, bio: e.target.value})}
rows="4"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-amber-500 focus:border-transparent"
placeholder="اكتب نبذة عن نفسك..."
/>
<div className="flex justify-end gap-2">
<button
onClick={() => saveField('bio')}
className="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 flex items-center gap-2"
>
<Check className="w-4 h-4" />
حفظ
</button>
<button
onClick={cancelEditing}
className="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 flex items-center gap-2"
>
<X className="w-4 h-4" />
إلغاء
</button>
</div>
</div>
) : (
<p className="text-gray-700 leading-relaxed">
{formData.bio || 'لا توجد نبذة تعريفية بعد'}
</p>
)}
</div>
{user?.role === 'owner' && (
<div className="grid grid-cols-3 gap-4 mt-6">
<div className="bg-amber-50 p-4 rounded-xl text-center">
<div className="text-2xl font-bold text-amber-600">12</div>
<div className="text-xs text-gray-600">عقارات</div>
</div>
<div className="bg-blue-50 p-4 rounded-xl text-center">
<div className="text-2xl font-bold text-blue-600">8</div>
<div className="text-xs text-gray-600">حجوزات نشطة</div>
</div>
<div className="bg-green-50 p-4 rounded-xl text-center">
<div className="text-2xl font-bold text-green-600">4.8</div>
<div className="text-xs text-gray-600">تقييم</div>
</div>
</div>
)}
</div>
</motion.div>
</div>
</div>
);
}

View File

@ -33,6 +33,7 @@ import {
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
const PropertyCard = ({ property, viewMode = 'grid' }) => { const PropertyCard = ({ property, viewMode = 'grid' }) => {
const [isFavorite, setIsFavorite] = useState(false); const [isFavorite, setIsFavorite] = useState(false);
const [currentImage, setCurrentImage] = useState(0); const [currentImage, setCurrentImage] = useState(0);
@ -315,11 +316,11 @@ const FilterBar = ({ filters, onFilterChange }) => {
const cities = [ const cities = [
{ id: 'all', label: 'جميع المدن' }, { id: 'all', label: 'جميع المدن' },
{ id: 'damascus', label: 'دمشق' }, { id: 'دمشق', label: 'دمشق' },
{ id: 'aleppo', label: 'حلب' }, { id: 'حلب', label: 'حلب' },
{ id: 'homs', label: 'حمص' }, { id: 'حمص', label: 'حمص' },
{ id: 'latakia', label: 'اللاذقية' }, { id: 'اللاذقية', label: 'اللاذقية' },
{ id: 'daraa', label: 'درعا' } { id: 'درعا', label: 'درعا' }
]; ];
return ( return (
@ -670,6 +671,7 @@ export default function PropertiesPage() {
className={`p-2 rounded-xl transition-colors ${ className={`p-2 rounded-xl transition-colors ${
viewMode === 'grid' ? 'bg-gray-800 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200' viewMode === 'grid' ? 'bg-gray-800 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`} }`}
title="عرض شبكي"
> >
<Grid3x3 className="w-5 h-5" /> <Grid3x3 className="w-5 h-5" />
</button> </button>
@ -678,6 +680,7 @@ export default function PropertiesPage() {
className={`p-2 rounded-xl transition-colors ${ className={`p-2 rounded-xl transition-colors ${
viewMode === 'list' ? 'bg-gray-800 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200' viewMode === 'list' ? 'bg-gray-800 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`} }`}
title="عرض قائمة"
> >
<List className="w-5 h-5" /> <List className="w-5 h-5" />
</button> </button>

709
app/register/owner/page.js Normal file
View File

@ -0,0 +1,709 @@
'use client';
import { useState, useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import Image from 'next/image';
import {
User,
Mail,
Phone,
Lock,
Eye,
EyeOff,
MessageCircle,
Camera,
Upload,
X,
CheckCircle,
XCircle,
AlertCircle,
ArrowLeft,
Building,
Loader2,
Home
} from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
export default function OwnerRegisterPage() {
const router = useRouter();
const [step, setStep] = useState(1);
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [formData, setFormData] = useState({
name: '',
email: '',
phone: '',
whatsapp: '',
password: '',
confirmPassword: '',
agreeTerms: false
});
const [idImages, setIdImages] = useState({
front: null,
back: null
});
const [idImagePreviews, setIdImagePreviews] = useState({
front: '',
back: ''
});
const [errors, setErrors] = useState({});
const fileInputFrontRef = useRef(null);
const fileInputBackRef = useRef(null);
const handleImageUpload = (side, file) => {
if (!file) return;
if (!file.type.startsWith('image/')) {
toast.error('الرجاء اختيار صورة صالحة');
return;
}
if (file.size > 5 * 1024 * 1024) {
toast.error('حجم الصورة يجب أن يكون أقل من 5 ميجابايت');
return;
}
const reader = new FileReader();
reader.onloadend = () => {
setIdImagePreviews(prev => ({
...prev,
[side]: reader.result
}));
};
reader.readAsDataURL(file);
setIdImages(prev => ({
...prev,
[side]: file
}));
toast.success(`تم رفع الصورة بنجاح`, {
style: { background: '#dcfce7', color: '#166534' }
});
};
const validateEmail = (email) => {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
};
const validatePhone = (phone) => {
const re = /^(09|05)[0-9]{8}$/;
return re.test(phone);
};
const validateStep1 = () => {
const newErrors = {};
if (!formData.name) {
newErrors.name = 'الاسم الكامل مطلوب';
} else if (formData.name.length < 3) {
newErrors.name = 'الاسم يجب أن يكون 3 أحرف على الأقل';
}
if (!formData.email) {
newErrors.email = 'البريد الإلكتروني مطلوب';
} else if (!validateEmail(formData.email)) {
newErrors.email = 'البريد الإلكتروني غير صالح';
}
if (!formData.whatsapp) {
newErrors.whatsapp = 'رقم الواتساب مطلوب';
} else if (!validatePhone(formData.whatsapp)) {
newErrors.whatsapp = 'رقم الواتساب غير صالح (يجب أن يبدأ 09 أو 05)';
}
if (formData.phone && !validatePhone(formData.phone)) {
newErrors.phone = 'رقم الهاتف غير صالح';
}
if (!formData.password) {
newErrors.password = 'كلمة المرور مطلوبة';
} else if (formData.password.length < 6) {
newErrors.password = 'كلمة المرور يجب أن تكون 6 أحرف على الأقل';
}
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = 'كلمات المرور غير متطابقة';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const validateStep2 = () => {
const newErrors = {};
if (!idImages.front) {
newErrors.front = 'صورة الوجه الأمامي للهوية مطلوبة';
}
if (!idImages.back) {
newErrors.back = 'صورة الوجه الخلفي للهوية مطلوبة';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleNextStep = () => {
if (validateStep1()) {
setStep(2);
window.scrollTo({ top: 0, behavior: 'smooth' });
} else {
toast.error('يرجى تصحيح الأخطاء في النموذج');
}
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateStep2()) {
toast.error('يرجى إكمال جميع الصور المطلوبة');
return;
}
if (!formData.agreeTerms) {
toast.error('يجب الموافقة على الشروط والأحكام');
return;
}
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
toast.success('تم إنشاء الحساب بنجاح!', {
style: { background: '#dcfce7', color: '#166534' },
duration: 3000
});
localStorage.setItem('user', JSON.stringify({
name: formData.name,
email: formData.email,
role: 'owner',
avatar: formData.name.charAt(0).toUpperCase()
}));
setTimeout(() => {
router.push('/');
}, 1500);
}, 2000);
};
const fadeInUp = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.5 }
};
const staggerContainer = {
animate: {
transition: {
staggerChildren: 0.1
}
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center p-4 relative overflow-hidden">
<Toaster position="top-center" reverseOrder={false} />
<div className="absolute inset-0 overflow-hidden">
{[...Array(20)].map((_, i) => (
<motion.div
key={i}
className="absolute rounded-full bg-amber-500/10"
style={{
left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`,
width: Math.random() * 200 + 50,
height: Math.random() * 200 + 50,
}}
animate={{
x: [0, Math.random() * 100 - 50, 0],
y: [0, Math.random() * 100 - 50, 0],
}}
transition={{
duration: Math.random() * 15 + 15,
repeat: Infinity,
ease: "linear"
}}
/>
))}
</div>
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5 }}
className="relative z-10 w-full max-w-2xl"
>
<div className="mb-8">
<div className="flex items-center justify-between mb-4">
<Link
href="/auth/choose-role"
className="flex items-center gap-2 text-gray-400 hover:text-white transition-colors group"
>
<motion.div whileHover={{ x: -5 }}>
<ArrowLeft className="w-4 h-4" />
</motion.div>
<span>العودة</span>
</Link>
<div className="flex items-center gap-2">
<span className="text-sm text-gray-400">خطوة {step} من 2</span>
</div>
</div>
<div className="flex gap-2">
<motion.div
className={`h-2 flex-1 rounded-full ${
step >= 1 ? 'bg-amber-500' : 'bg-gray-700'
}`}
animate={{ scaleX: step >= 1 ? 1 : 0.5 }}
/>
<motion.div
className={`h-2 flex-1 rounded-full ${
step >= 2 ? 'bg-amber-500' : 'bg-gray-700'
}`}
animate={{ scaleX: step >= 2 ? 1 : 0.5 }}
/>
</div>
</div>
<motion.div
key={step}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
className="bg-white/5 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/10 overflow-hidden"
>
<div className="bg-gradient-to-r from-amber-500 to-amber-600 p-8 text-center relative overflow-hidden">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: "spring" }}
className="absolute -top-10 -right-10 w-40 h-40 bg-white/10 rounded-full"
/>
<motion.div
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
className="relative z-10"
>
<motion.div
animate={{ rotate: [0, 10, -10, 0] }}
transition={{ duration: 2, repeat: Infinity }}
className="w-20 h-20 mx-auto mb-4 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-sm"
>
<Building className="w-10 h-10 text-white" />
</motion.div>
<h1 className="text-3xl font-bold text-white mb-2">
{step === 1 ? 'معلومات المالك' : 'الوثائق الرسمية'}
</h1>
<p className="text-amber-100">
{step === 1
? 'أدخل معلوماتك الأساسية للتواصل'
: 'يرجى رفع صور الهوية الشخصية للتحقق'}
</p>
</motion.div>
</div>
<div className="p-8">
<motion.form
variants={staggerContainer}
initial="initial"
animate="animate"
onSubmit={step === 1 ? handleNextStep : handleSubmit}
className="space-y-6"
>
{step === 1 ? (
<>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
الاسم الكامل <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<User className={`w-5 h-5 ${
errors.name ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'
}`} />
</div>
<input
type="text"
value={formData.name}
onChange={(e) => {
setFormData({...formData, name: e.target.value});
setErrors({...errors, name: null});
}}
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.name ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أدخل اسمك الكامل"
/>
</div>
{errors.name && (
<p className="text-red-500 text-sm mt-1">{errors.name}</p>
)}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
البريد الإلكتروني <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Mail className={`w-5 h-5 ${
errors.email ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'
}`} />
</div>
<input
type="email"
value={formData.email}
onChange={(e) => {
setFormData({...formData, email: e.target.value});
setErrors({...errors, email: null});
}}
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.email ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أدخل بريدك الإلكتروني"
/>
</div>
{errors.email && (
<p className="text-red-500 text-sm mt-1">{errors.email}</p>
)}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
رقم الهاتف <span className="text-gray-500">(اختياري)</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Phone className="w-5 h-5 text-gray-400 group-focus-within:text-amber-500" />
</div>
<input
type="tel"
value={formData.phone}
onChange={(e) => {
setFormData({...formData, phone: e.target.value});
setErrors({...errors, phone: null});
}}
className="w-full pr-12 pl-4 py-3 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all"
placeholder="أدخل رقم هاتفك (اختياري)"
/>
</div>
{errors.phone && (
<p className="text-red-500 text-sm mt-1">{errors.phone}</p>
)}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
رقم الواتساب <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<MessageCircle className={`w-5 h-5 ${
errors.whatsapp ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'
}`} />
</div>
<input
type="tel"
value={formData.whatsapp}
onChange={(e) => {
setFormData({...formData, whatsapp: e.target.value});
setErrors({...errors, whatsapp: null});
}}
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.whatsapp ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أدخل رقم الواتساب"
/>
</div>
{errors.whatsapp && (
<p className="text-red-500 text-sm mt-1">{errors.whatsapp}</p>
)}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
كلمة المرور <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className={`w-5 h-5 ${
errors.password ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'
}`} />
</div>
<input
type={showPassword ? "text" : "password"}
value={formData.password}
onChange={(e) => {
setFormData({...formData, password: e.target.value});
setErrors({...errors, password: null});
}}
className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.password ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أدخل كلمة المرور"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute inset-y-0 left-0 pl-3 flex items-center"
>
{showPassword ? (
<EyeOff className="w-5 h-5 text-gray-400 hover:text-gray-300" />
) : (
<Eye className="w-5 h-5 text-gray-400 hover:text-gray-300" />
)}
</button>
</div>
{errors.password && (
<p className="text-red-500 text-sm mt-1">{errors.password}</p>
)}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
تأكيد كلمة المرور <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className={`w-5 h-5 ${
errors.confirmPassword ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'
}`} />
</div>
<input
type={showConfirmPassword ? "text" : "password"}
value={formData.confirmPassword}
onChange={(e) => {
setFormData({...formData, confirmPassword: e.target.value});
setErrors({...errors, confirmPassword: null});
}}
className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.confirmPassword ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أعد إدخال كلمة المرور"
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute inset-y-0 left-0 pl-3 flex items-center"
>
{showConfirmPassword ? (
<EyeOff className="w-5 h-5 text-gray-400 hover:text-gray-300" />
) : (
<Eye className="w-5 h-5 text-gray-400 hover:text-gray-300" />
)}
</button>
{formData.confirmPassword && (
<div className="absolute inset-y-0 left-12 flex items-center">
{formData.password === formData.confirmPassword ? (
<CheckCircle className="w-5 h-5 text-green-500" />
) : (
<XCircle className="w-5 h-5 text-red-500" />
)}
</div>
)}
</div>
{errors.confirmPassword && (
<p className="text-red-500 text-sm mt-1">{errors.confirmPassword}</p>
)}
</motion.div>
</>
) : (
<>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
صورة الهوية - الوجه الأمامي <span className="text-red-500">*</span>
</label>
<div
onClick={() => fileInputFrontRef.current?.click()}
className={`relative border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all ${
idImagePreviews.front
? 'border-green-500 bg-green-500/10'
: errors.front
? 'border-red-500 bg-red-500/10'
: 'border-gray-700 hover:border-amber-500 hover:bg-white/5'
}`}
>
<input
ref={fileInputFrontRef}
type="file"
accept="image/*"
onChange={(e) => handleImageUpload('front', e.target.files?.[0])}
className="hidden"
/>
{idImagePreviews.front ? (
<div className="relative">
<Image
src={idImagePreviews.front}
alt="Front ID"
width={200}
height={120}
className="mx-auto rounded-lg object-cover"
/>
<button
onClick={(e) => {
e.stopPropagation();
setIdImages(prev => ({...prev, front: null}));
setIdImagePreviews(prev => ({...prev, front: ''}));
}}
className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 rounded-full flex items-center justify-center hover:bg-red-600"
>
<X className="w-4 h-4 text-white" />
</button>
</div>
) : (
<>
<Camera className="w-12 h-12 text-gray-500 mx-auto mb-3" />
<p className="text-gray-400">اضغط لرفع الصورة</p>
<p className="text-xs text-gray-500 mt-2">
JPEG, PNG, JPG حتى 5MB 800x600 بكسل
</p>
</>
)}
</div>
{errors.front && (
<p className="text-red-500 text-sm mt-1">{errors.front}</p>
)}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
صورة الهوية - الوجه الخلفي <span className="text-red-500">*</span>
</label>
<div
onClick={() => fileInputBackRef.current?.click()}
className={`relative border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all ${
idImagePreviews.back
? 'border-green-500 bg-green-500/10'
: errors.back
? 'border-red-500 bg-red-500/10'
: 'border-gray-700 hover:border-amber-500 hover:bg-white/5'
}`}
>
<input
ref={fileInputBackRef}
type="file"
accept="image/*"
onChange={(e) => handleImageUpload('back', e.target.files?.[0])}
className="hidden"
/>
{idImagePreviews.back ? (
<div className="relative">
<Image
src={idImagePreviews.back}
alt="Back ID"
width={200}
height={120}
className="mx-auto rounded-lg object-cover"
/>
<button
onClick={(e) => {
e.stopPropagation();
setIdImages(prev => ({...prev, back: null}));
setIdImagePreviews(prev => ({...prev, back: ''}));
}}
className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 rounded-full flex items-center justify-center hover:bg-red-600"
>
<X className="w-4 h-4 text-white" />
</button>
</div>
) : (
<>
<Camera className="w-12 h-12 text-gray-500 mx-auto mb-3" />
<p className="text-gray-400">اضغط لرفع الصورة</p>
<p className="text-xs text-gray-500 mt-2">
JPEG, PNG, JPG حتى 5MB 800x600 بكسل
</p>
</>
)}
</div>
{errors.back && (
<p className="text-red-500 text-sm mt-1">{errors.back}</p>
)}
</motion.div>
<motion.div variants={fadeInUp} className="flex items-center gap-2">
<input
type="checkbox"
id="terms"
checked={formData.agreeTerms}
onChange={(e) => setFormData({...formData, agreeTerms: e.target.checked})}
className="w-4 h-4 rounded border-gray-600 bg-white/5 text-amber-500 focus:ring-amber-500 focus:ring-offset-0"
required
/>
<label htmlFor="terms" className="text-sm text-gray-300">
أوافق على{' '}
<Link href="/terms" className="text-amber-400 hover:text-amber-300">
شروط الاستخدام
</Link>
{' '}و{' '}
<Link href="/privacy" className="text-amber-400 hover:text-amber-300">
سياسة الخصوصية
</Link>
</label>
</motion.div>
</>
)}
<motion.div variants={fadeInUp} className="flex gap-3 pt-4">
{step === 1 ? (
<>
<button
type="button"
onClick={() => router.push('/auth/choose-role')}
className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors"
>
إلغاء
</button>
<button
type="button"
onClick={handleNextStep}
className="flex-1 bg-gradient-to-r from-amber-500 to-amber-600 text-white py-3 px-4 rounded-xl font-medium hover:from-amber-600 hover:to-amber-700 transition-all"
>
التالي
</button>
</>
) : (
<>
<button
type="button"
onClick={() => setStep(1)}
className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors"
>
السابق
</button>
<button
type="submit"
disabled={isLoading || !formData.agreeTerms}
className="flex-1 bg-gradient-to-r from-amber-500 to-amber-600 text-white py-3 px-4 rounded-xl font-medium hover:from-amber-600 hover:to-amber-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? (
<div className="flex items-center justify-center gap-2">
<Loader2 className="w-5 h-5 animate-spin" />
<span>جاري التسجيل...</span>
</div>
) : (
'إنشاء حساب'
)}
</button>
</>
)}
</motion.div>
</motion.form>
</div>
</motion.div>
</motion.div>
</div>
);
}

438
app/register/tenant/page.js Normal file
View File

@ -0,0 +1,438 @@
'use client';
import { useState } from 'react';
import { motion } from 'framer-motion';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import {
User,
Mail,
Phone,
Lock,
Eye,
EyeOff,
CheckCircle,
XCircle,
ArrowLeft,
Home,
Loader2
} from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
export default function TenantRegisterPage() {
const router = useRouter();
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [formData, setFormData] = useState({
name: '',
email: '',
phone: '',
password: '',
confirmPassword: '',
agreeTerms: false
});
const [errors, setErrors] = useState({});
const validateEmail = (email) => {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
};
const validatePhone = (phone) => {
const re = /^(09|05)[0-9]{8}$/;
return re.test(phone);
};
const validateForm = () => {
const newErrors = {};
if (!formData.name) {
newErrors.name = 'الاسم الكامل مطلوب';
} else if (formData.name.length < 3) {
newErrors.name = 'الاسم يجب أن يكون 3 أحرف على الأقل';
}
if (!formData.email) {
newErrors.email = 'البريد الإلكتروني مطلوب';
} else if (!validateEmail(formData.email)) {
newErrors.email = 'البريد الإلكتروني غير صالح';
}
if (!formData.phone) {
newErrors.phone = 'رقم الهاتف مطلوب';
} else if (!validatePhone(formData.phone)) {
newErrors.phone = 'رقم الهاتف غير صالح (يجب أن يبدأ 09 أو 05)';
}
if (!formData.password) {
newErrors.password = 'كلمة المرور مطلوبة';
} else if (formData.password.length < 6) {
newErrors.password = 'كلمة المرور يجب أن تكون 6 أحرف على الأقل';
}
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = 'كلمات المرور غير متطابقة';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) {
toast.error('يرجى تصحيح الأخطاء في النموذج');
return;
}
if (!formData.agreeTerms) {
toast.error('يجب الموافقة على الشروط والأحكام');
return;
}
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
toast.success('تم إنشاء الحساب بنجاح!', {
style: { background: '#dcfce7', color: '#166534' },
duration: 3000
});
localStorage.setItem('user', JSON.stringify({
name: formData.name,
email: formData.email,
role: 'tenant',
avatar: formData.name.charAt(0).toUpperCase()
}));
setTimeout(() => {
router.push('/');
}, 1500);
}, 2000);
};
const fadeInUp = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.5 }
};
const staggerContainer = {
animate: {
transition: {
staggerChildren: 0.1
}
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center p-4 relative overflow-hidden">
<Toaster position="top-center" reverseOrder={false} />
<div className="absolute inset-0 overflow-hidden">
{[...Array(20)].map((_, i) => (
<motion.div
key={i}
className="absolute rounded-full bg-blue-500/10"
style={{
left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`,
width: Math.random() * 200 + 50,
height: Math.random() * 200 + 50,
}}
animate={{
x: [0, Math.random() * 100 - 50, 0],
y: [0, Math.random() * 100 - 50, 0],
}}
transition={{
duration: Math.random() * 15 + 15,
repeat: Infinity,
ease: "linear"
}}
/>
))}
</div>
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5 }}
className="relative z-10 w-full max-w-md"
>
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
className="absolute -top-16 left-0"
>
<Link
href="/auth/choose-role"
className="flex items-center gap-2 text-gray-400 hover:text-white transition-colors group"
>
<motion.div whileHover={{ x: -5 }}>
<ArrowLeft className="w-4 h-4" />
</motion.div>
<span>العودة</span>
</Link>
</motion.div>
<div className="bg-white/5 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/10 overflow-hidden">
<div className="bg-gradient-to-r from-blue-500 to-blue-600 p-8 text-center relative overflow-hidden">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: "spring" }}
className="absolute -top-10 -right-10 w-40 h-40 bg-white/10 rounded-full"
/>
<motion.div
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
className="relative z-10"
>
<motion.div
animate={{ rotate: [0, 10, -10, 0] }}
transition={{ duration: 2, repeat: Infinity }}
className="w-20 h-20 mx-auto mb-4 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-sm"
>
<Home className="w-10 h-10 text-white" />
</motion.div>
<h1 className="text-3xl font-bold text-white mb-2">إنشاء حساب مستأجر</h1>
<p className="text-blue-100">انضم إلينا وابحث عن منزل أحلامك</p>
</motion.div>
</div>
<div className="p-8">
<motion.form
variants={staggerContainer}
initial="initial"
animate="animate"
onSubmit={handleSubmit}
className="space-y-6"
>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
الاسم الكامل <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<User className={`w-5 h-5 ${
errors.name ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'
}`} />
</div>
<input
type="text"
value={formData.name}
onChange={(e) => {
setFormData({...formData, name: e.target.value});
setErrors({...errors, name: null});
}}
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.name ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أدخل اسمك الكامل"
/>
</div>
{errors.name && (
<p className="text-red-500 text-sm mt-1">{errors.name}</p>
)}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
البريد الإلكتروني <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Mail className={`w-5 h-5 ${
errors.email ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'
}`} />
</div>
<input
type="email"
value={formData.email}
onChange={(e) => {
setFormData({...formData, email: e.target.value});
setErrors({...errors, email: null});
}}
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.email ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أدخل بريدك الإلكتروني"
/>
</div>
{errors.email && (
<p className="text-red-500 text-sm mt-1">{errors.email}</p>
)}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
رقم الهاتف <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Phone className={`w-5 h-5 ${
errors.phone ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'
}`} />
</div>
<input
type="tel"
value={formData.phone}
onChange={(e) => {
setFormData({...formData, phone: e.target.value});
setErrors({...errors, phone: null});
}}
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.phone ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أدخل رقم هاتفك"
/>
</div>
{errors.phone && (
<p className="text-red-500 text-sm mt-1">{errors.phone}</p>
)}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
كلمة المرور <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className={`w-5 h-5 ${
errors.password ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'
}`} />
</div>
<input
type={showPassword ? "text" : "password"}
value={formData.password}
onChange={(e) => {
setFormData({...formData, password: e.target.value});
setErrors({...errors, password: null});
}}
className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.password ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أدخل كلمة المرور"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute inset-y-0 left-0 pl-3 flex items-center"
>
{showPassword ? (
<EyeOff className="w-5 h-5 text-gray-400 hover:text-gray-300" />
) : (
<Eye className="w-5 h-5 text-gray-400 hover:text-gray-300" />
)}
</button>
</div>
{errors.password && (
<p className="text-red-500 text-sm mt-1">{errors.password}</p>
)}
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">
تأكيد كلمة المرور <span className="text-red-500">*</span>
</label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className={`w-5 h-5 ${
errors.confirmPassword ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'
}`} />
</div>
<input
type={showConfirmPassword ? "text" : "password"}
value={formData.confirmPassword}
onChange={(e) => {
setFormData({...formData, confirmPassword: e.target.value});
setErrors({...errors, confirmPassword: null});
}}
className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.confirmPassword ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أعد إدخال كلمة المرور"
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute inset-y-0 left-0 pl-3 flex items-center"
>
{showConfirmPassword ? (
<EyeOff className="w-5 h-5 text-gray-400 hover:text-gray-300" />
) : (
<Eye className="w-5 h-5 text-gray-400 hover:text-gray-300" />
)}
</button>
{formData.confirmPassword && (
<div className="absolute inset-y-0 left-12 flex items-center">
{formData.password === formData.confirmPassword ? (
<CheckCircle className="w-5 h-5 text-green-500" />
) : (
<XCircle className="w-5 h-5 text-red-500" />
)}
</div>
)}
</div>
{errors.confirmPassword && (
<p className="text-red-500 text-sm mt-1">{errors.confirmPassword}</p>
)}
</motion.div>
<motion.div variants={fadeInUp} className="flex items-center gap-2">
<input
type="checkbox"
id="terms"
checked={formData.agreeTerms}
onChange={(e) => setFormData({...formData, agreeTerms: e.target.checked})}
className="w-4 h-4 rounded border-gray-600 bg-white/5 text-blue-500 focus:ring-blue-500 focus:ring-offset-0"
required
/>
<label htmlFor="terms" className="text-sm text-gray-300">
أوافق على{' '}
<Link href="/terms" className="text-blue-400 hover:text-blue-300">
شروط الاستخدام
</Link>
{' '}و{' '}
<Link href="/privacy" className="text-blue-400 hover:text-blue-300">
سياسة الخصوصية
</Link>
</label>
</motion.div>
<motion.button
variants={fadeInUp}
type="submit"
disabled={isLoading || !formData.agreeTerms}
className="w-full bg-gradient-to-r from-blue-500 to-blue-600 text-white py-4 rounded-xl font-bold text-lg hover:from-blue-600 hover:to-blue-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-blue-500/25"
>
{isLoading ? (
<div className="flex items-center justify-center gap-2">
<Loader2 className="w-5 h-5 animate-spin" />
<span>جاري إنشاء الحساب...</span>
</div>
) : (
'إنشاء حساب'
)}
</motion.button>
<motion.p variants={fadeInUp} className="text-center text-gray-400 mt-4">
لديك حساب بالفعل؟{' '}
<Link
href="/login"
className="text-blue-400 hover:text-blue-300 font-medium transition-colors"
>
تسجيل الدخول
</Link>
</motion.p>
</motion.form>
</div>
</div>
</motion.div>
</div>
);
}

128
package-lock.json generated
View File

@ -8,16 +8,21 @@
"name": "sweethome", "name": "sweethome",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@pbe/react-yandex-maps": "^1.2.5",
"flowbite": "^4.0.1", "flowbite": "^4.0.1",
"flowbite-react": "^0.12.16", "flowbite-react": "^0.12.16",
"framer-motion": "^12.29.2", "framer-motion": "^12.29.2",
"i18next": "^25.8.0", "i18next": "^25.8.0",
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.0",
"leaflet": "^1.9.4",
"lucide-react": "^0.563.0", "lucide-react": "^0.563.0",
"next": "16.1.6", "next": "16.1.6",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"react-i18next": "^16.5.4" "react-hot-toast": "^2.6.0",
"react-i18next": "^16.5.4",
"react-intersection-observer": "^10.0.3",
"react-leaflet": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
@ -44,7 +49,7 @@
"version": "7.27.1", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"devOptional": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@ -54,7 +59,7 @@
"version": "7.28.5", "version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"devOptional": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@ -73,7 +78,7 @@
"version": "7.28.6", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz",
"integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==",
"devOptional": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-string-parser": "^7.27.1", "@babel/helper-string-parser": "^7.27.1",
@ -841,6 +846,21 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@pbe/react-yandex-maps": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/@pbe/react-yandex-maps/-/react-yandex-maps-1.2.5.tgz",
"integrity": "sha512-cBojin5e1fPx9XVCAqHQJsCnHGMeBNsP0TrNfpWCrPFfxb30ye+JgcGr2mn767Gbr1d+RufBLRiUcX2kaiAwjQ==",
"license": "MIT",
"dependencies": {
"@types/yandex-maps": "2.1.29"
},
"engines": {
"node": ">=16"
},
"peerDependencies": {
"react": "^16.x || ^17.x || ^18.x"
}
},
"node_modules/@popperjs/core": { "node_modules/@popperjs/core": {
"version": "2.11.8", "version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
@ -851,6 +871,17 @@
"url": "https://opencollective.com/popperjs" "url": "https://opencollective.com/popperjs"
} }
}, },
"node_modules/@react-leaflet/core": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz",
"integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==",
"license": "Hippocratic-2.1",
"peerDependencies": {
"leaflet": "^1.9.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
},
"node_modules/@rollup/plugin-node-resolve": { "node_modules/@rollup/plugin-node-resolve": {
"version": "15.3.1", "version": "15.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz",
@ -1174,6 +1205,12 @@
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/yandex-maps": {
"version": "2.1.29",
"resolved": "https://registry.npmjs.org/@types/yandex-maps/-/yandex-maps-2.1.29.tgz",
"integrity": "sha512-nuibRWj3RU/9KXlCzTrRtDE+n6V9l7NbT9JakicqZ5OXIdwyb6blvV2Uwn6lB58WYm3DSUDP2I2AWlnWMc8z2w==",
"license": "MIT"
},
"node_modules/@typescript-eslint/project-service": { "node_modules/@typescript-eslint/project-service": {
"version": "8.46.2", "version": "8.46.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz",
@ -1328,7 +1365,7 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz",
"integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==",
"devOptional": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/types": "^7.26.0" "@babel/types": "^7.26.0"
@ -1471,6 +1508,12 @@
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
},
"node_modules/daisyui": { "node_modules/daisyui": {
"version": "5.5.14", "version": "5.5.14",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.5.14.tgz", "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.5.14.tgz",
@ -1750,6 +1793,15 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/goober": {
"version": "2.1.18",
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz",
"integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==",
"license": "MIT",
"peerDependencies": {
"csstype": "^3.0.10"
}
},
"node_modules/graceful-fs": { "node_modules/graceful-fs": {
"version": "4.2.11", "version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@ -1886,6 +1938,12 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/leaflet": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"license": "BSD-2-Clause"
},
"node_modules/lightningcss": { "node_modules/lightningcss": {
"version": "1.30.2", "version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
@ -2444,6 +2502,23 @@
"react": "^19.2.3" "react": "^19.2.3"
} }
}, },
"node_modules/react-hot-toast": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",
"integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==",
"license": "MIT",
"dependencies": {
"csstype": "^3.1.3",
"goober": "^2.1.16"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
}
},
"node_modules/react-i18next": { "node_modules/react-i18next": {
"version": "16.5.4", "version": "16.5.4",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.4.tgz", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.4.tgz",
@ -2471,6 +2546,35 @@
} }
} }
}, },
"node_modules/react-intersection-observer": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-10.0.3.tgz",
"integrity": "sha512-luICLMbs0zxTO/70Zy7K5jOXkABPEVSAF8T3FdZUlctsrIaPLmx8TZe2SSA+CY2HGWfz2INyNTnp82pxNNsShA==",
"license": "MIT",
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-leaflet": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz",
"integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==",
"license": "Hippocratic-2.1",
"dependencies": {
"@react-leaflet/core": "^3.0.0"
},
"peerDependencies": {
"leaflet": "^1.9.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
},
"node_modules/readdirp": { "node_modules/readdirp": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@ -2752,20 +2856,6 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",

View File

@ -8,16 +8,21 @@
"start": "next start" "start": "next start"
}, },
"dependencies": { "dependencies": {
"@pbe/react-yandex-maps": "^1.2.5",
"flowbite": "^4.0.1", "flowbite": "^4.0.1",
"flowbite-react": "^0.12.16", "flowbite-react": "^0.12.16",
"framer-motion": "^12.29.2", "framer-motion": "^12.29.2",
"i18next": "^25.8.0", "i18next": "^25.8.0",
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.0",
"leaflet": "^1.9.4",
"lucide-react": "^0.563.0", "lucide-react": "^0.563.0",
"next": "16.1.6", "next": "16.1.6",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"react-i18next": "^16.5.4" "react-hot-toast": "^2.6.0",
"react-i18next": "^16.5.4",
"react-intersection-observer": "^10.0.3",
"react-leaflet": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",