From 838a06f2155997a036b33fe57799220e5a09e0a0 Mon Sep 17 00:00:00 2001 From: alma Date: Sun, 4 May 2025 22:42:54 +0200 Subject: [PATCH] courrier --- components/announcement/announcement-form.tsx | 6 +- .../announcement/announcements-list.tsx | 84 +++++----- components/auth/auth-check.tsx | 14 +- components/background-switcher.tsx | 50 +++--- components/layout/layout-wrapper.tsx | 37 ++-- components/main-nav.tsx | 158 ++++++++---------- components/sidebar.tsx | 79 ++++----- components/ui/scroll-area.tsx | 42 +++-- hooks/use-local-storage.ts | 56 ------- hooks/use-unread-announcements.ts | 93 ----------- 10 files changed, 205 insertions(+), 414 deletions(-) delete mode 100644 hooks/use-local-storage.ts delete mode 100644 hooks/use-unread-announcements.ts diff --git a/components/announcement/announcement-form.tsx b/components/announcement/announcement-form.tsx index 5f9e25ce..04738bf0 100644 --- a/components/announcement/announcement-form.tsx +++ b/components/announcement/announcement-form.tsx @@ -219,11 +219,11 @@ export function AnnouncementForm({ userRole }: AnnouncementFormProps) { {availableRoles.map(role => ( handleRoleToggle(role.id)} > diff --git a/components/announcement/announcements-list.tsx b/components/announcement/announcements-list.tsx index 5c898b97..220eafbd 100644 --- a/components/announcement/announcements-list.tsx +++ b/components/announcement/announcements-list.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect } from "react"; import { Table, TableBody, @@ -34,50 +34,58 @@ import { } from "@/components/ui/dialog"; import { Announcement } from "@/app/types/announcement"; import { useToast } from "@/components/ui/use-toast"; -import { useUnreadAnnouncements } from '@/hooks/use-unread-announcements'; interface AnnouncementsListProps { userRole: string[]; } export function AnnouncementsList({ userRole }: AnnouncementsListProps) { + const [announcements, setAnnouncements] = useState([]); const [selectedAnnouncement, setSelectedAnnouncement] = useState(null); const [isViewDialogOpen, setIsViewDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const { toast } = useToast(); - - // Use the updated hook which provides announcements data and loading state - const { - announcements, - isLoading: loading, - checkForUnreadAnnouncements: fetchAnnouncements, - markAsRead - } = useUnreadAnnouncements(); + + // Fetch announcements + const fetchAnnouncements = async () => { + try { + setLoading(true); + const response = await fetch('/api/announcements'); + + if (!response.ok) { + throw new Error('Failed to fetch announcements'); + } + + const data = await response.json(); + setAnnouncements(data); + setError(null); + } catch (err) { + console.error('Error fetching announcements:', err); + setError('Failed to load announcements'); + } finally { + setLoading(false); + } + }; useEffect(() => { - // Use the fetch function from the hook fetchAnnouncements(); - }, [fetchAnnouncements]); - - // Handle viewing an announcement - const handleViewAnnouncement = useCallback((announcement: Announcement) => { - setSelectedAnnouncement(announcement); - setIsViewDialogOpen(true); - - // Use setTimeout to separate the state updates - setTimeout(() => { - markAsRead(announcement.id); - }, 0); - }, [markAsRead]); - - // Handle deleting an announcement - const handleDeleteClick = useCallback((announcement: Announcement) => { - setSelectedAnnouncement(announcement); - setIsDeleteDialogOpen(true); }, []); - const confirmDelete = useCallback(async () => { + // Handle viewing an announcement + const handleViewAnnouncement = (announcement: Announcement) => { + setSelectedAnnouncement(announcement); + setIsViewDialogOpen(true); + }; + + // Handle deleting an announcement + const handleDeleteClick = (announcement: Announcement) => { + setSelectedAnnouncement(announcement); + setIsDeleteDialogOpen(true); + }; + + const confirmDelete = async () => { if (!selectedAnnouncement) return; try { @@ -89,14 +97,10 @@ export function AnnouncementsList({ userRole }: AnnouncementsListProps) { throw new Error('Failed to delete announcement'); } - // Close dialog first + // Update the local state + setAnnouncements(announcements.filter(a => a.id !== selectedAnnouncement.id)); setIsDeleteDialogOpen(false); - // Then refresh data using the hook - setTimeout(() => { - fetchAnnouncements(); - }, 0); - toast({ title: "Announcement deleted", description: "The announcement has been deleted successfully.", @@ -109,22 +113,22 @@ export function AnnouncementsList({ userRole }: AnnouncementsListProps) { variant: "destructive", }); } - }, [selectedAnnouncement, fetchAnnouncements, toast]); + }; // Format roles for display - const formatRoles = useCallback((roles: string[]) => { + const formatRoles = (roles: string[]) => { return roles.map(role => { const roleName = role === "all" ? "All Users" : role.charAt(0).toUpperCase() + role.slice(1); return ( - + {roleName} ); }); - }, []); + }; return ( @@ -178,7 +182,6 @@ export function AnnouncementsList({ userRole }: AnnouncementsListProps) { variant="outline" size="sm" onClick={() => handleViewAnnouncement(announcement)} - className="bg-white hover:bg-gray-50 border-gray-200 text-gray-700" > View @@ -187,7 +190,6 @@ export function AnnouncementsList({ userRole }: AnnouncementsListProps) { variant="outline" size="sm" onClick={() => handleDeleteClick(announcement)} - className="bg-white hover:bg-gray-50 border-gray-200 text-gray-700" > Delete diff --git a/components/auth/auth-check.tsx b/components/auth/auth-check.tsx index c32f098c..c8e0c21e 100644 --- a/components/auth/auth-check.tsx +++ b/components/auth/auth-check.tsx @@ -2,29 +2,19 @@ import { useSession } from "next-auth/react"; import { usePathname, useRouter } from "next/navigation"; -import { useEffect, useCallback } from "react"; +import { useEffect } from "react"; export function AuthCheck({ children }: { children: React.ReactNode }) { const { data: session, status } = useSession(); const pathname = usePathname(); const router = useRouter(); - // Memoize the redirect logic to prevent unnecessary re-renders - const handleAuthRedirect = useCallback(() => { + useEffect(() => { if (status === "unauthenticated" && pathname !== "/signin") { router.push("/signin"); } }, [status, router, pathname]); - useEffect(() => { - // Use setTimeout to avoid immediate state updates during rendering - const timer = setTimeout(() => { - handleAuthRedirect(); - }, 0); - - return () => clearTimeout(timer); - }, [handleAuthRedirect]); - if (status === "loading") { return
Chargement...
; } diff --git a/components/background-switcher.tsx b/components/background-switcher.tsx index ff8e81e6..d67c170c 100644 --- a/components/background-switcher.tsx +++ b/components/background-switcher.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect } from "react"; const backgroundImages = [ "/background/Autumn birger-strahl-6YZgnYaPD5s-unsplash.jpeg", @@ -48,17 +48,14 @@ const backgroundImages = [ export function useBackgroundImage() { const [currentBackground, setCurrentBackground] = useState(backgroundImages[0]); - // Memoize the change background function - const changeBackground = useCallback(() => { - setCurrentBackground(prevBackground => { - const currentIndex = backgroundImages.indexOf(prevBackground); - const nextIndex = (currentIndex + 1) % backgroundImages.length; - return backgroundImages[nextIndex]; - }); - }, []); + const changeBackground = () => { + const currentIndex = backgroundImages.indexOf(currentBackground); + const nextIndex = (currentIndex + 1) % backgroundImages.length; + setCurrentBackground(backgroundImages[nextIndex]); + }; useEffect(() => { - // Set initial random background only once on mount + // Set initial random background const randomIndex = Math.floor(Math.random() * backgroundImages.length); setCurrentBackground(backgroundImages[randomIndex]); }, []); @@ -71,16 +68,16 @@ export function BackgroundSwitcher({ children }: { children: React.ReactNode }) const [imageError, setImageError] = useState(false); // Function to preload an image - const preloadImage = useCallback((src: string): Promise => { + const preloadImage = (src: string): Promise => { return new Promise((resolve, reject) => { const img = new Image(); img.src = src; img.onload = () => resolve(src); img.onerror = () => reject(new Error(`Failed to load image: ${src}`)); }); - }, []); + }; - const getRandomBackground = useCallback(async () => { + const getRandomBackground = async () => { let attempts = 0; const maxAttempts = backgroundImages.length; @@ -92,6 +89,7 @@ export function BackgroundSwitcher({ children }: { children: React.ReactNode }) if (newBackground !== background) { // Try to preload the image await preloadImage(newBackground); + console.log("Successfully loaded:", newBackground); return newBackground; } } catch (error) { @@ -102,38 +100,28 @@ export function BackgroundSwitcher({ children }: { children: React.ReactNode }) // If all attempts fail, return the first image as fallback return backgroundImages[0]; - }, [background, preloadImage]); + }; useEffect(() => { - let isMounted = true; - const initBackground = async () => { try { const newBg = await getRandomBackground(); - if (isMounted) { - setBackground(newBg); - setImageError(false); - } + setBackground(newBg); + setImageError(false); } catch (error) { console.error("Error setting initial background:", error); - if (isMounted) { - setImageError(true); - } + setImageError(true); } }; initBackground(); + }, []); - // Cleanup function to prevent state updates after unmount - return () => { - isMounted = false; - }; - }, [getRandomBackground]); - - const handleClick = useCallback(async (e: React.MouseEvent) => { + const handleClick = async (e: React.MouseEvent) => { if (e.target === e.currentTarget) { try { const newBg = await getRandomBackground(); + console.log("Changing background to:", newBg); setBackground(newBg); setImageError(false); } catch (error) { @@ -141,7 +129,7 @@ export function BackgroundSwitcher({ children }: { children: React.ReactNode }) setImageError(true); } } - }, [getRandomBackground]); + }; return (
{ - if (isSignInPage) return {}; - - return { - backgroundImage: `url('${currentBackground}')`, - backgroundSize: 'cover', - backgroundPosition: 'center', - backgroundRepeat: 'no-repeat', - backgroundAttachment: 'fixed', - cursor: 'pointer', - transition: 'background-image 0.5s ease-in-out' - }; - }, [currentBackground, isSignInPage]); - return ( {!isSignInPage && isAuthenticated && }
{children}
@@ -50,4 +39,4 @@ export const LayoutWrapper = memo(function LayoutWrapper({ ); -}); \ No newline at end of file +} \ No newline at end of file diff --git a/components/main-nav.tsx b/components/main-nav.tsx index f2766bbe..09bdc03c 100644 --- a/components/main-nav.tsx +++ b/components/main-nav.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useCallback, useMemo, memo } from "react"; +import { useState } from "react"; import { Calendar, MessageSquare, @@ -38,7 +38,6 @@ import { format } from 'date-fns'; import { fr } from 'date-fns/locale'; import { NotificationBadge } from './notification-badge'; import { NotesDialog } from './notes-dialog'; -import { useUnreadAnnouncements } from '@/hooks/use-unread-announcements'; const requestNotificationPermission = async () => { try { @@ -50,18 +49,17 @@ const requestNotificationPermission = async () => { } }; -// Use React.memo to memoize the entire component -export const MainNav = memo(function MainNav() { +export function MainNav() { const [isSidebarOpen, setIsSidebarOpen] = useState(false); const { data: session, status } = useSession(); const [userStatus, setUserStatus] = useState<'online' | 'busy' | 'away'>('online'); const [isNotesDialogOpen, setIsNotesDialogOpen] = useState(false); - // Use the unread announcements hook with memo to prevent unnecessary re-renders - const { hasUnread } = useUnreadAnnouncements(); + console.log("Session:", session); + console.log("Status:", status); - // Updated function to get user initials - memoize this - const getUserInitials = useCallback(() => { + // Updated function to get user initials + const getUserInitials = () => { if (session?.user?.name) { // Split the full name and get initials const names = session.user.name.split(' '); @@ -72,15 +70,15 @@ export const MainNav = memo(function MainNav() { return names[0].slice(0, 2).toUpperCase(); } return "?"; - }, [session?.user?.name]); + }; - // Function to get display name - memoize this - const getDisplayName = useCallback(() => { + // Function to get display name + const getDisplayName = () => { return session?.user?.name || "User"; - }, [session?.user?.name]); + }; - // Function to get user role - memoize this - const getUserRole = useCallback(() => { + // Function to get user role + const getUserRole = () => { if (session?.user?.role) { if (Array.isArray(session.user.role)) { // Filter out technical roles and format remaining ones @@ -106,15 +104,17 @@ export const MainNav = memo(function MainNav() { return session.user.role; } return ""; - }, [session?.user?.role]); + }; - // Function to check if user has a specific role - memoize this - const hasRole = useCallback((requiredRoles: string[]) => { + // Function to check if user has a specific role + const hasRole = (requiredRoles: string[]) => { if (!session?.user?.role) { + console.log('No user roles found'); return false; } const userRoles = Array.isArray(session.user.role) ? session.user.role : [session.user.role]; + console.log('Raw user roles:', userRoles); // Clean up user roles by removing prefixes and converting to lowercase const cleanUserRoles = userRoles.map(role => @@ -122,18 +122,21 @@ export const MainNav = memo(function MainNav() { .replace(/^ROLE_/, '') // Remove ROLE_ prefix .toLowerCase() ); + console.log('Clean user roles:', cleanUserRoles); // Clean required roles const cleanRequiredRoles = requiredRoles.map(role => role.toLowerCase()); + console.log('Clean required roles:', cleanRequiredRoles); // Check if user has any of the required roles const hasAnyRole = cleanRequiredRoles.some(role => cleanUserRoles.includes(role)); + console.log('Has any role:', hasAnyRole); return hasAnyRole; - }, [session?.user?.role]); + }; // Status configurations - const statusConfig = useMemo(() => ({ + const statusConfig = { online: { color: 'text-green-500', label: 'Online', @@ -149,10 +152,10 @@ export const MainNav = memo(function MainNav() { label: 'Away', notifications: false }, - }), []); + }; // Handle status change - const handleStatusChange = useCallback(async (newStatus: 'online' | 'busy' | 'away') => { + const handleStatusChange = async (newStatus: 'online' | 'busy' | 'away') => { setUserStatus(newStatus); if (newStatus !== 'online') { @@ -174,19 +177,19 @@ export const MainNav = memo(function MainNav() { // Re-enable notifications if going back online requestNotificationPermission(); } - }, []); + }; // Base menu items (available for everyone) - const baseMenuItems = useMemo(() => [ + const baseMenuItems = [ { title: "QG", icon: Target, href: '/qg', }, - ], []); + ]; // Role-specific menu items - const roleSpecificItems = useMemo(() => [ + const roleSpecificItems = [ { title: "ShowCase", icon: Lightbulb, @@ -205,64 +208,18 @@ export const MainNav = memo(function MainNav() { href: '/the-message', requiredRoles: ["mediation", "expression"], }, - ], []); + ]; // Get visible menu items based on user roles - const visibleMenuItems = useMemo(() => [ + const visibleMenuItems = [ ...baseMenuItems, ...roleSpecificItems.filter(item => hasRole(item.requiredRoles)) - ], [baseMenuItems, roleSpecificItems, hasRole]); + ]; // Format current date and time - const dateTimeDisplay = useMemo(() => { - const now = new Date(); - const formattedDate = format(now, "d MMMM yyyy", { locale: fr }); - const formattedTime = format(now, "HH:mm"); - return { formattedDate, formattedTime }; - }, []); - - // Handle sidebar toggle - const toggleSidebar = useCallback(() => { - setIsSidebarOpen(prev => !prev); - }, []); - - // Handle notes dialog - const toggleNotesDialog = useCallback(() => { - setIsNotesDialogOpen(prev => !prev); - }, []); - - // Handle logout - const handleLogout = useCallback(async () => { - try { - // First sign out from NextAuth - await signOut({ - callbackUrl: '/signin', - redirect: false - }); - - // Then redirect to Keycloak logout with proper parameters - const keycloakLogoutUrl = new URL( - `${process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER}/protocol/openid-connect/logout` - ); - - // Add required parameters - keycloakLogoutUrl.searchParams.append( - 'post_logout_redirect_uri', - window.location.origin - ); - keycloakLogoutUrl.searchParams.append( - 'id_token_hint', - session?.accessToken || '' - ); - - // Redirect to Keycloak logout - window.location.href = keycloakLogoutUrl.toString(); - } catch (error) { - console.error('Error during logout:', error); - // Fallback to simple redirect if something goes wrong - window.location.href = '/signin'; - } - }, [session?.accessToken]); + const now = new Date(); + const formattedDate = format(now, "d MMMM yyyy", { locale: fr }); + const formattedTime = format(now, "HH:mm"); return ( <> @@ -271,7 +228,7 @@ export const MainNav = memo(function MainNav() { {/* Left side */}
@@ -328,8 +282,8 @@ export const MainNav = memo(function MainNav() {
{/* Date and Time with smaller text */}
- {dateTimeDisplay.formattedDate} - {dateTimeDisplay.formattedTime} + {formattedDate} + {formattedTime}
@@ -391,7 +345,37 @@ export const MainNav = memo(function MainNav() { ))} { + try { + // First sign out from NextAuth + await signOut({ + callbackUrl: '/signin', + redirect: false + }); + + // Then redirect to Keycloak logout with proper parameters + const keycloakLogoutUrl = new URL( + `${process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER}/protocol/openid-connect/logout` + ); + + // Add required parameters + keycloakLogoutUrl.searchParams.append( + 'post_logout_redirect_uri', + window.location.origin + ); + keycloakLogoutUrl.searchParams.append( + 'id_token_hint', + session?.accessToken || '' + ); + + // Redirect to Keycloak logout + window.location.href = keycloakLogoutUrl.toString(); + } catch (error) { + console.error('Error during logout:', error); + // Fallback to simple redirect if something goes wrong + window.location.href = '/signin'; + } + }} > Déconnexion @@ -415,4 +399,4 @@ export const MainNav = memo(function MainNav() { /> ); -}); +} diff --git a/components/sidebar.tsx b/components/sidebar.tsx index 283b09c0..cb09e5ff 100644 --- a/components/sidebar.tsx +++ b/components/sidebar.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { memo, useCallback, useMemo } from "react"; +import type React from "react"; import { useState } from "react"; import { cn } from "@/lib/utils"; @@ -44,8 +44,7 @@ interface MenuItem { requiredRole?: string | string[]; } -// Memoize the entire Sidebar component to prevent unnecessary re-renders -export const Sidebar = memo(function Sidebar({ isOpen, onClose }: SidebarProps) { +export function Sidebar({ isOpen, onClose }: SidebarProps) { const { data: session, status } = useSession(); const router = useRouter(); const pathname = usePathname(); @@ -60,8 +59,8 @@ export const Sidebar = memo(function Sidebar({ isOpen, onClose }: SidebarProps) return null; } - // Function to check if user has a specific role - memoize this logic - const hasRole = useCallback((requiredRole: string | string[] | undefined) => { + // Function to check if user has a specific role + const hasRole = (requiredRole: string | string[] | undefined) => { // If no role is required, allow access if (!requiredRole) { return true; @@ -108,10 +107,10 @@ export const Sidebar = memo(function Sidebar({ isOpen, onClose }: SidebarProps) } return false; - }, [session]); + }; // Base menu items (available for everyone) - const baseMenuItems: MenuItem[] = useMemo(() => [ + const baseMenuItems: MenuItem[] = [ { title: "Pages", icon: FileText, @@ -159,10 +158,10 @@ export const Sidebar = memo(function Sidebar({ isOpen, onClose }: SidebarProps) href: "/agilite", iframe: process.env.NEXT_PUBLIC_IFRAME_AGILITY_URL, }, - ], []); + ]; // Role-specific menu items - const roleSpecificItems: MenuItem[] = useMemo(() => [ + const roleSpecificItems: MenuItem[] = [ { title: "Artlab", icon: Palette, @@ -191,45 +190,25 @@ export const Sidebar = memo(function Sidebar({ isOpen, onClose }: SidebarProps) iframe: process.env.NEXT_PUBLIC_IFRAME_MEDIATIONS_URL, requiredRole: "mediation", }, - ], []); + ]; // Combine base items with role-specific items based on user roles - const visibleMenuItems = useMemo(() => [ + const visibleMenuItems = [ ...baseMenuItems, - ...roleSpecificItems.filter(item => hasRole(item.requiredRole)) - ], [baseMenuItems, roleSpecificItems, hasRole]); + ...roleSpecificItems.filter(item => { + const isVisible = hasRole(item.requiredRole); + return isVisible; + }) + ]; - const handleNavigation = useCallback((href: string, external?: boolean) => { + const handleNavigation = (href: string, external?: boolean) => { if (external && href) { window.open(href, "_blank"); } else { router.push(href); } onClose(); - }, [router, onClose]); - - // Memoize the menu render to prevent unnecessary recalculation - const renderMenu = useMemo(() => ( -
- {visibleMenuItems.map((item, index) => ( -
- -
- ))} -
- ), [visibleMenuItems, pathname, handleNavigation]); + }; return ( <> @@ -268,15 +247,25 @@ export const Sidebar = memo(function Sidebar({ isOpen, onClose }: SidebarProps) />
- {/* Menu Items - Use memoized menu */} - {renderMenu} - - {/* Calendar Navigation */} -
- + {/* Menu Items */} +
+ {visibleMenuItems.map((item) => ( + + ))}
); -}); +} diff --git a/components/ui/scroll-area.tsx b/components/ui/scroll-area.tsx index c668a260..0b4a48d8 100644 --- a/components/ui/scroll-area.tsx +++ b/components/ui/scroll-area.tsx @@ -5,8 +5,25 @@ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" import { cn } from "@/lib/utils" -// Memoize ScrollBar component to prevent unnecessary re-renders -const ScrollBar = React.memo(React.forwardRef< +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)) +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName + +const ScrollBar = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, orientation = "vertical", ...props }, ref) => ( @@ -25,26 +42,7 @@ const ScrollBar = React.memo(React.forwardRef< > -))) +)) ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName -// Memoize ScrollArea component to prevent unnecessary re-renders -const ScrollArea = React.memo(React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - {children} - - - - -))) -ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName - export { ScrollArea, ScrollBar } diff --git a/hooks/use-local-storage.ts b/hooks/use-local-storage.ts deleted file mode 100644 index 320bf14b..00000000 --- a/hooks/use-local-storage.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { useState, useEffect } from 'react'; - -export function useLocalStorage(key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] { - // State to store our value - // Pass initial state function to useState so logic is only executed once - const [storedValue, setStoredValue] = useState(() => { - if (typeof window === 'undefined') { - return initialValue; - } - try { - // Get from local storage by key - const item = window.localStorage.getItem(key); - // Parse stored json or if none return initialValue - return item ? JSON.parse(item) : initialValue; - } catch (error) { - // If error also return initialValue - console.error('Error reading from localStorage:', error); - return initialValue; - } - }); - - // Return a wrapped version of useState's setter function that ... - // ... persists the new value to localStorage. - const setValue = (value: T | ((val: T) => T)) => { - try { - // Allow value to be a function so we have same API as useState - const valueToStore = - value instanceof Function ? value(storedValue) : value; - // Save state - setStoredValue(valueToStore); - // Save to local storage - if (typeof window !== 'undefined') { - window.localStorage.setItem(key, JSON.stringify(valueToStore)); - } - } catch (error) { - // A more advanced implementation would handle the error case - console.error('Error saving to localStorage:', error); - } - }; - - // Update stored value if the key changes - useEffect(() => { - if (typeof window === 'undefined') { - return; - } - try { - const item = window.localStorage.getItem(key); - setStoredValue(item ? JSON.parse(item) : initialValue); - } catch (error) { - console.error('Error updating from localStorage:', error); - setStoredValue(initialValue); - } - }, [key, initialValue]); - - return [storedValue, setValue]; -} \ No newline at end of file diff --git a/hooks/use-unread-announcements.ts b/hooks/use-unread-announcements.ts deleted file mode 100644 index e0e2780f..00000000 --- a/hooks/use-unread-announcements.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; -import { useLocalStorage } from '@/hooks/use-local-storage'; -import { Announcement } from '@/app/types/announcement'; - -type AnnouncementRead = { - [id: string]: boolean; -}; - -export function useUnreadAnnouncements() { - const [hasUnread, setHasUnread] = useState(false); - const [announcements, setAnnouncements] = useState([]); - const [readAnnouncements, setReadAnnouncements] = useLocalStorage('read-announcements', {}); - const [isLoading, setIsLoading] = useState(true); - - // Memoized function to check if there are unread announcements - const checkUnreadStatus = useCallback((anncs: Announcement[], readState: AnnouncementRead) => { - return anncs.some((announcement: Announcement) => !readState[announcement.id]); - }, []); - - // Fetch announcements and check if there are any unread - const checkForUnreadAnnouncements = useCallback(async () => { - try { - setIsLoading(true); - const response = await fetch('/api/announcements'); - - if (!response.ok) { - throw new Error('Failed to fetch announcements'); - } - - const data = await response.json(); - - // Check if there are any unread announcements with the current read state - const hasUnreadAnnouncements = checkUnreadStatus(data, readAnnouncements); - - // Update both states in a single render cycle - setAnnouncements(data); - setHasUnread(hasUnreadAnnouncements); - } catch (err) { - console.error('Error checking for unread announcements:', err); - } finally { - setIsLoading(false); - } - }, [readAnnouncements, checkUnreadStatus]); - - // Mark an announcement as read - const markAsRead = useCallback((announcementId: string) => { - setReadAnnouncements((prev: AnnouncementRead) => { - // Create new state object without mutating the original - const newReadState = { ...prev, [announcementId]: true }; - - // Schedule the unread status update for the next tick - // This prevents state updates from interfering with each other - setTimeout(() => { - setHasUnread(checkUnreadStatus(announcements, newReadState)); - }, 0); - - return newReadState; - }); - }, [announcements, checkUnreadStatus, setReadAnnouncements]); - - // Mark all announcements as read - const markAllAsRead = useCallback(() => { - const allRead: AnnouncementRead = {}; - announcements.forEach(announcement => { - allRead[announcement.id] = true; - }); - - setReadAnnouncements(allRead); - - // Schedule the unread status update for the next tick - setTimeout(() => { - setHasUnread(false); - }, 0); - }, [announcements, setReadAnnouncements]); - - // Check for unread announcements on mount or when dependencies change - useEffect(() => { - const timer = setTimeout(() => { - checkForUnreadAnnouncements(); - }, 0); - - return () => clearTimeout(timer); - }, [checkForUnreadAnnouncements]); - - return { - hasUnread, - isLoading, - announcements, - checkForUnreadAnnouncements, - markAsRead, - markAllAsRead - }; -} \ No newline at end of file