From bf348135ff8708a68566e09b7e88949fb6320e1f Mon Sep 17 00:00:00 2001 From: alma Date: Sun, 4 May 2025 22:37:26 +0200 Subject: [PATCH] courrier --- .../announcement/announcements-list.tsx | 67 +++++++++---------- components/auth/auth-check.tsx | 14 +++- components/background-switcher.tsx | 50 ++++++++------ components/layout/layout-wrapper.tsx | 37 ++++++---- components/main-nav.tsx | 2 +- hooks/use-unread-announcements.ts | 59 ++++++++-------- 6 files changed, 126 insertions(+), 103 deletions(-) diff --git a/components/announcement/announcements-list.tsx b/components/announcement/announcements-list.tsx index 150398e8..5c898b97 100644 --- a/components/announcement/announcements-list.tsx +++ b/components/announcement/announcements-list.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback } from "react"; import { Table, TableBody, @@ -41,54 +41,43 @@ interface AnnouncementsListProps { } 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(); - const { 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); - } - }; + + // Use the updated hook which provides announcements data and loading state + const { + announcements, + isLoading: loading, + checkForUnreadAnnouncements: fetchAnnouncements, + markAsRead + } = useUnreadAnnouncements(); useEffect(() => { + // Use the fetch function from the hook fetchAnnouncements(); - }, []); + }, [fetchAnnouncements]); // Handle viewing an announcement - const handleViewAnnouncement = (announcement: Announcement) => { + const handleViewAnnouncement = useCallback((announcement: Announcement) => { setSelectedAnnouncement(announcement); setIsViewDialogOpen(true); - markAsRead(announcement.id); - }; + + // Use setTimeout to separate the state updates + setTimeout(() => { + markAsRead(announcement.id); + }, 0); + }, [markAsRead]); // Handle deleting an announcement - const handleDeleteClick = (announcement: Announcement) => { + const handleDeleteClick = useCallback((announcement: Announcement) => { setSelectedAnnouncement(announcement); setIsDeleteDialogOpen(true); - }; + }, []); - const confirmDelete = async () => { + const confirmDelete = useCallback(async () => { if (!selectedAnnouncement) return; try { @@ -100,10 +89,14 @@ export function AnnouncementsList({ userRole }: AnnouncementsListProps) { throw new Error('Failed to delete announcement'); } - // Update the local state - setAnnouncements(announcements.filter(a => a.id !== selectedAnnouncement.id)); + // Close dialog first setIsDeleteDialogOpen(false); + // Then refresh data using the hook + setTimeout(() => { + fetchAnnouncements(); + }, 0); + toast({ title: "Announcement deleted", description: "The announcement has been deleted successfully.", @@ -116,10 +109,10 @@ export function AnnouncementsList({ userRole }: AnnouncementsListProps) { variant: "destructive", }); } - }; + }, [selectedAnnouncement, fetchAnnouncements, toast]); // Format roles for display - const formatRoles = (roles: string[]) => { + const formatRoles = useCallback((roles: string[]) => { return roles.map(role => { const roleName = role === "all" ? "All Users" @@ -131,7 +124,7 @@ export function AnnouncementsList({ userRole }: AnnouncementsListProps) { ); }); - }; + }, []); return ( diff --git a/components/auth/auth-check.tsx b/components/auth/auth-check.tsx index c8e0c21e..c32f098c 100644 --- a/components/auth/auth-check.tsx +++ b/components/auth/auth-check.tsx @@ -2,19 +2,29 @@ import { useSession } from "next-auth/react"; import { usePathname, useRouter } from "next/navigation"; -import { useEffect } from "react"; +import { useEffect, useCallback } from "react"; export function AuthCheck({ children }: { children: React.ReactNode }) { const { data: session, status } = useSession(); const pathname = usePathname(); const router = useRouter(); - useEffect(() => { + // Memoize the redirect logic to prevent unnecessary re-renders + const handleAuthRedirect = useCallback(() => { 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 d67c170c..ff8e81e6 100644 --- a/components/background-switcher.tsx +++ b/components/background-switcher.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback } from "react"; const backgroundImages = [ "/background/Autumn birger-strahl-6YZgnYaPD5s-unsplash.jpeg", @@ -48,14 +48,17 @@ const backgroundImages = [ export function useBackgroundImage() { const [currentBackground, setCurrentBackground] = useState(backgroundImages[0]); - const changeBackground = () => { - const currentIndex = backgroundImages.indexOf(currentBackground); - const nextIndex = (currentIndex + 1) % backgroundImages.length; - setCurrentBackground(backgroundImages[nextIndex]); - }; + // Memoize the change background function + const changeBackground = useCallback(() => { + setCurrentBackground(prevBackground => { + const currentIndex = backgroundImages.indexOf(prevBackground); + const nextIndex = (currentIndex + 1) % backgroundImages.length; + return backgroundImages[nextIndex]; + }); + }, []); useEffect(() => { - // Set initial random background + // Set initial random background only once on mount const randomIndex = Math.floor(Math.random() * backgroundImages.length); setCurrentBackground(backgroundImages[randomIndex]); }, []); @@ -68,16 +71,16 @@ export function BackgroundSwitcher({ children }: { children: React.ReactNode }) const [imageError, setImageError] = useState(false); // Function to preload an image - const preloadImage = (src: string): Promise => { + const preloadImage = useCallback((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 = async () => { + const getRandomBackground = useCallback(async () => { let attempts = 0; const maxAttempts = backgroundImages.length; @@ -89,7 +92,6 @@ 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) { @@ -100,28 +102,38 @@ 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(); - setBackground(newBg); - setImageError(false); + if (isMounted) { + setBackground(newBg); + setImageError(false); + } } catch (error) { console.error("Error setting initial background:", error); - setImageError(true); + if (isMounted) { + setImageError(true); + } } }; initBackground(); - }, []); - const handleClick = async (e: React.MouseEvent) => { + // Cleanup function to prevent state updates after unmount + return () => { + isMounted = false; + }; + }, [getRandomBackground]); + + const handleClick = useCallback(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) { @@ -129,7 +141,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}
@@ -39,4 +50,4 @@ export function LayoutWrapper({ children, isSignInPage, isAuthenticated }: Layou ); -} \ 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 cecd8ca8..b32b73b5 100644 --- a/components/main-nav.tsx +++ b/components/main-nav.tsx @@ -56,7 +56,7 @@ export function MainNav() { const [userStatus, setUserStatus] = useState<'online' | 'busy' | 'away'>('online'); const [isNotesDialogOpen, setIsNotesDialogOpen] = useState(false); - // Use the unread announcements hook + // Use the unread announcements hook with memo to prevent unnecessary re-renders const { hasUnread } = useUnreadAnnouncements(); console.log("Session:", session); diff --git a/hooks/use-unread-announcements.ts b/hooks/use-unread-announcements.ts index 2655cf7a..e0e2780f 100644 --- a/hooks/use-unread-announcements.ts +++ b/hooks/use-unread-announcements.ts @@ -1,5 +1,5 @@ -import { useState, useEffect } from 'react'; -import { useLocalStorage } from './use-local-storage'; +import { useState, useEffect, useCallback } from 'react'; +import { useLocalStorage } from '@/hooks/use-local-storage'; import { Announcement } from '@/app/types/announcement'; type AnnouncementRead = { @@ -11,9 +11,14 @@ export function useUnreadAnnouncements() { 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 = async () => { + const checkForUnreadAnnouncements = useCallback(async () => { try { setIsLoading(true); const response = await fetch('/api/announcements'); @@ -23,45 +28,38 @@ export function useUnreadAnnouncements() { } 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); - - // Check if there are any unread announcements - const hasUnreadAnnouncements = data.some((announcement: Announcement) => { - return !readAnnouncements[announcement.id]; - }); - setHasUnread(hasUnreadAnnouncements); } catch (err) { console.error('Error checking for unread announcements:', err); } finally { setIsLoading(false); } - }; + }, [readAnnouncements, checkUnreadStatus]); // Mark an announcement as read - const markAsRead = (announcementId: string) => { + const markAsRead = useCallback((announcementId: string) => { setReadAnnouncements((prev: AnnouncementRead) => { - const newState = { - ...prev, - [announcementId]: true - }; + // Create new state object without mutating the original + const newReadState = { ...prev, [announcementId]: true }; - // Check if there are still any unread announcements using the updated state - const hasUnreadAnnouncements = announcements.some(announcement => { - return !newState[announcement.id]; - }); - - // Update the hasUnread state in the next tick to avoid the infinite loop + // Schedule the unread status update for the next tick + // This prevents state updates from interfering with each other setTimeout(() => { - setHasUnread(hasUnreadAnnouncements); + setHasUnread(checkUnreadStatus(announcements, newReadState)); }, 0); - return newState; + return newReadState; }); - }; + }, [announcements, checkUnreadStatus, setReadAnnouncements]); // Mark all announcements as read - const markAllAsRead = () => { + const markAllAsRead = useCallback(() => { const allRead: AnnouncementRead = {}; announcements.forEach(announcement => { allRead[announcement.id] = true; @@ -69,26 +67,25 @@ export function useUnreadAnnouncements() { setReadAnnouncements(allRead); - // Update the hasUnread state in the next tick to avoid the infinite loop + // Schedule the unread status update for the next tick setTimeout(() => { setHasUnread(false); }, 0); - }; + }, [announcements, setReadAnnouncements]); - // Check for unread announcements on mount + // Check for unread announcements on mount or when dependencies change useEffect(() => { - // Run the check in the next tick to avoid unnecessary re-renders const timer = setTimeout(() => { checkForUnreadAnnouncements(); }, 0); - // Cleanup timer on component unmount return () => clearTimeout(timer); - }, []); + }, [checkForUnreadAnnouncements]); return { hasUnread, isLoading, + announcements, checkForUnreadAnnouncements, markAsRead, markAllAsRead