This commit is contained in:
alma 2025-05-04 22:41:45 +02:00
parent bf348135ff
commit 0ad05e08ce
3 changed files with 147 additions and 125 deletions

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import { useState } from "react"; import { useState, useCallback, useMemo, memo } from "react";
import { import {
Calendar, Calendar,
MessageSquare, MessageSquare,
@ -50,7 +50,8 @@ const requestNotificationPermission = async () => {
} }
}; };
export function MainNav() { // Use React.memo to memoize the entire component
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');
@ -59,11 +60,8 @@ export function MainNav() {
// Use the unread announcements hook with memo to prevent unnecessary re-renders // Use the unread announcements hook with memo to prevent unnecessary re-renders
const { hasUnread } = useUnreadAnnouncements(); const { hasUnread } = useUnreadAnnouncements();
console.log("Session:", session); // Updated function to get user initials - memoize this
console.log("Status:", status); const getUserInitials = useCallback(() => {
// Updated function to get user initials
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(' ');
@ -74,15 +72,15 @@ export 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 // Function to get display name - memoize this
const getDisplayName = () => { const getDisplayName = useCallback(() => {
return session?.user?.name || "User"; return session?.user?.name || "User";
}; }, [session?.user?.name]);
// Function to get user role // Function to get user role - memoize this
const getUserRole = () => { const getUserRole = useCallback(() => {
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
@ -108,17 +106,15 @@ export function MainNav() {
return session.user.role; return session.user.role;
} }
return ""; return "";
}; }, [session?.user?.role]);
// Function to check if user has a specific role // Function to check if user has a specific role - memoize this
const hasRole = (requiredRoles: string[]) => { const hasRole = useCallback((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 =>
@ -126,21 +122,18 @@ export 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 = { const statusConfig = useMemo(() => ({
online: { online: {
color: 'text-green-500', color: 'text-green-500',
label: 'Online', label: 'Online',
@ -156,10 +149,10 @@ export function MainNav() {
label: 'Away', label: 'Away',
notifications: false notifications: false
}, },
}; }), []);
// Handle status change // Handle status change
const handleStatusChange = async (newStatus: 'online' | 'busy' | 'away') => { const handleStatusChange = useCallback(async (newStatus: 'online' | 'busy' | 'away') => {
setUserStatus(newStatus); setUserStatus(newStatus);
if (newStatus !== 'online') { if (newStatus !== 'online') {
@ -181,19 +174,19 @@ export 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 = [ const baseMenuItems = useMemo(() => [
{ {
title: "QG", title: "QG",
icon: Target, icon: Target,
href: '/qg', href: '/qg',
}, },
]; ], []);
// Role-specific menu items // Role-specific menu items
const roleSpecificItems = [ const roleSpecificItems = useMemo(() => [
{ {
title: "ShowCase", title: "ShowCase",
icon: Lightbulb, icon: Lightbulb,
@ -212,18 +205,64 @@ export 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 = [ const visibleMenuItems = useMemo(() => [
...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 now = new Date(); const dateTimeDisplay = useMemo(() => {
const formattedDate = format(now, "d MMMM yyyy", { locale: fr }); const now = new Date();
const formattedTime = format(now, "HH:mm"); const formattedDate = format(now, "d MMMM yyyy", { locale: fr });
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 (
<> <>
@ -232,7 +271,7 @@ export 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={() => setIsSidebarOpen(true)} onClick={toggleSidebar}
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" />
@ -254,7 +293,7 @@ export function MainNav() {
<span className="sr-only">TimeTracker</span> <span className="sr-only">TimeTracker</span>
</Link> </Link>
<button <button
onClick={() => setIsNotesDialogOpen(true)} onClick={toggleNotesDialog}
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' />
@ -289,8 +328,8 @@ export 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">{formattedDate}</span> <span className="mr-2">{dateTimeDisplay.formattedDate}</span>
<span>{formattedTime}</span> <span>{dateTimeDisplay.formattedTime}</span>
</div> </div>
<NotificationBadge /> <NotificationBadge />
@ -352,37 +391,7 @@ export 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={async () => { onClick={handleLogout}
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>
@ -406,4 +415,4 @@ export function MainNav() {
/> />
</> </>
); );
} });

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import type React from "react"; import React, { memo, useCallback, useMemo } from "react";
import { useState } from "react"; import { useState } from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@ -44,7 +44,8 @@ interface MenuItem {
requiredRole?: string | string[]; requiredRole?: string | string[];
} }
export function Sidebar({ isOpen, onClose }: SidebarProps) { // Memoize the entire Sidebar component to prevent unnecessary re-renders
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();
@ -59,8 +60,8 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
return null; return null;
} }
// Function to check if user has a specific role // Function to check if user has a specific role - memoize this logic
const hasRole = (requiredRole: string | string[] | undefined) => { const hasRole = useCallback((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;
@ -107,10 +108,10 @@ export 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[] = [ const baseMenuItems: MenuItem[] = useMemo(() => [
{ {
title: "Pages", title: "Pages",
icon: FileText, icon: FileText,
@ -158,10 +159,10 @@ export 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[] = [ const roleSpecificItems: MenuItem[] = useMemo(() => [
{ {
title: "Artlab", title: "Artlab",
icon: Palette, icon: Palette,
@ -190,25 +191,45 @@ export 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 = [ const visibleMenuItems = useMemo(() => [
...baseMenuItems, ...baseMenuItems,
...roleSpecificItems.filter(item => { ...roleSpecificItems.filter(item => hasRole(item.requiredRole))
const isVisible = hasRole(item.requiredRole); ], [baseMenuItems, roleSpecificItems, hasRole]);
return isVisible;
})
];
const handleNavigation = (href: string, external?: boolean) => { const handleNavigation = useCallback((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 (
<> <>
@ -247,25 +268,15 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
/> />
</div> </div>
{/* Menu Items */} {/* Menu Items - Use memoized menu */}
<div className="space-y-1 p-4"> {renderMenu}
{visibleMenuItems.map((item) => (
<Button {/* Calendar Navigation */}
key={item.title} <div className="px-2 pb-4">
variant="ghost" <CalendarNav />
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>
</> </>
); );
} });

View File

@ -5,25 +5,8 @@ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef< // Memoize ScrollBar component to prevent unnecessary re-renders
React.ElementRef<typeof ScrollAreaPrimitive.Root>, const ScrollBar = React.memo(React.forwardRef<
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) => (
@ -42,7 +25,26 @@ const ScrollBar = 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 }