398 lines
14 KiB
TypeScript
398 lines
14 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import {
|
|
Calendar,
|
|
MessageSquare,
|
|
BotIcon as Robot,
|
|
Bell,
|
|
Users,
|
|
LogOut,
|
|
UserCog,
|
|
Clock,
|
|
PenLine,
|
|
Video,
|
|
Radio as RadioIcon,
|
|
Megaphone,
|
|
Heart,
|
|
Target,
|
|
Mail,
|
|
Telescope,
|
|
Lightbulb,
|
|
Circle,
|
|
Menu,
|
|
} from "lucide-react";
|
|
import Image from "next/image";
|
|
import Link from "next/link";
|
|
import { Sidebar } from "./sidebar";
|
|
import { useSession, signIn, signOut } from "next-auth/react";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import { format } from 'date-fns';
|
|
import { fr } from 'date-fns/locale';
|
|
|
|
const requestNotificationPermission = async () => {
|
|
try {
|
|
const permission = await Notification.requestPermission();
|
|
return permission === "granted";
|
|
} catch (error) {
|
|
console.error("Error requesting notification permission:", error);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
export function MainNav() {
|
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
|
const { data: session, status } = useSession();
|
|
const [userStatus, setUserStatus] = useState<'online' | 'busy' | 'away'>('online');
|
|
|
|
console.log("Session:", session);
|
|
console.log("Status:", status);
|
|
|
|
// Updated function to get user initials
|
|
const getUserInitials = () => {
|
|
if (session?.user?.name) {
|
|
// Split the full name and get initials
|
|
const names = session.user.name.split(' ');
|
|
if (names.length >= 2) {
|
|
return `${names[0][0]}${names[names.length - 1][0]}`.toUpperCase();
|
|
}
|
|
// If only one name, use first two letters
|
|
return names[0].slice(0, 2).toUpperCase();
|
|
}
|
|
return "?";
|
|
};
|
|
|
|
// Function to get display name
|
|
const getDisplayName = () => {
|
|
return session?.user?.name || "User";
|
|
};
|
|
|
|
// Function to get user role
|
|
const getUserRole = () => {
|
|
if (session?.user?.role) {
|
|
if (Array.isArray(session.user.role)) {
|
|
// Filter out technical roles and format remaining ones
|
|
return session.user.role
|
|
.filter(role =>
|
|
!['offline_access', 'uma_authorization', 'default-roles-cercle'].includes(role)
|
|
)
|
|
.map(role => {
|
|
// Transform role names
|
|
switch(role) {
|
|
case 'ROLE_Mentors':
|
|
return 'Mentor';
|
|
case 'ROLE_apprentice':
|
|
return 'Apprentice';
|
|
case 'ROLE_Admin':
|
|
return 'Admin';
|
|
default:
|
|
return role.replace('ROLE_', '');
|
|
}
|
|
})
|
|
.join(', ');
|
|
}
|
|
return session.user.role;
|
|
}
|
|
return "";
|
|
};
|
|
|
|
// Function to check if user has a specific role
|
|
const hasRole = (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 =>
|
|
role.replace(/^[\/]/, '') // Remove leading slash
|
|
.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;
|
|
};
|
|
|
|
// Status configurations
|
|
const statusConfig = {
|
|
online: {
|
|
color: 'text-green-500',
|
|
label: 'Online',
|
|
notifications: true
|
|
},
|
|
busy: {
|
|
color: 'text-orange-500',
|
|
label: 'Busy',
|
|
notifications: false
|
|
},
|
|
away: {
|
|
color: 'text-gray-500',
|
|
label: 'Away',
|
|
notifications: false
|
|
},
|
|
};
|
|
|
|
// Handle status change
|
|
const handleStatusChange = async (newStatus: 'online' | 'busy' | 'away') => {
|
|
setUserStatus(newStatus);
|
|
|
|
if (newStatus !== 'online') {
|
|
// If status is busy or away, check and request notification permission if needed
|
|
const hasPermission = await requestNotificationPermission();
|
|
|
|
if (hasPermission) {
|
|
// Disable notifications
|
|
if ('serviceWorker' in navigator) {
|
|
const registration = await navigator.serviceWorker.ready;
|
|
await registration.pushManager.getSubscription()?.then(subscription => {
|
|
if (subscription) {
|
|
subscription.unsubscribe();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
} else {
|
|
// Re-enable notifications if going back online
|
|
requestNotificationPermission();
|
|
}
|
|
};
|
|
|
|
// Base menu items (available for everyone)
|
|
const baseMenuItems = [
|
|
{
|
|
title: "QG",
|
|
icon: Target,
|
|
href: '/qg',
|
|
},
|
|
];
|
|
|
|
// Role-specific menu items
|
|
const roleSpecificItems = [
|
|
{
|
|
title: "ShowCase",
|
|
icon: Lightbulb,
|
|
href: '/showcase',
|
|
requiredRoles: ["Expression"],
|
|
},
|
|
{
|
|
title: "Equipes",
|
|
icon: UserCog,
|
|
href: '/equipes',
|
|
requiredRoles: ["Admin", "Entrepreneurship"],
|
|
},
|
|
{
|
|
title: "TheMessage",
|
|
icon: Mail,
|
|
href: '/the-message',
|
|
requiredRoles: ["Mediation", "Expression"],
|
|
},
|
|
];
|
|
|
|
// Get visible menu items based on user roles
|
|
const visibleMenuItems = [
|
|
...baseMenuItems,
|
|
...roleSpecificItems.filter(item => hasRole(item.requiredRoles))
|
|
];
|
|
|
|
// Format current date and time
|
|
const now = new Date();
|
|
const formattedDate = format(now, "d MMMM yyyy", { locale: fr });
|
|
const formattedTime = format(now, "HH:mm");
|
|
|
|
return (
|
|
<>
|
|
<div className="fixed top-0 left-0 right-0 z-50 bg-black">
|
|
<div className="flex items-center justify-between px-4 py-1">
|
|
{/* Left side */}
|
|
<div className="flex items-center space-x-4">
|
|
<button
|
|
onClick={() => setIsSidebarOpen(true)}
|
|
className="text-white/80 hover:text-white"
|
|
>
|
|
<Menu className="w-5 h-5" />
|
|
</button>
|
|
<Link href='/'>
|
|
<Image
|
|
src='/Neahv2 logo W.png'
|
|
alt='Neah Logo'
|
|
width={40}
|
|
height={13}
|
|
className='text-white'
|
|
/>
|
|
</Link>
|
|
<Link href='/agenda' className='text-white/80 hover:text-white'>
|
|
<Calendar className='w-5 h-5' />
|
|
</Link>
|
|
<Link href='/timetracker' className='text-white/80 hover:text-white'>
|
|
<Clock className='w-5 h-5' />
|
|
<span className="sr-only">TimeTracker</span>
|
|
</Link>
|
|
<Link href='/notes' className='text-white/80 hover:text-white'>
|
|
<PenLine className='w-5 h-5' />
|
|
<span className="sr-only">Notes</span>
|
|
</Link>
|
|
<Link href='/alma' className='text-white/80 hover:text-white'>
|
|
<Robot className='w-5 h-5' />
|
|
<span className="sr-only">ALMA</span>
|
|
</Link>
|
|
<Link href='/vision' className='text-white/80 hover:text-white'>
|
|
<Video className='w-5 h-5' />
|
|
<span className="sr-only">Vision</span>
|
|
</Link>
|
|
<Link href='/observatory' className='text-white/80 hover:text-white'>
|
|
<Telescope className='w-5 h-5' />
|
|
<span className="sr-only">Observatory</span>
|
|
</Link>
|
|
<Link href='/radio' className='text-white/80 hover:text-white'>
|
|
<RadioIcon className='w-5 h-5' />
|
|
<span className="sr-only">Radio</span>
|
|
</Link>
|
|
<Link href='/announcement' className='text-white/80 hover:text-white'>
|
|
<Megaphone className='w-5 h-5' />
|
|
<span className="sr-only">Announcement</span>
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Right side */}
|
|
<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>
|
|
</div>
|
|
|
|
<Link
|
|
href='/notifications'
|
|
className='text-white/80 hover:text-white'
|
|
>
|
|
<Bell className='w-5 h-5' />
|
|
</Link>
|
|
|
|
{status === "authenticated" && session?.user ? (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger className="outline-none">
|
|
<div className="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-white cursor-pointer hover:bg-blue-700 transition-colors">
|
|
{getUserInitials()}
|
|
</div>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-56 bg-black/90 border-gray-700">
|
|
<DropdownMenuLabel className="text-white/80">
|
|
<div className="flex items-center justify-between">
|
|
<span>{getDisplayName()}</span>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger className="outline-none">
|
|
<div className="flex items-center space-x-1 text-sm">
|
|
<Circle className={`h-3 w-3 ${statusConfig[userStatus].color}`} />
|
|
<span className="text-gray-400">{statusConfig[userStatus].label}</span>
|
|
</div>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent className="bg-black/90 border-gray-700">
|
|
<DropdownMenuItem
|
|
className="text-white/80 hover:text-white hover:bg-black/50 cursor-pointer"
|
|
onClick={() => handleStatusChange('online')}
|
|
>
|
|
<Circle className="h-3 w-3 text-green-500 mr-2" />
|
|
<span>Online</span>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
className="text-white/80 hover:text-white hover:bg-black/50 cursor-pointer"
|
|
onClick={() => handleStatusChange('busy')}
|
|
>
|
|
<Circle className="h-3 w-3 text-orange-500 mr-2" />
|
|
<span>Busy</span>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
className="text-white/80 hover:text-white hover:bg-black/50 cursor-pointer"
|
|
onClick={() => handleStatusChange('away')}
|
|
>
|
|
<Circle className="h-3 w-3 text-gray-500 mr-2" />
|
|
<span>Away</span>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</DropdownMenuLabel>
|
|
<DropdownMenuSeparator className="bg-gray-700" />
|
|
{visibleMenuItems.map((item) => (
|
|
<DropdownMenuItem
|
|
key={item.title}
|
|
className="text-white/80 hover:text-white hover:bg-black/50 cursor-pointer"
|
|
onClick={() => window.location.href = item.href}
|
|
>
|
|
<item.icon className="mr-2 h-4 w-4" />
|
|
<span>{item.title}</span>
|
|
</DropdownMenuItem>
|
|
))}
|
|
<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';
|
|
}
|
|
}}
|
|
>
|
|
<LogOut className="mr-2 h-4 w-4" />
|
|
<span>Déconnexion</span>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
) : (
|
|
<div className='cursor-pointer text-white/80 hover:text-white'>
|
|
<span onClick={() => signIn("keycloak", { callbackUrl: "/" })}>
|
|
Login
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<Sidebar isOpen={isSidebarOpen} onClose={() => setIsSidebarOpen(false)} />
|
|
</>
|
|
);
|
|
}
|