courrier
This commit is contained in:
parent
f7905e185d
commit
bf348135ff
@ -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<Announcement[]>([]);
|
||||
const [selectedAnnouncement, setSelectedAnnouncement] = useState<Announcement | null>(null);
|
||||
const [isViewDialogOpen, setIsViewDialogOpen] = useState(false);
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(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) {
|
||||
</Badge>
|
||||
);
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
|
||||
@ -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 <div>Chargement...</div>;
|
||||
}
|
||||
|
||||
@ -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<string> => {
|
||||
const preloadImage = useCallback((src: string): Promise<string> => {
|
||||
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 (
|
||||
<div
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { memo, useMemo } from 'react';
|
||||
import { MainNav } from "@/components/main-nav";
|
||||
import { Footer } from "@/components/footer";
|
||||
import { AuthCheck } from "@/components/auth/auth-check";
|
||||
@ -12,25 +13,35 @@ interface LayoutWrapperProps {
|
||||
isAuthenticated: boolean;
|
||||
}
|
||||
|
||||
export function LayoutWrapper({ children, isSignInPage, isAuthenticated }: LayoutWrapperProps) {
|
||||
// Use memo to prevent unnecessary rerenders of the entire layout
|
||||
export const LayoutWrapper = memo(function LayoutWrapper({
|
||||
children,
|
||||
isSignInPage,
|
||||
isAuthenticated
|
||||
}: LayoutWrapperProps) {
|
||||
const { currentBackground, changeBackground } = useBackgroundImage();
|
||||
|
||||
// Memoize the background style to prevent recalculation on each render
|
||||
const backgroundStyle = useMemo(() => {
|
||||
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 (
|
||||
<AuthCheck>
|
||||
{!isSignInPage && isAuthenticated && <MainNav />}
|
||||
<div
|
||||
className={isSignInPage ? "" : "min-h-screen"}
|
||||
style={
|
||||
!isSignInPage ? {
|
||||
backgroundImage: `url('${currentBackground}')`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundAttachment: 'fixed',
|
||||
cursor: 'pointer',
|
||||
transition: 'background-image 0.5s ease-in-out'
|
||||
} : {}
|
||||
}
|
||||
style={!isSignInPage ? backgroundStyle : {}}
|
||||
onClick={!isSignInPage ? changeBackground : undefined}
|
||||
>
|
||||
<main>{children}</main>
|
||||
@ -39,4 +50,4 @@ export function LayoutWrapper({ children, isSignInPage, isAuthenticated }: Layou
|
||||
<Toaster />
|
||||
</AuthCheck>
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -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);
|
||||
|
||||
@ -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<Announcement[]>([]);
|
||||
const [readAnnouncements, setReadAnnouncements] = useLocalStorage<AnnouncementRead>('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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user