courrier
This commit is contained in:
parent
bf348135ff
commit
0ad05e08ce
@ -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() {
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@ -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 }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user