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";
import { useState } from "react";
import { useState, useCallback, useMemo, memo } from "react";
import {
Calendar,
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 { data: session, status } = useSession();
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
const { hasUnread } = useUnreadAnnouncements();
console.log("Session:", session);
console.log("Status:", status);
// Updated function to get user initials
const getUserInitials = () => {
// Updated function to get user initials - memoize this
const getUserInitials = useCallback(() => {
if (session?.user?.name) {
// Split the full name and get initials
const names = session.user.name.split(' ');
@ -74,15 +72,15 @@ export function MainNav() {
return names[0].slice(0, 2).toUpperCase();
}
return "?";
};
}, [session?.user?.name]);
// Function to get display name
const getDisplayName = () => {
// Function to get display name - memoize this
const getDisplayName = useCallback(() => {
return session?.user?.name || "User";
};
}, [session?.user?.name]);
// Function to get user role
const getUserRole = () => {
// Function to get user role - memoize this
const getUserRole = useCallback(() => {
if (session?.user?.role) {
if (Array.isArray(session.user.role)) {
// Filter out technical roles and format remaining ones
@ -108,17 +106,15 @@ export function MainNav() {
return session.user.role;
}
return "";
};
}, [session?.user?.role]);
// Function to check if user has a specific role
const hasRole = (requiredRoles: string[]) => {
// Function to check if user has a specific role - memoize this
const hasRole = useCallback((requiredRoles: string[]) => {
if (!session?.user?.role) {
console.log('No user roles found');
return false;
}
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
const cleanUserRoles = userRoles.map(role =>
@ -126,21 +122,18 @@ export function MainNav() {
.replace(/^ROLE_/, '') // Remove ROLE_ prefix
.toLowerCase()
);
console.log('Clean user roles:', cleanUserRoles);
// Clean required roles
const cleanRequiredRoles = requiredRoles.map(role => role.toLowerCase());
console.log('Clean required roles:', cleanRequiredRoles);
// Check if user has any of the required roles
const hasAnyRole = cleanRequiredRoles.some(role => cleanUserRoles.includes(role));
console.log('Has any role:', hasAnyRole);
return hasAnyRole;
};
}, [session?.user?.role]);
// Status configurations
const statusConfig = {
const statusConfig = useMemo(() => ({
online: {
color: 'text-green-500',
label: 'Online',
@ -156,10 +149,10 @@ export function MainNav() {
label: 'Away',
notifications: false
},
};
}), []);
// Handle status change
const handleStatusChange = async (newStatus: 'online' | 'busy' | 'away') => {
const handleStatusChange = useCallback(async (newStatus: 'online' | 'busy' | 'away') => {
setUserStatus(newStatus);
if (newStatus !== 'online') {
@ -181,19 +174,19 @@ export function MainNav() {
// Re-enable notifications if going back online
requestNotificationPermission();
}
};
}, []);
// Base menu items (available for everyone)
const baseMenuItems = [
const baseMenuItems = useMemo(() => [
{
title: "QG",
icon: Target,
href: '/qg',
},
];
], []);
// Role-specific menu items
const roleSpecificItems = [
const roleSpecificItems = useMemo(() => [
{
title: "ShowCase",
icon: Lightbulb,
@ -212,18 +205,64 @@ export function MainNav() {
href: '/the-message',
requiredRoles: ["mediation", "expression"],
},
];
], []);
// Get visible menu items based on user roles
const visibleMenuItems = [
const visibleMenuItems = useMemo(() => [
...baseMenuItems,
...roleSpecificItems.filter(item => hasRole(item.requiredRoles))
];
], [baseMenuItems, roleSpecificItems, hasRole]);
// Format current date and time
const now = new Date();
const formattedDate = format(now, "d MMMM yyyy", { locale: fr });
const formattedTime = format(now, "HH:mm");
const dateTimeDisplay = useMemo(() => {
const now = new Date();
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 (
<>
@ -232,7 +271,7 @@ export function MainNav() {
{/* Left side */}
<div className="flex items-center space-x-4">
<button
onClick={() => setIsSidebarOpen(true)}
onClick={toggleSidebar}
className="text-white/80 hover:text-white"
>
<Menu className="w-5 h-5" />
@ -254,7 +293,7 @@ export function MainNav() {
<span className="sr-only">TimeTracker</span>
</Link>
<button
onClick={() => setIsNotesDialogOpen(true)}
onClick={toggleNotesDialog}
className='text-white/80 hover:text-white'
>
<PenLine className='w-5 h-5' />
@ -289,8 +328,8 @@ export function MainNav() {
<div className="flex items-center space-x-8">
{/* Date and Time with smaller text */}
<div className="text-white/80 text-sm">
<span className="mr-2">{formattedDate}</span>
<span>{formattedTime}</span>
<span className="mr-2">{dateTimeDisplay.formattedDate}</span>
<span>{dateTimeDisplay.formattedTime}</span>
</div>
<NotificationBadge />
@ -352,37 +391,7 @@ export function MainNav() {
))}
<DropdownMenuItem
className="text-white/80 hover:text-white hover:bg-black/50 cursor-pointer"
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';
}
}}
onClick={handleLogout}
>
<LogOut className="mr-2 h-4 w-4" />
<span>Déconnexion</span>
@ -406,4 +415,4 @@ export function MainNav() {
/>
</>
);
}
});

View File

@ -1,6 +1,6 @@
"use client";
import type React from "react";
import React, { memo, useCallback, useMemo } from "react";
import { useState } from "react";
import { cn } from "@/lib/utils";
@ -44,7 +44,8 @@ interface MenuItem {
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 router = useRouter();
const pathname = usePathname();
@ -59,8 +60,8 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
return null;
}
// Function to check if user has a specific role
const hasRole = (requiredRole: string | string[] | undefined) => {
// Function to check if user has a specific role - memoize this logic
const hasRole = useCallback((requiredRole: string | string[] | undefined) => {
// If no role is required, allow access
if (!requiredRole) {
return true;
@ -107,10 +108,10 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
}
return false;
};
}, [session]);
// Base menu items (available for everyone)
const baseMenuItems: MenuItem[] = [
const baseMenuItems: MenuItem[] = useMemo(() => [
{
title: "Pages",
icon: FileText,
@ -158,10 +159,10 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
href: "/agilite",
iframe: process.env.NEXT_PUBLIC_IFRAME_AGILITY_URL,
},
];
], []);
// Role-specific menu items
const roleSpecificItems: MenuItem[] = [
const roleSpecificItems: MenuItem[] = useMemo(() => [
{
title: "Artlab",
icon: Palette,
@ -190,25 +191,45 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
iframe: process.env.NEXT_PUBLIC_IFRAME_MEDIATIONS_URL,
requiredRole: "mediation",
},
];
], []);
// Combine base items with role-specific items based on user roles
const visibleMenuItems = [
const visibleMenuItems = useMemo(() => [
...baseMenuItems,
...roleSpecificItems.filter(item => {
const isVisible = hasRole(item.requiredRole);
return isVisible;
})
];
...roleSpecificItems.filter(item => hasRole(item.requiredRole))
], [baseMenuItems, roleSpecificItems, hasRole]);
const handleNavigation = (href: string, external?: boolean) => {
const handleNavigation = useCallback((href: string, external?: boolean) => {
if (external && href) {
window.open(href, "_blank");
} else {
router.push(href);
}
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 (
<>
@ -247,25 +268,15 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
/>
</div>
{/* Menu Items */}
<div className="space-y-1 p-4">
{visibleMenuItems.map((item) => (
<Button
key={item.title}
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>
))}
{/* Menu Items - Use memoized menu */}
{renderMenu}
{/* Calendar Navigation */}
<div className="px-2 pb-4">
<CalendarNav />
</div>
</ScrollArea>
</div>
</>
);
}
});

View File

@ -5,25 +5,8 @@ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = 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<
// Memoize ScrollBar component to prevent unnecessary re-renders
const ScrollBar = React.memo(React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
@ -42,7 +25,26 @@ const ScrollBar = React.forwardRef<
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
)))
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 }