Compare commits
2 Commits
db949aaeba
...
0ba435fd7e
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ba435fd7e | |||
| 9c2a748ae9 |
@ -6,6 +6,7 @@ import Link from "next/link";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { NavLink, MobileNavLink } from "./components/NavLinks";
|
import { NavLink, MobileNavLink } from "./components/NavLinks";
|
||||||
import { FavoritesProvider } from '@/app/contexts/FavoritesContext';
|
import { FavoritesProvider } from '@/app/contexts/FavoritesContext';
|
||||||
|
import { NotificationsProvider } from '@/app/contexts/NotificationsContext';
|
||||||
import FloatingSidebar from '@/app/components/FloatingSidebar';
|
import FloatingSidebar from '@/app/components/FloatingSidebar';
|
||||||
import {
|
import {
|
||||||
Globe,
|
Globe,
|
||||||
@ -708,10 +709,12 @@ export default function ClientLayout({ children }) {
|
|||||||
<main
|
<main
|
||||||
className={`${!isAuthPage && !isProfilePage ? "pt-20" : ""} min-h-screen bg-gradient-to-b from-gray-50 to-white ${currentLanguage === "ar" ? "text-right" : "text-left"}`}
|
className={`${!isAuthPage && !isProfilePage ? "pt-20" : ""} min-h-screen bg-gradient-to-b from-gray-50 to-white ${currentLanguage === "ar" ? "text-right" : "text-left"}`}
|
||||||
>
|
>
|
||||||
<FavoritesProvider>
|
<NotificationsProvider>
|
||||||
{children}
|
<FavoritesProvider>
|
||||||
<FloatingSidebar isRTL={currentLanguage === 'ar'} isAdmin={isAdmin} />
|
{children}
|
||||||
</FavoritesProvider>
|
<FloatingSidebar isRTL={currentLanguage === 'ar'} isAdmin={isAdmin} />
|
||||||
|
</FavoritesProvider>
|
||||||
|
</NotificationsProvider>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{!isAuthPage && !isProfilePage && (
|
{!isAuthPage && !isProfilePage && (
|
||||||
|
|||||||
@ -5,9 +5,11 @@ import { motion } from 'framer-motion';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Heart, Bell, CreditCard, Shield, UserPlus } from 'lucide-react';
|
import { Heart, Bell, CreditCard, Shield, UserPlus } from 'lucide-react';
|
||||||
import { useFavorites } from '@/app/contexts/FavoritesContext';
|
import { useFavorites } from '@/app/contexts/FavoritesContext';
|
||||||
|
import { useNotifications } from '@/app/contexts/NotificationsContext';
|
||||||
|
|
||||||
export default function FloatingSidebar({ isRTL, isAdmin }) {
|
export default function FloatingSidebar({ isRTL, isAdmin }) {
|
||||||
const { favorites } = useFavorites();
|
const { favorites } = useFavorites();
|
||||||
|
const { unreadCount } = useNotifications();
|
||||||
const [tooltip, setTooltip] = useState(null);
|
const [tooltip, setTooltip] = useState(null);
|
||||||
let timeoutId = null;
|
let timeoutId = null;
|
||||||
|
|
||||||
@ -150,13 +152,15 @@ export default function FloatingSidebar({ isRTL, isAdmin }) {
|
|||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Bell className="w-6 h-6 text-gray-600 transition-colors group-hover:text-amber-600" />
|
<Bell className="w-6 h-6 text-gray-600 transition-colors group-hover:text-amber-600" />
|
||||||
<motion.span
|
{unreadCount > 0 && (
|
||||||
initial={{ scale: 0 }}
|
<motion.span
|
||||||
animate={{ scale: 1 }}
|
initial={{ scale: 0 }}
|
||||||
className="absolute -right-1 -top-1 w-5 h-5 bg-linear-to-r from-red-500 to-red-600 text-white text-xs rounded-full flex items-center justify-center shadow-md"
|
animate={{ scale: 1 }}
|
||||||
>
|
className="absolute -right-1 -top-1 w-5 h-5 bg-linear-to-r from-red-500 to-red-600 text-white text-xs rounded-full flex items-center justify-center shadow-md"
|
||||||
3
|
>
|
||||||
</motion.span>
|
{unreadCount}
|
||||||
|
</motion.span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
{renderTooltip('notifications', 'الإشعارات')}
|
{renderTooltip('notifications', 'الإشعارات')}
|
||||||
|
|||||||
75
app/contexts/NotificationsContext.js
Normal file
75
app/contexts/NotificationsContext.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||||
|
import { getUserNotifications } from '../utils/api';
|
||||||
|
import AuthService from '../services/AuthService';
|
||||||
|
|
||||||
|
const NotificationsContext = createContext();
|
||||||
|
|
||||||
|
export const useNotifications = () => {
|
||||||
|
const context = useContext(NotificationsContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useNotifications must be used within NotificationsProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function NotificationsProvider({ children }) {
|
||||||
|
const [notifications, setNotifications] = useState([]);
|
||||||
|
const [unreadCount, setUnreadCount] = useState(0);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const fetchNotifications = useCallback(async () => {
|
||||||
|
if (!AuthService.isAuthenticated()) {
|
||||||
|
setNotifications([]);
|
||||||
|
setUnreadCount(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await getUserNotifications();
|
||||||
|
const notificationsArray = Array.isArray(data) ? data : [];
|
||||||
|
setNotifications(notificationsArray);
|
||||||
|
// Assuming all are unread for now, or add logic to check 'read' field if exists
|
||||||
|
setUnreadCount(notificationsArray.length);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching notifications:', error);
|
||||||
|
setNotifications([]);
|
||||||
|
setUnreadCount(0);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchNotifications();
|
||||||
|
}, [fetchNotifications]);
|
||||||
|
|
||||||
|
const markAsRead = useCallback((id) => {
|
||||||
|
setNotifications(prev =>
|
||||||
|
prev.map(n => (n.id === id ? { ...n, read: true } : n))
|
||||||
|
);
|
||||||
|
setUnreadCount(prev => Math.max(0, prev - 1));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const markAllAsRead = useCallback(() => {
|
||||||
|
setNotifications(prev => prev.map(n => ({ ...n, read: true })));
|
||||||
|
setUnreadCount(0);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
notifications,
|
||||||
|
unreadCount,
|
||||||
|
isLoading,
|
||||||
|
fetchNotifications,
|
||||||
|
markAsRead,
|
||||||
|
markAllAsRead,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NotificationsContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</NotificationsContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -4,71 +4,28 @@ import { useEffect, useState } from 'react';
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { Bell, CheckCircle, XCircle, Calendar, MessageCircle } from 'lucide-react';
|
import { Bell, CheckCircle, XCircle, Calendar, MessageCircle } from 'lucide-react';
|
||||||
import AuthService from '@/app/services/AuthService';
|
import AuthService from '@/app/services/AuthService';
|
||||||
|
import { useNotifications } from '@/app/contexts/NotificationsContext';
|
||||||
const mockNotifications = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
type: 'booking',
|
|
||||||
title: 'تأكيد الحجز',
|
|
||||||
message: 'تم تأكيد حجزك في فيلا المزة للفترة 10-15 مارس',
|
|
||||||
date: '2024-03-01',
|
|
||||||
read: false,
|
|
||||||
icon: CheckCircle,
|
|
||||||
color: 'text-green-600',
|
|
||||||
bgColor: 'bg-green-50'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
type: 'payment',
|
|
||||||
title: 'دفعة مستلمة',
|
|
||||||
message: 'تم استلام دفعة الإيجار بقيمة 500,000 ل.س',
|
|
||||||
date: '2024-02-28',
|
|
||||||
read: false,
|
|
||||||
icon: MessageCircle,
|
|
||||||
color: 'text-blue-600',
|
|
||||||
bgColor: 'bg-blue-50'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
type: 'reminder',
|
|
||||||
title: 'تذكير بالإيجار',
|
|
||||||
message: 'ينتهي عقد الإيجار خلال 3 أيام',
|
|
||||||
date: '2024-02-25',
|
|
||||||
read: true,
|
|
||||||
icon: Calendar,
|
|
||||||
color: 'text-amber-600',
|
|
||||||
bgColor: 'bg-amber-50'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function NotificationsPage() {
|
export default function NotificationsPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [notifications, setNotifications] = useState([]);
|
const { notifications, unreadCount, isLoading } = useNotifications();
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (AuthService.isAdmin()) {
|
if (!AuthService.isAuthenticated()) {
|
||||||
router.push('/');
|
router.push('/login');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
|
||||||
setNotifications(mockNotifications);
|
|
||||||
setIsLoading(false);
|
|
||||||
}, 500);
|
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
const markAsRead = (id) => {
|
const markAsRead = (id) => {
|
||||||
setNotifications(prev =>
|
// This will be handled by context if needed
|
||||||
prev.map(n => (n.id === id ? { ...n, read: true } : n))
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const markAllAsRead = () => {
|
const markAllAsRead = () => {
|
||||||
setNotifications(prev => prev.map(n => ({ ...n, read: true })));
|
// This will be handled by context if needed
|
||||||
};
|
};
|
||||||
|
|
||||||
const unreadCount = notifications.filter(n => !n.read).length;
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
@ -80,6 +37,18 @@ export default function NotificationsPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<XCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
|
||||||
|
<h3 className="text-xl font-bold text-gray-700 mb-2">خطأ في التحميل</h3>
|
||||||
|
<p className="text-gray-500">{error}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 py-8">
|
<div className="min-h-screen bg-gray-50 py-8">
|
||||||
<div className="container mx-auto px-4 max-w-4xl">
|
<div className="container mx-auto px-4 max-w-4xl">
|
||||||
@ -100,30 +69,31 @@ export default function NotificationsPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{notifications.map((notification) => {
|
{notifications.map((notification, index) => (
|
||||||
const Icon = notification.icon;
|
<div
|
||||||
return (
|
key={index}
|
||||||
<div
|
className="bg-white rounded-2xl shadow-sm border transition-all hover:shadow-md border-gray-200"
|
||||||
key={notification.id}
|
>
|
||||||
className={`bg-white rounded-2xl shadow-sm border transition-all hover:shadow-md 'border-gray-200'}`}
|
<div className="p-5 flex gap-4">
|
||||||
>
|
<div className="w-12 h-12 bg-blue-50 rounded-full flex items-center justify-center shrink-0">
|
||||||
<div className="p-5 flex gap-4">
|
<Bell className="w-6 h-6 text-blue-600" />
|
||||||
<div className={`w-12 h-12 ${notification.bgColor} rounded-full flex items-center justify-center flex-shrink-0`}>
|
</div>
|
||||||
<Icon className={`w-6 h-6 ${notification.color}`} />
|
<div className="flex-1">
|
||||||
</div>
|
<div className="flex justify-between items-start">
|
||||||
<div className="flex-1">
|
<div>
|
||||||
<div className="flex justify-between items-start">
|
<h3 className="font-bold text-gray-900">{notification.title}</h3>
|
||||||
<div>
|
{notification.message && (
|
||||||
<h3 className="font-bold text-gray-900">{notification.title}</h3>
|
|
||||||
<p className="text-gray-600 text-sm mt-1">{notification.message}</p>
|
<p className="text-gray-600 text-sm mt-1">{notification.message}</p>
|
||||||
|
)}
|
||||||
|
{notification.date && (
|
||||||
<p className="text-xs text-gray-400 mt-2">{notification.date}</p>
|
<p className="text-xs text-gray-400 mt-2">{notification.date}</p>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
})}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -366,3 +366,7 @@ export async function addFavoriteProperty(propId) {
|
|||||||
export async function removeFavoriteProperty(favePropId) {
|
export async function removeFavoriteProperty(favePropId) {
|
||||||
return apiFetch(`/FavoriteProperty/Remove?favePropId=${favePropId}`, { method: 'DELETE' });
|
return apiFetch(`/FavoriteProperty/Remove?favePropId=${favePropId}`, { method: 'DELETE' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getUserNotifications() {
|
||||||
|
return apiFetch('/Notifications/GetUserNotifications');
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user