courrier
This commit is contained in:
parent
0ad05e08ce
commit
838a06f215
@ -219,11 +219,11 @@ export function AnnouncementForm({ userRole }: AnnouncementFormProps) {
|
|||||||
{availableRoles.map(role => (
|
{availableRoles.map(role => (
|
||||||
<Badge
|
<Badge
|
||||||
key={role.id}
|
key={role.id}
|
||||||
variant={selectedRoles.includes(role.id) ? "secondary" : "outline"}
|
variant={selectedRoles.includes(role.id) ? "default" : "outline"}
|
||||||
className={`cursor-pointer px-3 py-1 ${
|
className={`cursor-pointer px-3 py-1 ${
|
||||||
selectedRoles.includes(role.id)
|
selectedRoles.includes(role.id)
|
||||||
? "bg-blue-600 hover:bg-blue-700 text-white"
|
? "bg-blue-600 hover:bg-blue-700"
|
||||||
: "bg-white hover:bg-gray-100 text-gray-700 border-gray-200"
|
: "hover:bg-gray-100"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleRoleToggle(role.id)}
|
onClick={() => handleRoleToggle(role.id)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@ -34,50 +34,58 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Announcement } from "@/app/types/announcement";
|
import { Announcement } from "@/app/types/announcement";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
import { useUnreadAnnouncements } from '@/hooks/use-unread-announcements';
|
|
||||||
|
|
||||||
interface AnnouncementsListProps {
|
interface AnnouncementsListProps {
|
||||||
userRole: string[];
|
userRole: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
|
|
||||||
// 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
|
|
||||||
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;
|
if (!selectedAnnouncement) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -89,14 +97,10 @@ export function AnnouncementsList({ userRole }: AnnouncementsListProps) {
|
|||||||
throw new Error('Failed to delete announcement');
|
throw new Error('Failed to delete announcement');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close dialog first
|
// Update the local state
|
||||||
|
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.",
|
||||||
@ -109,22 +113,22 @@ export function AnnouncementsList({ userRole }: AnnouncementsListProps) {
|
|||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [selectedAnnouncement, fetchAnnouncements, toast]);
|
};
|
||||||
|
|
||||||
// Format roles for display
|
// Format roles for display
|
||||||
const formatRoles = useCallback((roles: string[]) => {
|
const formatRoles = (roles: string[]) => {
|
||||||
return roles.map(role => {
|
return roles.map(role => {
|
||||||
const roleName = role === "all"
|
const roleName = role === "all"
|
||||||
? "All Users"
|
? "All Users"
|
||||||
: role.charAt(0).toUpperCase() + role.slice(1);
|
: role.charAt(0).toUpperCase() + role.slice(1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge key={role} variant="secondary" className="mr-1 bg-gray-100 hover:bg-gray-200 text-gray-800 border-gray-200">
|
<Badge key={role} variant="outline" className="mr-1">
|
||||||
{roleName}
|
{roleName}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
@ -178,7 +182,6 @@ export function AnnouncementsList({ userRole }: AnnouncementsListProps) {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleViewAnnouncement(announcement)}
|
onClick={() => handleViewAnnouncement(announcement)}
|
||||||
className="bg-white hover:bg-gray-50 border-gray-200 text-gray-700"
|
|
||||||
>
|
>
|
||||||
<Eye className="h-4 w-4" />
|
<Eye className="h-4 w-4" />
|
||||||
<span className="sr-only">View</span>
|
<span className="sr-only">View</span>
|
||||||
@ -187,7 +190,6 @@ export function AnnouncementsList({ userRole }: AnnouncementsListProps) {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleDeleteClick(announcement)}
|
onClick={() => handleDeleteClick(announcement)}
|
||||||
className="bg-white hover:bg-gray-50 border-gray-200 text-gray-700"
|
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
<span className="sr-only">Delete</span>
|
<span className="sr-only">Delete</span>
|
||||||
|
|||||||
@ -2,29 +2,19 @@
|
|||||||
|
|
||||||
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, useCallback } from "react";
|
import { useEffect } 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();
|
||||||
|
|
||||||
// Memoize the redirect logic to prevent unnecessary re-renders
|
useEffect(() => {
|
||||||
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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
const backgroundImages = [
|
const backgroundImages = [
|
||||||
"/background/Autumn birger-strahl-6YZgnYaPD5s-unsplash.jpeg",
|
"/background/Autumn birger-strahl-6YZgnYaPD5s-unsplash.jpeg",
|
||||||
@ -48,17 +48,14 @@ const backgroundImages = [
|
|||||||
export function useBackgroundImage() {
|
export function useBackgroundImage() {
|
||||||
const [currentBackground, setCurrentBackground] = useState(backgroundImages[0]);
|
const [currentBackground, setCurrentBackground] = useState(backgroundImages[0]);
|
||||||
|
|
||||||
// Memoize the change background function
|
const changeBackground = () => {
|
||||||
const changeBackground = useCallback(() => {
|
const currentIndex = backgroundImages.indexOf(currentBackground);
|
||||||
setCurrentBackground(prevBackground => {
|
const nextIndex = (currentIndex + 1) % backgroundImages.length;
|
||||||
const currentIndex = backgroundImages.indexOf(prevBackground);
|
setCurrentBackground(backgroundImages[nextIndex]);
|
||||||
const nextIndex = (currentIndex + 1) % backgroundImages.length;
|
};
|
||||||
return backgroundImages[nextIndex];
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Set initial random background only once on mount
|
// Set initial random background
|
||||||
const randomIndex = Math.floor(Math.random() * backgroundImages.length);
|
const randomIndex = Math.floor(Math.random() * backgroundImages.length);
|
||||||
setCurrentBackground(backgroundImages[randomIndex]);
|
setCurrentBackground(backgroundImages[randomIndex]);
|
||||||
}, []);
|
}, []);
|
||||||
@ -71,16 +68,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 = useCallback((src: string): Promise<string> => {
|
const preloadImage = (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 = useCallback(async () => {
|
const getRandomBackground = async () => {
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
const maxAttempts = backgroundImages.length;
|
const maxAttempts = backgroundImages.length;
|
||||||
|
|
||||||
@ -92,6 +89,7 @@ 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) {
|
||||||
@ -102,38 +100,28 @@ 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();
|
||||||
if (isMounted) {
|
setBackground(newBg);
|
||||||
setBackground(newBg);
|
setImageError(false);
|
||||||
setImageError(false);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error setting initial background:", error);
|
console.error("Error setting initial background:", error);
|
||||||
if (isMounted) {
|
setImageError(true);
|
||||||
setImageError(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
initBackground();
|
initBackground();
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Cleanup function to prevent state updates after unmount
|
const handleClick = async (e: React.MouseEvent) => {
|
||||||
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) {
|
||||||
@ -141,7 +129,7 @@ export function BackgroundSwitcher({ children }: { children: React.ReactNode })
|
|||||||
setImageError(true);
|
setImageError(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [getRandomBackground]);
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
"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";
|
||||||
@ -13,35 +12,25 @@ interface LayoutWrapperProps {
|
|||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use memo to prevent unnecessary rerenders of the entire layout
|
export function LayoutWrapper({ children, isSignInPage, isAuthenticated }: LayoutWrapperProps) {
|
||||||
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={!isSignInPage ? backgroundStyle : {}}
|
style={
|
||||||
|
!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>
|
||||||
@ -50,4 +39,4 @@ export const LayoutWrapper = memo(function LayoutWrapper({
|
|||||||
<Toaster />
|
<Toaster />
|
||||||
</AuthCheck>
|
</AuthCheck>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useCallback, useMemo, memo } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Calendar,
|
Calendar,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
@ -38,7 +38,6 @@ import { format } from 'date-fns';
|
|||||||
import { fr } from 'date-fns/locale';
|
import { fr } from 'date-fns/locale';
|
||||||
import { NotificationBadge } from './notification-badge';
|
import { NotificationBadge } from './notification-badge';
|
||||||
import { NotesDialog } from './notes-dialog';
|
import { NotesDialog } from './notes-dialog';
|
||||||
import { useUnreadAnnouncements } from '@/hooks/use-unread-announcements';
|
|
||||||
|
|
||||||
const requestNotificationPermission = async () => {
|
const requestNotificationPermission = async () => {
|
||||||
try {
|
try {
|
||||||
@ -50,18 +49,17 @@ const requestNotificationPermission = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use React.memo to memoize the entire component
|
export function MainNav() {
|
||||||
export const MainNav = memo(function MainNav() {
|
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
const { data: session, status } = useSession();
|
const { data: session, status } = useSession();
|
||||||
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 with memo to prevent unnecessary re-renders
|
console.log("Session:", session);
|
||||||
const { hasUnread } = useUnreadAnnouncements();
|
console.log("Status:", status);
|
||||||
|
|
||||||
// Updated function to get user initials - memoize this
|
// Updated function to get user initials
|
||||||
const getUserInitials = useCallback(() => {
|
const getUserInitials = () => {
|
||||||
if (session?.user?.name) {
|
if (session?.user?.name) {
|
||||||
// Split the full name and get initials
|
// Split the full name and get initials
|
||||||
const names = session.user.name.split(' ');
|
const names = session.user.name.split(' ');
|
||||||
@ -72,15 +70,15 @@ export const MainNav = memo(function MainNav() {
|
|||||||
return names[0].slice(0, 2).toUpperCase();
|
return names[0].slice(0, 2).toUpperCase();
|
||||||
}
|
}
|
||||||
return "?";
|
return "?";
|
||||||
}, [session?.user?.name]);
|
};
|
||||||
|
|
||||||
// Function to get display name - memoize this
|
// Function to get display name
|
||||||
const getDisplayName = useCallback(() => {
|
const getDisplayName = () => {
|
||||||
return session?.user?.name || "User";
|
return session?.user?.name || "User";
|
||||||
}, [session?.user?.name]);
|
};
|
||||||
|
|
||||||
// Function to get user role - memoize this
|
// Function to get user role
|
||||||
const getUserRole = useCallback(() => {
|
const getUserRole = () => {
|
||||||
if (session?.user?.role) {
|
if (session?.user?.role) {
|
||||||
if (Array.isArray(session.user.role)) {
|
if (Array.isArray(session.user.role)) {
|
||||||
// Filter out technical roles and format remaining ones
|
// 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;
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}, [session?.user?.role]);
|
};
|
||||||
|
|
||||||
// Function to check if user has a specific role - memoize this
|
// Function to check if user has a specific role
|
||||||
const hasRole = useCallback((requiredRoles: string[]) => {
|
const hasRole = (requiredRoles: string[]) => {
|
||||||
if (!session?.user?.role) {
|
if (!session?.user?.role) {
|
||||||
|
console.log('No user roles found');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userRoles = Array.isArray(session.user.role) ? session.user.role : [session.user.role];
|
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
|
// Clean up user roles by removing prefixes and converting to lowercase
|
||||||
const cleanUserRoles = userRoles.map(role =>
|
const cleanUserRoles = userRoles.map(role =>
|
||||||
@ -122,18 +122,21 @@ export const MainNav = memo(function MainNav() {
|
|||||||
.replace(/^ROLE_/, '') // Remove ROLE_ prefix
|
.replace(/^ROLE_/, '') // Remove ROLE_ prefix
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
);
|
);
|
||||||
|
console.log('Clean user roles:', cleanUserRoles);
|
||||||
|
|
||||||
// Clean required roles
|
// Clean required roles
|
||||||
const cleanRequiredRoles = requiredRoles.map(role => role.toLowerCase());
|
const cleanRequiredRoles = requiredRoles.map(role => role.toLowerCase());
|
||||||
|
console.log('Clean required roles:', cleanRequiredRoles);
|
||||||
|
|
||||||
// Check if user has any of the required roles
|
// Check if user has any of the required roles
|
||||||
const hasAnyRole = cleanRequiredRoles.some(role => cleanUserRoles.includes(role));
|
const hasAnyRole = cleanRequiredRoles.some(role => cleanUserRoles.includes(role));
|
||||||
|
console.log('Has any role:', hasAnyRole);
|
||||||
|
|
||||||
return hasAnyRole;
|
return hasAnyRole;
|
||||||
}, [session?.user?.role]);
|
};
|
||||||
|
|
||||||
// Status configurations
|
// Status configurations
|
||||||
const statusConfig = useMemo(() => ({
|
const statusConfig = {
|
||||||
online: {
|
online: {
|
||||||
color: 'text-green-500',
|
color: 'text-green-500',
|
||||||
label: 'Online',
|
label: 'Online',
|
||||||
@ -149,10 +152,10 @@ export const MainNav = memo(function MainNav() {
|
|||||||
label: 'Away',
|
label: 'Away',
|
||||||
notifications: false
|
notifications: false
|
||||||
},
|
},
|
||||||
}), []);
|
};
|
||||||
|
|
||||||
// Handle status change
|
// Handle status change
|
||||||
const handleStatusChange = useCallback(async (newStatus: 'online' | 'busy' | 'away') => {
|
const handleStatusChange = async (newStatus: 'online' | 'busy' | 'away') => {
|
||||||
setUserStatus(newStatus);
|
setUserStatus(newStatus);
|
||||||
|
|
||||||
if (newStatus !== 'online') {
|
if (newStatus !== 'online') {
|
||||||
@ -174,19 +177,19 @@ export const MainNav = memo(function MainNav() {
|
|||||||
// Re-enable notifications if going back online
|
// Re-enable notifications if going back online
|
||||||
requestNotificationPermission();
|
requestNotificationPermission();
|
||||||
}
|
}
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
// Base menu items (available for everyone)
|
// Base menu items (available for everyone)
|
||||||
const baseMenuItems = useMemo(() => [
|
const baseMenuItems = [
|
||||||
{
|
{
|
||||||
title: "QG",
|
title: "QG",
|
||||||
icon: Target,
|
icon: Target,
|
||||||
href: '/qg',
|
href: '/qg',
|
||||||
},
|
},
|
||||||
], []);
|
];
|
||||||
|
|
||||||
// Role-specific menu items
|
// Role-specific menu items
|
||||||
const roleSpecificItems = useMemo(() => [
|
const roleSpecificItems = [
|
||||||
{
|
{
|
||||||
title: "ShowCase",
|
title: "ShowCase",
|
||||||
icon: Lightbulb,
|
icon: Lightbulb,
|
||||||
@ -205,64 +208,18 @@ export const MainNav = memo(function MainNav() {
|
|||||||
href: '/the-message',
|
href: '/the-message',
|
||||||
requiredRoles: ["mediation", "expression"],
|
requiredRoles: ["mediation", "expression"],
|
||||||
},
|
},
|
||||||
], []);
|
];
|
||||||
|
|
||||||
// Get visible menu items based on user roles
|
// Get visible menu items based on user roles
|
||||||
const visibleMenuItems = useMemo(() => [
|
const visibleMenuItems = [
|
||||||
...baseMenuItems,
|
...baseMenuItems,
|
||||||
...roleSpecificItems.filter(item => hasRole(item.requiredRoles))
|
...roleSpecificItems.filter(item => hasRole(item.requiredRoles))
|
||||||
], [baseMenuItems, roleSpecificItems, hasRole]);
|
];
|
||||||
|
|
||||||
// Format current date and time
|
// Format current date and time
|
||||||
const dateTimeDisplay = useMemo(() => {
|
const now = new Date();
|
||||||
const now = new Date();
|
const formattedDate = format(now, "d MMMM yyyy", { locale: fr });
|
||||||
const formattedDate = format(now, "d MMMM yyyy", { locale: fr });
|
const formattedTime = format(now, "HH:mm");
|
||||||
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]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -271,7 +228,7 @@ export const MainNav = memo(function MainNav() {
|
|||||||
{/* Left side */}
|
{/* Left side */}
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<button
|
<button
|
||||||
onClick={toggleSidebar}
|
onClick={() => setIsSidebarOpen(true)}
|
||||||
className="text-white/80 hover:text-white"
|
className="text-white/80 hover:text-white"
|
||||||
>
|
>
|
||||||
<Menu className="w-5 h-5" />
|
<Menu className="w-5 h-5" />
|
||||||
@ -293,7 +250,7 @@ export const MainNav = memo(function MainNav() {
|
|||||||
<span className="sr-only">TimeTracker</span>
|
<span className="sr-only">TimeTracker</span>
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
<button
|
||||||
onClick={toggleNotesDialog}
|
onClick={() => setIsNotesDialogOpen(true)}
|
||||||
className='text-white/80 hover:text-white'
|
className='text-white/80 hover:text-white'
|
||||||
>
|
>
|
||||||
<PenLine className='w-5 h-5' />
|
<PenLine className='w-5 h-5' />
|
||||||
@ -315,11 +272,8 @@ export const MainNav = memo(function MainNav() {
|
|||||||
<RadioIcon className='w-5 h-5' />
|
<RadioIcon className='w-5 h-5' />
|
||||||
<span className="sr-only">Radio</span>
|
<span className="sr-only">Radio</span>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href='/announcement' className='text-white/80 hover:text-white relative'>
|
<Link href='/announcement' className='text-white/80 hover:text-white'>
|
||||||
<Megaphone className='w-5 h-5' />
|
<Megaphone className='w-5 h-5' />
|
||||||
{hasUnread && (
|
|
||||||
<span className="absolute -top-1 -right-1 w-2.5 h-2.5 bg-red-500 rounded-full"></span>
|
|
||||||
)}
|
|
||||||
<span className="sr-only">Announcement</span>
|
<span className="sr-only">Announcement</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@ -328,8 +282,8 @@ export const MainNav = memo(function MainNav() {
|
|||||||
<div className="flex items-center space-x-8">
|
<div className="flex items-center space-x-8">
|
||||||
{/* Date and Time with smaller text */}
|
{/* Date and Time with smaller text */}
|
||||||
<div className="text-white/80 text-sm">
|
<div className="text-white/80 text-sm">
|
||||||
<span className="mr-2">{dateTimeDisplay.formattedDate}</span>
|
<span className="mr-2">{formattedDate}</span>
|
||||||
<span>{dateTimeDisplay.formattedTime}</span>
|
<span>{formattedTime}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NotificationBadge />
|
<NotificationBadge />
|
||||||
@ -391,7 +345,37 @@ export const MainNav = memo(function MainNav() {
|
|||||||
))}
|
))}
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="text-white/80 hover:text-white hover:bg-black/50 cursor-pointer"
|
className="text-white/80 hover:text-white hover:bg-black/50 cursor-pointer"
|
||||||
onClick={handleLogout}
|
onClick={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';
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<LogOut className="mr-2 h-4 w-4" />
|
<LogOut className="mr-2 h-4 w-4" />
|
||||||
<span>Déconnexion</span>
|
<span>Déconnexion</span>
|
||||||
@ -415,4 +399,4 @@ export const MainNav = memo(function MainNav() {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { memo, useCallback, useMemo } from "react";
|
import type React from "react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@ -44,8 +44,7 @@ interface MenuItem {
|
|||||||
requiredRole?: string | string[];
|
requiredRole?: string | string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Memoize the entire Sidebar component to prevent unnecessary re-renders
|
export function Sidebar({ isOpen, onClose }: SidebarProps) {
|
||||||
export const Sidebar = memo(function Sidebar({ isOpen, onClose }: SidebarProps) {
|
|
||||||
const { data: session, status } = useSession();
|
const { data: session, status } = useSession();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@ -60,8 +59,8 @@ export const Sidebar = memo(function Sidebar({ isOpen, onClose }: SidebarProps)
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to check if user has a specific role - memoize this logic
|
// Function to check if user has a specific role
|
||||||
const hasRole = useCallback((requiredRole: string | string[] | undefined) => {
|
const hasRole = (requiredRole: string | string[] | undefined) => {
|
||||||
// If no role is required, allow access
|
// If no role is required, allow access
|
||||||
if (!requiredRole) {
|
if (!requiredRole) {
|
||||||
return true;
|
return true;
|
||||||
@ -108,10 +107,10 @@ export const Sidebar = memo(function Sidebar({ isOpen, onClose }: SidebarProps)
|
|||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}, [session]);
|
};
|
||||||
|
|
||||||
// Base menu items (available for everyone)
|
// Base menu items (available for everyone)
|
||||||
const baseMenuItems: MenuItem[] = useMemo(() => [
|
const baseMenuItems: MenuItem[] = [
|
||||||
{
|
{
|
||||||
title: "Pages",
|
title: "Pages",
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
@ -159,10 +158,10 @@ export const Sidebar = memo(function Sidebar({ isOpen, onClose }: SidebarProps)
|
|||||||
href: "/agilite",
|
href: "/agilite",
|
||||||
iframe: process.env.NEXT_PUBLIC_IFRAME_AGILITY_URL,
|
iframe: process.env.NEXT_PUBLIC_IFRAME_AGILITY_URL,
|
||||||
},
|
},
|
||||||
], []);
|
];
|
||||||
|
|
||||||
// Role-specific menu items
|
// Role-specific menu items
|
||||||
const roleSpecificItems: MenuItem[] = useMemo(() => [
|
const roleSpecificItems: MenuItem[] = [
|
||||||
{
|
{
|
||||||
title: "Artlab",
|
title: "Artlab",
|
||||||
icon: Palette,
|
icon: Palette,
|
||||||
@ -191,45 +190,25 @@ export const Sidebar = memo(function Sidebar({ isOpen, onClose }: SidebarProps)
|
|||||||
iframe: process.env.NEXT_PUBLIC_IFRAME_MEDIATIONS_URL,
|
iframe: process.env.NEXT_PUBLIC_IFRAME_MEDIATIONS_URL,
|
||||||
requiredRole: "mediation",
|
requiredRole: "mediation",
|
||||||
},
|
},
|
||||||
], []);
|
];
|
||||||
|
|
||||||
// Combine base items with role-specific items based on user roles
|
// Combine base items with role-specific items based on user roles
|
||||||
const visibleMenuItems = useMemo(() => [
|
const visibleMenuItems = [
|
||||||
...baseMenuItems,
|
...baseMenuItems,
|
||||||
...roleSpecificItems.filter(item => hasRole(item.requiredRole))
|
...roleSpecificItems.filter(item => {
|
||||||
], [baseMenuItems, roleSpecificItems, hasRole]);
|
const isVisible = hasRole(item.requiredRole);
|
||||||
|
return isVisible;
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
const handleNavigation = useCallback((href: string, external?: boolean) => {
|
const handleNavigation = (href: string, external?: boolean) => {
|
||||||
if (external && href) {
|
if (external && href) {
|
||||||
window.open(href, "_blank");
|
window.open(href, "_blank");
|
||||||
} else {
|
} else {
|
||||||
router.push(href);
|
router.push(href);
|
||||||
}
|
}
|
||||||
onClose();
|
onClose();
|
||||||
}, [router, onClose]);
|
};
|
||||||
|
|
||||||
// Memoize the menu render to prevent unnecessary recalculation
|
|
||||||
const renderMenu = useMemo(() => (
|
|
||||||
<div className="px-2 py-2">
|
|
||||||
{visibleMenuItems.map((item, index) => (
|
|
||||||
<div key={`${item.title}-${index}`} className="mb-1">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className={cn(
|
|
||||||
"w-full justify-start font-normal",
|
|
||||||
pathname === item.href
|
|
||||||
? "bg-accent text-accent-foreground"
|
|
||||||
: "hover:bg-accent hover:text-accent-foreground"
|
|
||||||
)}
|
|
||||||
onClick={() => handleNavigation(item.href, item.external)}
|
|
||||||
>
|
|
||||||
<item.icon className="mr-2 h-4 w-4" />
|
|
||||||
{item.title}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
), [visibleMenuItems, pathname, handleNavigation]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -268,15 +247,25 @@ export const Sidebar = memo(function Sidebar({ isOpen, onClose }: SidebarProps)
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Menu Items - Use memoized menu */}
|
{/* Menu Items */}
|
||||||
{renderMenu}
|
<div className="space-y-1 p-4">
|
||||||
|
{visibleMenuItems.map((item) => (
|
||||||
{/* Calendar Navigation */}
|
<Button
|
||||||
<div className="px-2 pb-4">
|
key={item.title}
|
||||||
<CalendarNav />
|
variant="ghost"
|
||||||
|
className={cn(
|
||||||
|
"w-full justify-start gap-2 text-black hover:bg-gray-100",
|
||||||
|
pathname === item.href && !item.external && "bg-gray-100"
|
||||||
|
)}
|
||||||
|
onClick={() => handleNavigation(item.href, item.external)}
|
||||||
|
>
|
||||||
|
<item.icon className="h-5 w-5" />
|
||||||
|
<span>{item.title}</span>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|||||||
@ -5,8 +5,25 @@ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
// Memoize ScrollBar component to prevent unnecessary re-renders
|
const ScrollArea = React.forwardRef<
|
||||||
const ScrollBar = React.memo(React.forwardRef<
|
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<ScrollAreaPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn("relative overflow-hidden", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||||
|
{children}
|
||||||
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
<ScrollBar />
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
||||||
|
))
|
||||||
|
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||||
|
|
||||||
|
const ScrollBar = React.forwardRef<
|
||||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||||
@ -25,26 +42,7 @@ const ScrollBar = React.memo(React.forwardRef<
|
|||||||
>
|
>
|
||||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
)))
|
))
|
||||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||||
|
|
||||||
// Memoize ScrollArea component to prevent unnecessary re-renders
|
|
||||||
const ScrollArea = React.memo(React.forwardRef<
|
|
||||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<ScrollAreaPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn("relative overflow-hidden", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
|
||||||
{children}
|
|
||||||
</ScrollAreaPrimitive.Viewport>
|
|
||||||
<ScrollBar />
|
|
||||||
<ScrollAreaPrimitive.Corner />
|
|
||||||
</ScrollAreaPrimitive.Root>
|
|
||||||
)))
|
|
||||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
|
||||||
|
|
||||||
export { ScrollArea, ScrollBar }
|
export { ScrollArea, ScrollBar }
|
||||||
|
|||||||
@ -1,56 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
|
|
||||||
export function useLocalStorage<T>(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<T>(() => {
|
|
||||||
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];
|
|
||||||
}
|
|
||||||
@ -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<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 = 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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user