Files
workavia-calendar-front/src/components/Menubar/Menubar.tsx
T
lethemanh cadfa70e60 #708 apply strictier linting rules (#717)
* #708 apply strictier linting rules and fix simple eslint bugs

* #708 fix eslint errors relate to promise

* #708 fix eslint import/no-extraneous-dependencies

* #708 fix eslint errors of react-hook

* #708 enable eslint check for typescript

---------

Co-authored-by: lethemanh <lethemanh@lethemanhs-MacBook-Pro.local>
2026-04-01 17:15:10 +02:00

495 lines
13 KiB
TypeScript

import { useAppDispatch, useAppSelector } from '@/app/hooks'
import { setView } from '@/features/Settings/SettingsSlice'
import { Logout } from '@/features/User/oidcAuth'
import logo from '@/static/header-logo.svg'
import { getInitials, stringToGradient } from '@/utils/avatarUtils'
import { getUserDisplayName } from '@/utils/userUtils'
import { redirectTo } from '@/utils/navigation'
import { CalendarApi } from '@fullcalendar/core'
import {
Avatar,
Box,
Button,
ButtonGroup,
Divider,
FormControl,
IconButton,
Menu,
MenuItem,
Popover,
Select,
Typography
} from '@linagora/twake-mui'
import WidgetsOutlinedIcon from '@mui/icons-material/WidgetsOutlined'
import HelpOutlineIcon from '@mui/icons-material/HelpOutline'
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
import LogoutIcon from '@mui/icons-material/Logout'
import RefreshIcon from '@mui/icons-material/Refresh'
import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined'
import React, { useEffect, useState } from 'react'
import { push } from 'redux-first-history'
import { useI18n } from 'twake-i18n'
import SearchBar from './EventSearchBar'
import './Menubar.styl'
export type AppIconProps = {
name: string
link: string
icon: string
}
export type MenubarProps = {
calendarRef: React.RefObject<CalendarApi | null>
onRefresh: () => void
currentDate: Date
onDateChange?: (date: Date) => void
currentView: string
onViewChange?: (view: string) => void
isIframe?: boolean
}
export function Menubar({
calendarRef,
onRefresh,
currentDate,
onDateChange,
currentView,
onViewChange,
isIframe
}: MenubarProps) {
const { t } = useI18n() // deliberately NOT using f()
const user = useAppSelector(state => state.user.userData)
const applist: AppIconProps[] = window.appList ?? []
const supportLink = window.SUPPORT_URL
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
const [userMenuAnchorEl, setUserMenuAnchorEl] = useState<null | HTMLElement>(
null
)
const dispatch = useAppDispatch()
useEffect(() => {
if (!user) {
dispatch(push('/'))
}
}, [dispatch, user])
if (!user) {
return null
}
const handleOpen = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget)
}
const handleClose = () => {
setAnchorEl(null)
}
const handleNavigation = async (action: 'prev' | 'next' | 'today') => {
if (!calendarRef.current) return
await dispatch(setView('calendar'))
switch (action) {
case 'prev':
calendarRef.current.prev()
break
case 'next':
calendarRef.current.next()
break
case 'today':
calendarRef.current.today()
break
}
// Notify parent about date change after navigation
if (onDateChange) {
const newDate = calendarRef.current.getDate()
onDateChange(newDate)
}
}
const handleViewChange = async (view: string) => {
if (!calendarRef.current) return
await dispatch(setView('calendar'))
calendarRef.current.changeView(view)
// Notify parent about view change
if (onViewChange) {
onViewChange(view)
}
// Notify parent about date change after view change
if (onDateChange) {
const newDate = calendarRef.current.getDate()
onDateChange(newDate)
}
}
const open = Boolean(anchorEl)
const userMenuOpen = Boolean(userMenuAnchorEl)
const handleUserMenuClick = (event: React.MouseEvent<HTMLElement>) => {
setUserMenuAnchorEl(event.currentTarget)
}
const handleUserMenuClose = () => {
setUserMenuAnchorEl(null)
}
const handleSettingsClick = () => {
dispatch(setView('settings'))
handleUserMenuClose()
}
const handleLogoutClick = async () => {
const logoutUrl = await Logout()
sessionStorage.removeItem('tokenSet')
redirectTo(logoutUrl.href)
handleUserMenuClose()
}
// Use i18n for month names instead of date-fns
const monthIndex = currentDate.getMonth()
const year = currentDate.getFullYear()
const monthName = t(`months.standalone.${monthIndex}`)
const dateLabel = `${monthName} ${year}`
return (
<>
<header className="menubar">
<div className="left-menu">
{!isIframe && (
<div className="menu-items">
<MainTitle
calendarRef={calendarRef}
currentView={currentView}
onViewChange={onViewChange}
onDateChange={onDateChange}
/>
</div>
)}
<div className="menu-items">
<div className="navigation-controls">
<ButtonGroup
variant="outlined"
size="medium"
sx={{
'& button:first-of-type': {
borderRadius: '12px 0 0 12px'
},
'& button:last-of-type': {
borderRadius: '0 12px 12px 0'
}
}}
>
<Button
sx={{ width: 20 }}
onClick={() => handleNavigation('prev')}
>
<ChevronLeftIcon sx={{ height: 20 }} />
</Button>
<Button onClick={() => handleNavigation('today')}>
{t('menubar.today')}
</Button>
<Button
sx={{ width: 20 }}
onClick={() => handleNavigation('next')}
>
<ChevronRightIcon sx={{ height: 20 }} />
</Button>
</ButtonGroup>
</div>
</div>
<div className="menu-items">
<div className="current-date-time">
<p>{dateLabel}</p>
</div>
</div>
</div>
<div className="right-menu">
<div className="search-container">
<SearchBar />
</div>
<div className="menu-items">
<IconButton
className="refresh-button"
onClick={onRefresh}
aria-label={t('menubar.refresh')}
title={t('menubar.refresh')}
sx={{ mr: 1 }}
>
<RefreshIcon />
</IconButton>
</div>
<div className="menu-items">
<FormControl
size="small"
style={{ minWidth: 120, marginRight: 16 }}
className="select-display"
>
<Select
value={currentView}
onChange={e => handleViewChange(e.target.value)}
variant="outlined"
aria-label={t('menubar.viewSelector')}
sx={{
borderRadius: '12px',
marginLeft: 1,
'& fieldset': { borderRadius: '12px' }
}}
>
<MenuItem value="dayGridMonth">
{t('menubar.views.month')}
</MenuItem>
<MenuItem value="timeGridWeek">
{t('menubar.views.week')}
</MenuItem>
<MenuItem value="timeGridDay">
{t('menubar.views.day')}
</MenuItem>
</Select>
</FormControl>
</div>
{!isIframe && (
<>
{supportLink && (
<div className="menu-items">
<IconButton
component="a"
href={supportLink}
target="_blank"
rel="noopener noreferrer"
style={{ marginRight: 8 }}
aria-label={t('menubar.help')}
title={t('menubar.help')}
>
<HelpOutlineIcon />
</IconButton>
</div>
)}
<div className="menu-items">
{applist.length > 0 && (
<IconButton
onClick={handleOpen}
style={{ marginRight: 8 }}
aria-label={t('menubar.apps')}
title={t('menubar.apps')}
>
<WidgetsOutlinedIcon />
</IconButton>
)}
</div>
</>
)}
<div className="menu-items">
<IconButton
onClick={!isIframe ? handleUserMenuClick : handleSettingsClick}
aria-label={isIframe ? t('menubar.settings') : undefined}
>
{!isIframe ? (
<Avatar
color={stringToGradient(getUserDisplayName(user))}
size="m"
aria-label={t('menubar.userProfile')}
>
{getInitials(getUserDisplayName(user))}
</Avatar>
) : (
<SettingsOutlinedIcon />
)}
</IconButton>
</div>
</div>
</header>
<Popover
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right'
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right'
}}
PaperProps={{
sx: {
minWidth: 230,
mt: 2,
p: '14px 8px',
borderRadius: '14px'
}
}}
>
<div className="app-grid">
{applist.map((prop: AppIconProps) => (
<AppIcon key={prop.name} prop={prop} />
))}
</div>
</Popover>
<Menu
open={userMenuOpen}
anchorEl={userMenuAnchorEl}
onClose={handleUserMenuClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right'
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right'
}}
PaperProps={{
sx: {
minWidth: 280,
mt: 1,
padding: '0 !important',
borderRadius: '14px'
}
}}
>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
padding: '24px'
}}
>
<Avatar
color={stringToGradient(getUserDisplayName(user))}
size="l"
sx={{ marginBottom: '8px' }}
>
{getInitials(getUserDisplayName(user))}
</Avatar>
<Typography
sx={{
color: '#424244',
fontFamily: 'Inter',
fontSize: 22,
fontWeight: 600
}}
>
{getUserDisplayName(user)}
</Typography>
<Typography
sx={{
fontSize: 14,
fontWeight: 500
}}
>
{user?.email}
</Typography>
</Box>
<MenuItem onClick={handleSettingsClick} sx={{ py: 1.5 }}>
<SettingsOutlinedIcon
sx={{
mr: 2,
color: 'rgba(28, 27, 31, 0.48)',
fontSize: 20
}}
/>
{t('menubar.settings') || 'Settings'}
</MenuItem>
<Divider />
<MenuItem onClick={handleLogoutClick} sx={{ py: 1.5 }}>
<LogoutIcon
sx={{
mr: 2,
color: 'rgba(28, 27, 31, 0.48)',
fontSize: 20
}}
/>
{t('menubar.logout') || 'Logout'}
</MenuItem>
</Menu>
</>
)
}
export type MainTitleProps = {
calendarRef: React.RefObject<CalendarApi | null>
currentView: string
onViewChange?: (view: string) => void
onDateChange?: (date: Date) => void
}
export function MainTitle({
calendarRef,
currentView,
onViewChange,
onDateChange
}: MainTitleProps) {
const { t } = useI18n()
const dispatch = useAppDispatch()
const handleLogoClick = async () => {
if (!calendarRef.current) return
await dispatch(setView('calendar'))
if (currentView !== 'timeGridWeek') {
calendarRef.current.changeView('timeGridWeek')
if (onViewChange) {
onViewChange('timeGridWeek')
}
}
calendarRef.current.today()
if (onDateChange) {
const newDate = calendarRef.current.getDate()
onDateChange(newDate)
}
}
return (
<div className="menubar-item tc-home">
<img
className="logo"
src={logo}
alt={t('menubar.logoAlt')}
onClick={handleLogoClick}
/>
</div>
)
}
function AppIcon({ prop }: { prop: AppIconProps }) {
return (
<Box
component="a"
href={prop.link}
target="_blank"
rel="noreferrer"
sx={{
textDecoration: 'none',
color: 'inherit',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
p: '8px 12px 5px',
borderRadius: '14px',
'&:hover': {
backgroundColor: 'action.hover'
}
}}
>
<Box
component="img"
src={prop.icon}
alt={prop.name}
sx={{ maxWidth: 42, height: 42 }}
/>
<Typography sx={{ mt: 0.75, textAlign: 'center', fontSize: 12 }}>
{prop.name}
</Typography>
</Box>
)
}