This commit is contained in:
alma 2025-05-04 22:37:26 +02:00
parent f7905e185d
commit bf348135ff
6 changed files with 126 additions and 103 deletions

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect, useCallback } from "react";
import { import {
Table, Table,
TableBody, TableBody,
@ -41,54 +41,43 @@ interface AnnouncementsListProps {
} }
export function AnnouncementsList({ userRole }: AnnouncementsListProps) { export function AnnouncementsList({ userRole }: AnnouncementsListProps) {
const [announcements, setAnnouncements] = useState<Announcement[]>([]);
const [selectedAnnouncement, setSelectedAnnouncement] = useState<Announcement | null>(null); const [selectedAnnouncement, setSelectedAnnouncement] = useState<Announcement | null>(null);
const [isViewDialogOpen, setIsViewDialogOpen] = useState(false); const [isViewDialogOpen, setIsViewDialogOpen] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const { toast } = useToast(); const { toast } = useToast();
const { markAsRead } = useUnreadAnnouncements();
// Use the updated hook which provides announcements data and loading state
// Fetch announcements const {
const fetchAnnouncements = async () => { announcements,
try { isLoading: loading,
setLoading(true); checkForUnreadAnnouncements: fetchAnnouncements,
const response = await fetch('/api/announcements'); markAsRead
} = useUnreadAnnouncements();
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(() => { useEffect(() => {
// Use the fetch function from the hook
fetchAnnouncements(); fetchAnnouncements();
}, []); }, [fetchAnnouncements]);
// Handle viewing an announcement // Handle viewing an announcement
const handleViewAnnouncement = (announcement: Announcement) => { const handleViewAnnouncement = useCallback((announcement: Announcement) => {
setSelectedAnnouncement(announcement); setSelectedAnnouncement(announcement);
setIsViewDialogOpen(true); setIsViewDialogOpen(true);
markAsRead(announcement.id);
}; // Use setTimeout to separate the state updates
setTimeout(() => {
markAsRead(announcement.id);
}, 0);
}, [markAsRead]);
// Handle deleting an announcement // Handle deleting an announcement
const handleDeleteClick = (announcement: Announcement) => { const handleDeleteClick = useCallback((announcement: Announcement) => {
setSelectedAnnouncement(announcement); setSelectedAnnouncement(announcement);
setIsDeleteDialogOpen(true); setIsDeleteDialogOpen(true);
}; }, []);
const confirmDelete = async () => { const confirmDelete = useCallback(async () => {
if (!selectedAnnouncement) return; if (!selectedAnnouncement) return;
try { try {
@ -100,10 +89,14 @@ export function AnnouncementsList({ userRole }: AnnouncementsListProps) {
throw new Error('Failed to delete announcement'); throw new Error('Failed to delete announcement');
} }
// Update the local state // Close dialog first
setAnnouncements(announcements.filter(a => a.id !== selectedAnnouncement.id));
setIsDeleteDialogOpen(false); setIsDeleteDialogOpen(false);
// Then refresh data using the hook
setTimeout(() => {
fetchAnnouncements();
}, 0);
toast({ toast({
title: "Announcement deleted", title: "Announcement deleted",
description: "The announcement has been deleted successfully.", description: "The announcement has been deleted successfully.",
@ -116,10 +109,10 @@ export function AnnouncementsList({ userRole }: AnnouncementsListProps) {
variant: "destructive", variant: "destructive",
}); });
} }
}; }, [selectedAnnouncement, fetchAnnouncements, toast]);
// Format roles for display // Format roles for display
const formatRoles = (roles: string[]) => { const formatRoles = useCallback((roles: string[]) => {
return roles.map(role => { return roles.map(role => {
const roleName = role === "all" const roleName = role === "all"
? "All Users" ? "All Users"
@ -131,7 +124,7 @@ export function AnnouncementsList({ userRole }: AnnouncementsListProps) {
</Badge> </Badge>
); );
}); });
}; }, []);
return ( return (
<Card> <Card>

View File

@ -2,19 +2,29 @@
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { usePathname, useRouter } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
import { useEffect } from "react"; import { useEffect, useCallback } from "react";
export function AuthCheck({ children }: { children: React.ReactNode }) { export function AuthCheck({ children }: { children: React.ReactNode }) {
const { data: session, status } = useSession(); const { data: session, status } = useSession();
const pathname = usePathname(); const pathname = usePathname();
const router = useRouter(); const router = useRouter();
useEffect(() => { // Memoize the redirect logic to prevent unnecessary re-renders
const handleAuthRedirect = useCallback(() => {
if (status === "unauthenticated" && pathname !== "/signin") { if (status === "unauthenticated" && pathname !== "/signin") {
router.push("/signin"); router.push("/signin");
} }
}, [status, router, pathname]); }, [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") { if (status === "loading") {
return <div>Chargement...</div>; return <div>Chargement...</div>;
} }

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect, useCallback } from "react";
const backgroundImages = [ const backgroundImages = [
"/background/Autumn birger-strahl-6YZgnYaPD5s-unsplash.jpeg", "/background/Autumn birger-strahl-6YZgnYaPD5s-unsplash.jpeg",
@ -48,14 +48,17 @@ const backgroundImages = [
export function useBackgroundImage() { export function useBackgroundImage() {
const [currentBackground, setCurrentBackground] = useState(backgroundImages[0]); const [currentBackground, setCurrentBackground] = useState(backgroundImages[0]);
const changeBackground = () => { // Memoize the change background function
const currentIndex = backgroundImages.indexOf(currentBackground); const changeBackground = useCallback(() => {
const nextIndex = (currentIndex + 1) % backgroundImages.length; setCurrentBackground(prevBackground => {
setCurrentBackground(backgroundImages[nextIndex]); const currentIndex = backgroundImages.indexOf(prevBackground);
}; const nextIndex = (currentIndex + 1) % backgroundImages.length;
return backgroundImages[nextIndex];
});
}, []);
useEffect(() => { useEffect(() => {
// Set initial random background // Set initial random background only once on mount
const randomIndex = Math.floor(Math.random() * backgroundImages.length); const randomIndex = Math.floor(Math.random() * backgroundImages.length);
setCurrentBackground(backgroundImages[randomIndex]); setCurrentBackground(backgroundImages[randomIndex]);
}, []); }, []);
@ -68,16 +71,16 @@ export function BackgroundSwitcher({ children }: { children: React.ReactNode })
const [imageError, setImageError] = useState(false); const [imageError, setImageError] = useState(false);
// Function to preload an image // Function to preload an image
const preloadImage = (src: string): Promise<string> => { const preloadImage = useCallback((src: string): Promise<string> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const img = new Image(); const img = new Image();
img.src = src; img.src = src;
img.onload = () => resolve(src); img.onload = () => resolve(src);
img.onerror = () => reject(new Error(`Failed to load image: ${src}`)); img.onerror = () => reject(new Error(`Failed to load image: ${src}`));
}); });
}; }, []);
const getRandomBackground = async () => { const getRandomBackground = useCallback(async () => {
let attempts = 0; let attempts = 0;
const maxAttempts = backgroundImages.length; const maxAttempts = backgroundImages.length;
@ -89,7 +92,6 @@ export function BackgroundSwitcher({ children }: { children: React.ReactNode })
if (newBackground !== background) { if (newBackground !== background) {
// Try to preload the image // Try to preload the image
await preloadImage(newBackground); await preloadImage(newBackground);
console.log("Successfully loaded:", newBackground);
return newBackground; return newBackground;
} }
} catch (error) { } catch (error) {
@ -100,28 +102,38 @@ export function BackgroundSwitcher({ children }: { children: React.ReactNode })
// If all attempts fail, return the first image as fallback // If all attempts fail, return the first image as fallback
return backgroundImages[0]; return backgroundImages[0];
}; }, [background, preloadImage]);
useEffect(() => { useEffect(() => {
let isMounted = true;
const initBackground = async () => { const initBackground = async () => {
try { try {
const newBg = await getRandomBackground(); const newBg = await getRandomBackground();
setBackground(newBg); if (isMounted) {
setImageError(false); setBackground(newBg);
setImageError(false);
}
} catch (error) { } catch (error) {
console.error("Error setting initial background:", error); console.error("Error setting initial background:", error);
setImageError(true); if (isMounted) {
setImageError(true);
}
} }
}; };
initBackground(); 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) { if (e.target === e.currentTarget) {
try { try {
const newBg = await getRandomBackground(); const newBg = await getRandomBackground();
console.log("Changing background to:", newBg);
setBackground(newBg); setBackground(newBg);
setImageError(false); setImageError(false);
} catch (error) { } catch (error) {
@ -129,7 +141,7 @@ export function BackgroundSwitcher({ children }: { children: React.ReactNode })
setImageError(true); setImageError(true);
} }
} }
}; }, [getRandomBackground]);
return ( return (
<div <div

View File

@ -1,5 +1,6 @@
"use client"; "use client";
import { memo, useMemo } from 'react';
import { MainNav } from "@/components/main-nav"; import { MainNav } from "@/components/main-nav";
import { Footer } from "@/components/footer"; import { Footer } from "@/components/footer";
import { AuthCheck } from "@/components/auth/auth-check"; import { AuthCheck } from "@/components/auth/auth-check";
@ -12,25 +13,35 @@ interface LayoutWrapperProps {
isAuthenticated: boolean; 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(); 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 ( return (
<AuthCheck> <AuthCheck>
{!isSignInPage && isAuthenticated && <MainNav />} {!isSignInPage && isAuthenticated && <MainNav />}
<div <div
className={isSignInPage ? "" : "min-h-screen"} className={isSignInPage ? "" : "min-h-screen"}
style={ style={!isSignInPage ? backgroundStyle : {}}
!isSignInPage ? {
backgroundImage: `url('${currentBackground}')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
backgroundAttachment: 'fixed',
cursor: 'pointer',
transition: 'background-image 0.5s ease-in-out'
} : {}
}
onClick={!isSignInPage ? changeBackground : undefined} onClick={!isSignInPage ? changeBackground : undefined}
> >
<main>{children}</main> <main>{children}</main>
@ -39,4 +50,4 @@ export function LayoutWrapper({ children, isSignInPage, isAuthenticated }: Layou
<Toaster /> <Toaster />
</AuthCheck> </AuthCheck>
); );
} });

View File

@ -56,7 +56,7 @@ export function MainNav() {
const [userStatus, setUserStatus] = useState<'online' | 'busy' | 'away'>('online'); const [userStatus, setUserStatus] = useState<'online' | 'busy' | 'away'>('online');
const [isNotesDialogOpen, setIsNotesDialogOpen] = useState(false); 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(); const { hasUnread } = useUnreadAnnouncements();
console.log("Session:", session); console.log("Session:", session);

View File

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { useLocalStorage } from './use-local-storage'; import { useLocalStorage } from '@/hooks/use-local-storage';
import { Announcement } from '@/app/types/announcement'; import { Announcement } from '@/app/types/announcement';
type AnnouncementRead = { type AnnouncementRead = {
@ -11,9 +11,14 @@ export function useUnreadAnnouncements() {
const [announcements, setAnnouncements] = useState<Announcement[]>([]); const [announcements, setAnnouncements] = useState<Announcement[]>([]);
const [readAnnouncements, setReadAnnouncements] = useLocalStorage<AnnouncementRead>('read-announcements', {}); const [readAnnouncements, setReadAnnouncements] = useLocalStorage<AnnouncementRead>('read-announcements', {});
const [isLoading, setIsLoading] = useState(true); 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 // Fetch announcements and check if there are any unread
const checkForUnreadAnnouncements = async () => { const checkForUnreadAnnouncements = useCallback(async () => {
try { try {
setIsLoading(true); setIsLoading(true);
const response = await fetch('/api/announcements'); const response = await fetch('/api/announcements');
@ -23,45 +28,38 @@ export function useUnreadAnnouncements() {
} }
const data = await response.json(); 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); setAnnouncements(data);
// Check if there are any unread announcements
const hasUnreadAnnouncements = data.some((announcement: Announcement) => {
return !readAnnouncements[announcement.id];
});
setHasUnread(hasUnreadAnnouncements); setHasUnread(hasUnreadAnnouncements);
} catch (err) { } catch (err) {
console.error('Error checking for unread announcements:', err); console.error('Error checking for unread announcements:', err);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}; }, [readAnnouncements, checkUnreadStatus]);
// Mark an announcement as read // Mark an announcement as read
const markAsRead = (announcementId: string) => { const markAsRead = useCallback((announcementId: string) => {
setReadAnnouncements((prev: AnnouncementRead) => { setReadAnnouncements((prev: AnnouncementRead) => {
const newState = { // Create new state object without mutating the original
...prev, const newReadState = { ...prev, [announcementId]: true };
[announcementId]: true
};
// Check if there are still any unread announcements using the updated state // Schedule the unread status update for the next tick
const hasUnreadAnnouncements = announcements.some(announcement => { // This prevents state updates from interfering with each other
return !newState[announcement.id];
});
// Update the hasUnread state in the next tick to avoid the infinite loop
setTimeout(() => { setTimeout(() => {
setHasUnread(hasUnreadAnnouncements); setHasUnread(checkUnreadStatus(announcements, newReadState));
}, 0); }, 0);
return newState; return newReadState;
}); });
}; }, [announcements, checkUnreadStatus, setReadAnnouncements]);
// Mark all announcements as read // Mark all announcements as read
const markAllAsRead = () => { const markAllAsRead = useCallback(() => {
const allRead: AnnouncementRead = {}; const allRead: AnnouncementRead = {};
announcements.forEach(announcement => { announcements.forEach(announcement => {
allRead[announcement.id] = true; allRead[announcement.id] = true;
@ -69,26 +67,25 @@ export function useUnreadAnnouncements() {
setReadAnnouncements(allRead); 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(() => { setTimeout(() => {
setHasUnread(false); setHasUnread(false);
}, 0); }, 0);
}; }, [announcements, setReadAnnouncements]);
// Check for unread announcements on mount // Check for unread announcements on mount or when dependencies change
useEffect(() => { useEffect(() => {
// Run the check in the next tick to avoid unnecessary re-renders
const timer = setTimeout(() => { const timer = setTimeout(() => {
checkForUnreadAnnouncements(); checkForUnreadAnnouncements();
}, 0); }, 0);
// Cleanup timer on component unmount
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, []); }, [checkForUnreadAnnouncements]);
return { return {
hasUnread, hasUnread,
isLoading, isLoading,
announcements,
checkForUnreadAnnouncements, checkForUnreadAnnouncements,
markAsRead, markAsRead,
markAllAsRead markAllAsRead