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";
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>

View File

@ -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>;
}

View File

@ -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

View File

@ -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>
);
}
});

View File

@ -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);

View File

@ -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