Added Admin page, Login, forgot password, register and owner with profile
This commit is contained in:
651
app/ClientLayout.js
Normal file
651
app/ClientLayout.js
Normal 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>© {currentYear} {t("copyright")}. {t("allRightsReserved")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
// app/admin/page.js (محدث)
|
||||
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
232
app/auth/choose-role/page.js
Normal file
232
app/auth/choose-role/page.js
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { LogIn, UserPlus } from 'lucide-react';
|
||||
|
||||
export function NavLink({ href, children }) {
|
||||
const pathname = usePathname();
|
||||
|
||||
129
app/forgot-password/page.js
Normal file
129
app/forgot-password/page.js
Normal 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>
|
||||
);
|
||||
}
|
||||
276
app/layout.js
276
app/layout.js
@ -1,15 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import "./i18n/config";
|
||||
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';
|
||||
import ClientLayout from "./ClientLayout";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@ -21,264 +12,19 @@ const geistMono = Geist_Mono({
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [currentLanguage, setCurrentLanguage] = useState('en');
|
||||
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 const metadata = {
|
||||
title: "SweetHome",
|
||||
description: "Discover premium furniture and home decor",
|
||||
};
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return (
|
||||
<html lang={currentLanguage}>
|
||||
<head>
|
||||
<title>SweetHome</title>
|
||||
<meta name="description" content={currentLanguage === 'ar' ? 'اكتشف أثاث وديكور منزلي فاخر' : 'Discover premium furniture and home decor'} />
|
||||
</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'}`}>
|
||||
<html lang="en">
|
||||
<head />
|
||||
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
|
||||
<ClientLayout>
|
||||
{children}
|
||||
</main>
|
||||
|
||||
<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>© {currentYear} {t("copyright")}. {t("allRightsReserved")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</ClientLayout>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
424
app/login/page.js
Normal file
424
app/login/page.js
Normal 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>
|
||||
);
|
||||
}
|
||||
1177
app/owner/properties/add/page.js
Normal file
1177
app/owner/properties/add/page.js
Normal file
File diff suppressed because it is too large
Load Diff
1022
app/owner/properties/page.js
Normal file
1022
app/owner/properties/page.js
Normal file
File diff suppressed because it is too large
Load Diff
469
app/page.js
469
app/page.js
@ -16,11 +16,20 @@ import {
|
||||
ChevronDown,
|
||||
Shield,
|
||||
Award,
|
||||
Sparkles
|
||||
Sparkles,
|
||||
UserCircle,
|
||||
LogOut,
|
||||
Calendar,
|
||||
Building,
|
||||
PlusCircle,
|
||||
Heart,
|
||||
MessageCircle
|
||||
} from 'lucide-react';
|
||||
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() {
|
||||
const { t } = useTranslation();
|
||||
@ -29,6 +38,32 @@ export default function HomePage() {
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
const storedUser = localStorage.getItem('user');
|
||||
if (storedUser) {
|
||||
setUser(JSON.parse(storedUser));
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target)) {
|
||||
setShowUserMenu(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('user');
|
||||
setUser(null);
|
||||
setShowUserMenu(false);
|
||||
};
|
||||
|
||||
const [allProperties] = useState([
|
||||
{
|
||||
@ -258,6 +293,15 @@ export default function HomePage() {
|
||||
});
|
||||
};
|
||||
|
||||
const getUserInitial = () => {
|
||||
if (user?.name) {
|
||||
return user.name.charAt(0).toUpperCase();
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const isOwner = user?.role === 'owner';
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<section className="relative min-h-screen flex items-center justify-center overflow-hidden">
|
||||
@ -319,10 +363,35 @@ export default function HomePage() {
|
||||
{t("heroSubtitle")}
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
<HeroSearch onSearch={applyFilters} />
|
||||
|
||||
{!isOwner && <HeroSearch onSearch={applyFilters} />}
|
||||
|
||||
{isOwner && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="bg-white/10 backdrop-blur-lg rounded-2xl p-8 text-center border border-white/20"
|
||||
>
|
||||
<h2 className="text-2xl font-bold text-white mb-2">
|
||||
مرحباً {user?.name}!
|
||||
</h2>
|
||||
<p className="text-gray-200 mb-4">
|
||||
يمكنك إدارة عقاراتك من خلال لوحة التحكم الخاصة بك
|
||||
</p>
|
||||
{/* <Link
|
||||
href="/owner/properties"
|
||||
className="inline-flex items-center gap-2 bg-amber-500 text-white px-6 py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors"
|
||||
>
|
||||
<Building className="w-5 h-5" />
|
||||
إدارة عقاراتي
|
||||
</Link> */}
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!showMap && (
|
||||
|
||||
{!showMap && !isOwner && (
|
||||
<motion.div
|
||||
className="absolute bottom-8 left-1/2 transform -translate-x-1/2 cursor-pointer"
|
||||
animate={{
|
||||
@ -344,211 +413,219 @@ export default function HomePage() {
|
||||
</motion.div>
|
||||
)}
|
||||
</section>
|
||||
<AnimatePresence mode="wait">
|
||||
{showMap && (
|
||||
<motion.section
|
||||
ref={mapSectionRef}
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -50 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
damping: 20,
|
||||
stiffness: 100,
|
||||
duration: 0.6
|
||||
}}
|
||||
className="py-12 bg-gray-50 relative"
|
||||
>
|
||||
{isScrolling && (
|
||||
<motion.div
|
||||
className="absolute top-0 left-0 right-0 h-1 bg-amber-500 z-10"
|
||||
initial={{ scaleX: 0 }}
|
||||
animate={{ scaleX: 1 }}
|
||||
transition={{ duration: 1, ease: "easeInOut" }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="container mx-auto px-4">
|
||||
<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>
|
||||
{!isOwner && (
|
||||
<AnimatePresence mode="wait">
|
||||
{showMap && (
|
||||
<motion.section
|
||||
ref={mapSectionRef}
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -50 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
damping: 20,
|
||||
stiffness: 100,
|
||||
duration: 0.6
|
||||
}}
|
||||
className="py-12 bg-gray-50 relative"
|
||||
>
|
||||
{isScrolling && (
|
||||
<motion.div
|
||||
className="absolute top-0 left-0 right-0 h-1 bg-amber-500 z-10"
|
||||
initial={{ scaleX: 0 }}
|
||||
animate={{ scaleX: 1 }}
|
||||
transition={{ duration: 1, ease: "easeInOut" }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{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 && (
|
||||
<div className="container mx-auto px-4">
|
||||
<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"
|
||||
transition={{ delay: 0.2 }}
|
||||
className="text-center mb-8"
|
||||
>
|
||||
<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 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 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<div className="inline-block px-4 py-1 bg-amber-100 text-amber-700 rounded-full text-sm font-medium mb-4">
|
||||
لماذا نحن؟
|
||||
</div>
|
||||
</motion.section>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<section className="py-20 bg-gradient-to-b from-white to-gray-50">
|
||||
<div className="container mx-auto px-4">
|
||||
<motion.div
|
||||
className="text-center mb-12"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<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")}
|
||||
</h2>
|
||||
<p className="text-gray-600 max-w-2xl mx-auto text-lg">
|
||||
{t("whyChooseUsSubtitle")}
|
||||
</p>
|
||||
</motion.div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4 tracking-tight">
|
||||
{t("whyChooseUsTitle")}
|
||||
</h2>
|
||||
<p className="text-gray-600 max-w-2xl mx-auto text-lg">
|
||||
{t("whyChooseUsSubtitle")}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<motion.div
|
||||
className="group bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: 0.1 }}
|
||||
whileHover={{ y: -4 }}
|
||||
>
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="w-12 h-12 bg-amber-100 rounded-xl flex items-center justify-center group-hover:bg-amber-200 transition-colors duration-300">
|
||||
<ShieldCheck className="w-6 h-6 text-amber-600" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<motion.div
|
||||
className="group bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: 0.1 }}
|
||||
whileHover={{ y: -4 }}
|
||||
>
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="w-12 h-12 bg-amber-100 rounded-xl flex items-center justify-center group-hover:bg-amber-200 transition-colors duration-300">
|
||||
<ShieldCheck className="w-6 h-6 text-amber-600" />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-gray-900">
|
||||
{t("feature1Title")}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-600 text-sm leading-relaxed">
|
||||
{t("feature1Description")}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="group bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
whileHover={{ y: -4 }}
|
||||
>
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center group-hover:bg-blue-200 transition-colors duration-300">
|
||||
<Lock className="w-6 h-6 text-blue-600" />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-gray-900">
|
||||
{t("feature2Title")}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-600 text-sm leading-relaxed">
|
||||
{t("feature2Description")}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="group bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: 0.3 }}
|
||||
whileHover={{ y: -4 }}
|
||||
>
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center group-hover:bg-green-200 transition-colors duration-300">
|
||||
<Zap className="w-6 h-6 text-green-600" />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-gray-900">
|
||||
{t("feature3Title")}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-600 text-sm leading-relaxed">
|
||||
{t("feature3Description")}
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-gray-900">
|
||||
{t("feature1Title")}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-600 text-sm leading-relaxed">
|
||||
{t("feature1Description")}
|
||||
</p>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="group bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
whileHover={{ y: -4 }}
|
||||
>
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center group-hover:bg-blue-200 transition-colors duration-300">
|
||||
<Lock className="w-6 h-6 text-blue-600" />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-gray-900">
|
||||
{t("feature2Title")}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-600 text-sm leading-relaxed">
|
||||
{t("feature2Description")}
|
||||
</p>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="group bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: 0.3 }}
|
||||
whileHover={{ y: -4 }}
|
||||
>
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center group-hover:bg-green-200 transition-colors duration-300">
|
||||
<Zap className="w-6 h-6 text-green-600" />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-gray-900">
|
||||
{t("feature3Title")}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-600 text-sm leading-relaxed">
|
||||
{t("feature3Description")}
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
641
app/profile/page.js
Normal file
641
app/profile/page.js
Normal 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>
|
||||
);
|
||||
}
|
||||
709
app/register/owner/page.js
Normal file
709
app/register/owner/page.js
Normal 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
438
app/register/tenant/page.js
Normal 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>
|
||||
);
|
||||
}
|
||||
93
package-lock.json
generated
93
package-lock.json
generated
@ -8,6 +8,7 @@
|
||||
"name": "sweethome",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@pbe/react-yandex-maps": "^1.2.5",
|
||||
"flowbite": "^4.0.1",
|
||||
"flowbite-react": "^0.12.16",
|
||||
"framer-motion": "^12.29.2",
|
||||
@ -18,7 +19,9 @@
|
||||
"next": "16.1.6",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"react-i18next": "^16.5.4",
|
||||
"react-intersection-observer": "^10.0.3",
|
||||
"react-leaflet": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -46,7 +49,7 @@
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@ -56,7 +59,7 @@
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
||||
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@ -75,7 +78,7 @@
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz",
|
||||
"integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
@ -843,6 +846,21 @@
|
||||
"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": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
@ -1187,6 +1205,12 @@
|
||||
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
|
||||
"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": {
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz",
|
||||
@ -1341,7 +1365,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz",
|
||||
"integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.26.0"
|
||||
@ -1484,6 +1508,12 @@
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"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": {
|
||||
"version": "5.5.14",
|
||||
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.5.14.tgz",
|
||||
@ -1763,6 +1793,15 @@
|
||||
"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": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@ -2463,6 +2502,23 @@
|
||||
"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": {
|
||||
"version": "16.5.4",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.4.tgz",
|
||||
@ -2490,6 +2546,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@ -2785,20 +2856,6 @@
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"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": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pbe/react-yandex-maps": "^1.2.5",
|
||||
"flowbite": "^4.0.1",
|
||||
"flowbite-react": "^0.12.16",
|
||||
"framer-motion": "^12.29.2",
|
||||
@ -18,7 +19,9 @@
|
||||
"next": "16.1.6",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"react-i18next": "^16.5.4",
|
||||
"react-intersection-observer": "^10.0.3",
|
||||
"react-leaflet": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Reference in New Issue
Block a user