From 4bb4ec64ad56eeed54d0af7830c8b318fa500884 Mon Sep 17 00:00:00 2001 From: lethemanh Date: Tue, 7 Apr 2026 15:07:44 +0700 Subject: [PATCH] #681 day view with sidebar on mobile --- src/components/Calendar/Calendar.tsx | 3 +- src/components/Calendar/CalendarLayout.tsx | 2 - src/components/Calendar/SideBar.tsx | 148 ------------------ .../Calendar/Sidebar/MobileSidebar.tsx | 125 +++++++++++++++ src/components/Calendar/Sidebar/SideBar.tsx | 8 +- .../Calendar/Sidebar/TabletSidebar.tsx | 88 ++--------- .../Calendar/Sidebar/ViewSwitcher.tsx | 85 ++++++++++ .../Calendar/hooks/useCalendarViewHandlers.ts | 4 +- src/components/Calendar/hooks/useUtilMenus.ts | 77 +++++++++ src/components/Menubar/AppListMenu.tsx | 11 +- src/components/Menubar/MainTitle.tsx | 30 ++-- src/components/Menubar/Menubar.styl | 4 + src/components/Menubar/Menubar.tsx | 56 ++----- src/components/Menubar/MobileMenuBar.tsx | 8 +- src/components/Menubar/UserMenu.tsx | 6 +- .../Menubar/components/MonthSelector.tsx | 1 + src/locales/fr.json | 4 +- 17 files changed, 362 insertions(+), 298 deletions(-) delete mode 100644 src/components/Calendar/SideBar.tsx create mode 100644 src/components/Calendar/Sidebar/MobileSidebar.tsx create mode 100644 src/components/Calendar/Sidebar/ViewSwitcher.tsx create mode 100644 src/components/Calendar/hooks/useUtilMenus.ts diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index c7ca232..7cd8e0e 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -385,6 +385,7 @@ const CalendarApp: React.FC = ({ tempUsers={tempUsers} setTempUsers={setTempUsers} currentView={currentView} + onDateChange={onDateChange} />
@@ -505,7 +506,7 @@ const CalendarApp: React.FC = ({ hour12: false }} datesSet={arg => { - setCurrentView(arg.view.type) + onViewChange?.(arg.view.type) const calendarCurrentDate = calendarRef.current?.getDate() || new Date(arg.start) setDisplayedDateAndRange(calendarCurrentDate) diff --git a/src/components/Calendar/CalendarLayout.tsx b/src/components/Calendar/CalendarLayout.tsx index 6293780..53a14d9 100644 --- a/src/components/Calendar/CalendarLayout.tsx +++ b/src/components/Calendar/CalendarLayout.tsx @@ -72,8 +72,6 @@ export default function CalendarLayout(): JSX.Element { if (!calendarRef.current) return dispatch(setView('calendar')) - calendarRef.current.changeView(view) - // Notify parent about view change setCurrentView(view) diff --git a/src/components/Calendar/SideBar.tsx b/src/components/Calendar/SideBar.tsx deleted file mode 100644 index 4b8ef01..0000000 --- a/src/components/Calendar/SideBar.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import { CalendarApi } from '@fullcalendar/core' -import { Box, Button, Drawer, radius } from '@linagora/twake-mui' -import AddIcon from '@mui/icons-material/Add' -import CalendarViewDayOutlinedIcon from '@mui/icons-material/CalendarViewDayOutlined' -import CalendarViewMonthOutlinedIcon from '@mui/icons-material/CalendarViewMonthOutlined' -import CalendarViewWeekOutlinedIcon from '@mui/icons-material/CalendarViewWeekOutlined' -import { Dispatch, MutableRefObject, SetStateAction } from 'react' -import { useI18n } from 'twake-i18n' -import { User } from '../Attendees/PeopleSearch' -import { FieldWithLabel } from '../Event/components/FieldWithLabel' -import CalendarSelection from './CalendarSelection' -import { MiniCalendar } from './MiniCalendar' -import { TempCalendarsInput } from './TempCalendarsInput' - -interface CalendarSidebarProps { - isTablet: boolean - open: boolean - onClose: () => void - calendarRef: MutableRefObject - isIframe?: boolean - onCreateEvent: () => void - onViewChange: (view: string) => void - selectedMiniDate: Date - setSelectedMiniDate: (date: Date) => void - selectedCalendars: string[] - setSelectedCalendars: Dispatch> - tempUsers: User[] - setTempUsers: Dispatch> -} - -export default function Sidebar({ - isTablet, - open, - onClose, - calendarRef, - isIframe, - onCreateEvent, - onViewChange, - selectedMiniDate, - setSelectedMiniDate, - selectedCalendars, - setSelectedCalendars, - tempUsers, - setTempUsers -}: CalendarSidebarProps) { - const { t } = useI18n() - - return ( - - {!isTablet && ( - <> - - - - - - - - )} - - {isTablet && ( - - - - - - )} - - - - - - - - - - ) -} diff --git a/src/components/Calendar/Sidebar/MobileSidebar.tsx b/src/components/Calendar/Sidebar/MobileSidebar.tsx new file mode 100644 index 0000000..3f41e84 --- /dev/null +++ b/src/components/Calendar/Sidebar/MobileSidebar.tsx @@ -0,0 +1,125 @@ +import { Drawer, IconButton, Box } from '@linagora/twake-mui' +import { CalendarSidebarProps } from './SideBar' +import { SidebarCommonContent } from './SidebarCommonContent' +import { ViewSwitcher } from './ViewSwitcher' +import { MainTitle } from '@/components/Menubar/MainTitle' +import HelpOutlineIcon from '@mui/icons-material/HelpOutline' +import { useI18n } from 'twake-i18n' +import { AppListMenu } from '@/components/Menubar/AppListMenu' +import { UserMenu } from '@/components/Menubar/UserMenu' +import { useAppSelector } from '@/app/hooks' +import { useUtilMenus } from '../hooks/useUtilMenus' + +export const MobileSidebar: React.FC = ({ + open, + onClose, + onViewChange, + onCreateEvent, + tempUsers, + setTempUsers, + selectedCalendars, + setSelectedCalendars, + currentView, + isIframe, + calendarRef, + onDateChange +}) => { + const { t } = useI18n() + const user = useAppSelector(state => state.user.userData) + + const { + anchorEl, + supportLink, + userMenuAnchorEl, + handleAppMenuOpen, + handleAppMenuClose, + handleUserMenuOpen, + handleUserMenuClose, + handleSettingsClick, + handleLogoutClick + } = useUtilMenus() + + return ( + + {!isIframe && ( + + + + + {supportLink && ( + + + + )} + + + + void handleLogoutClick()} + onUserMenuOpen={handleUserMenuOpen} + isIframe={isIframe} + user={user} + size="s" + /> + + + )} + + + + + + ) +} diff --git a/src/components/Calendar/Sidebar/SideBar.tsx b/src/components/Calendar/Sidebar/SideBar.tsx index 76a3d47..9e00cf4 100644 --- a/src/components/Calendar/Sidebar/SideBar.tsx +++ b/src/components/Calendar/Sidebar/SideBar.tsx @@ -4,6 +4,7 @@ import { Dispatch, MutableRefObject, SetStateAction } from 'react' import { User } from '../../Attendees/PeopleSearch' import { DesktopSidebar } from './DesktopSidebar' import { TabletSidebar } from './TabletSidebar' +import { MobileSidebar } from './MobileSidebar' export interface CalendarSidebarProps { open: boolean @@ -19,6 +20,7 @@ export interface CalendarSidebarProps { tempUsers: User[] setTempUsers: Dispatch> currentView: string + onDateChange?: (date: Date) => void } const Sidebar: React.FC = ( @@ -26,9 +28,11 @@ const Sidebar: React.FC = ( ) => { const { isTablet, isTooSmall: isMobile } = useScreenSizeDetection() - if (isMobile) return null + if (isMobile) { + return + } - return isTablet ? ( + return isTablet || isMobile ? ( ) : ( diff --git a/src/components/Calendar/Sidebar/TabletSidebar.tsx b/src/components/Calendar/Sidebar/TabletSidebar.tsx index 444de47..a505b97 100644 --- a/src/components/Calendar/Sidebar/TabletSidebar.tsx +++ b/src/components/Calendar/Sidebar/TabletSidebar.tsx @@ -1,39 +1,9 @@ -import { - Drawer, - ListItemIcon, - ListItemText, - MenuItem, - MenuList, - useTheme -} from '@linagora/twake-mui' -import CalendarViewDayOutlinedIcon from '@mui/icons-material/CalendarViewDayOutlined' -import CalendarViewMonthOutlinedIcon from '@mui/icons-material/CalendarViewMonthOutlined' -import CalendarViewWeekOutlinedIcon from '@mui/icons-material/CalendarViewWeekOutlined' -import { useI18n } from 'twake-i18n' -import { FieldWithLabel } from '../../Event/components/FieldWithLabel' -import { CALENDAR_VIEWS } from '../utils/constants' +import { Drawer } from '@linagora/twake-mui' import { CalendarSidebarProps } from './SideBar' import { SidebarCommonContent } from './SidebarCommonContent' +import { ViewSwitcher } from './ViewSwitcher' -const VIEW_OPTIONS = [ - { - label: 'menubar.views.day', - value: CALENDAR_VIEWS.timeGridDay, - icon: - }, - { - label: 'menubar.views.week', - value: CALENDAR_VIEWS.timeGridWeek, - icon: - }, - { - label: 'menubar.views.month', - value: CALENDAR_VIEWS.dayGridMonth, - icon: - } -] - -export function TabletSidebar({ +export const TabletSidebar: React.FC = ({ open, onClose, onViewChange, @@ -43,15 +13,7 @@ export function TabletSidebar({ selectedCalendars, setSelectedCalendars, currentView -}: CalendarSidebarProps) { - const { t } = useI18n() - const theme = useTheme() - - const changeViewAndClose = (view: string): void => { - onViewChange(view) - onClose() - } - +}) => { return ( - - - {VIEW_OPTIONS.map(option => { - const isSelected = option.value === currentView - return ( - changeViewAndClose(option.value)} - > - - {option.icon} - - - - ) - })} - - + + }, + { + label: 'menubar.views.week', + value: CALENDAR_VIEWS.timeGridWeek, + icon: + }, + { + label: 'menubar.views.month', + value: CALENDAR_VIEWS.dayGridMonth, + icon: + } +] + +export const ViewSwitcher: React.FC< + Pick +> = ({ onClose, onViewChange, currentView }) => { + const { t } = useI18n() + const theme = useTheme() + + const changeViewAndClose = (view: string): void => { + onViewChange(view) + onClose() + } + + return ( + + + {VIEW_OPTIONS.map(option => { + const isSelected = option.value === currentView + return ( + changeViewAndClose(option.value)} + > + + {option.icon} + + + + ) + })} + + + ) +} diff --git a/src/components/Calendar/hooks/useCalendarViewHandlers.ts b/src/components/Calendar/hooks/useCalendarViewHandlers.ts index 421f9d3..0e9a7dd 100644 --- a/src/components/Calendar/hooks/useCalendarViewHandlers.ts +++ b/src/components/Calendar/hooks/useCalendarViewHandlers.ts @@ -2,7 +2,9 @@ import { useCallback } from 'react' import { createViewHandlers, ViewHandlersProps } from '../handlers/viewHandlers' -export const useCalendarViewHandlers = (props: ViewHandlersProps) => { +export const useCalendarViewHandlers = ( + props: ViewHandlersProps +): ReturnType => { const viewHandlers = createViewHandlers(props) return { diff --git a/src/components/Calendar/hooks/useUtilMenus.ts b/src/components/Calendar/hooks/useUtilMenus.ts new file mode 100644 index 0000000..5974124 --- /dev/null +++ b/src/components/Calendar/hooks/useUtilMenus.ts @@ -0,0 +1,77 @@ +import { useAppDispatch } from '@/app/hooks' +import { setView } from '@/features/Settings/SettingsSlice' +import { Logout } from '@/features/User/oidcAuth' +import { useScreenSizeDetection } from '@/useScreenSizeDetection' +import { redirectTo } from '@/utils/navigation' +import { useEffect, useState } from 'react' + +export const useUtilMenus = (): { + anchorEl: null | HTMLElement + userMenuAnchorEl: null | HTMLElement + supportLink: string + handleAppMenuOpen: (event: React.MouseEvent) => void + handleAppMenuClose: () => void + handleUserMenuOpen: (event: React.MouseEvent) => void + handleUserMenuClose: () => void + handleSettingsClick: () => void + handleLogoutClick: () => Promise +} => { + const [anchorEl, setAnchorEl] = useState(null) + const [userMenuAnchorEl, setUserMenuAnchorEl] = useState( + null + ) + + const supportLink = window.SUPPORT_URL + + const dispatch = useAppDispatch() + const { isTablet, isTooSmall: isMobile } = useScreenSizeDetection() + + useEffect(() => { + const resetMenuAnchorsOnResize = (): void => { + setAnchorEl(null) + setUserMenuAnchorEl(null) + } + resetMenuAnchorsOnResize() + }, [isTablet, isMobile]) + + const handleAppMenuOpen = (event: React.MouseEvent): void => + setAnchorEl(event.currentTarget) + + const handleAppMenuClose = (): void => setAnchorEl(null) + + const handleUserMenuOpen = (event: React.MouseEvent): void => + setUserMenuAnchorEl(event.currentTarget) + + const handleUserMenuClose = (): void => { + setUserMenuAnchorEl(null) + } + + const handleSettingsClick = (): void => { + dispatch(setView('settings')) + handleUserMenuClose() + } + + const handleLogoutClick = async (): Promise => { + try { + const logoutUrl = await Logout() + redirectTo(logoutUrl.href) + } catch (error) { + console.error('Logout failed:', error) + } finally { + sessionStorage.removeItem('tokenSet') + handleUserMenuClose() + } + } + + return { + anchorEl, + userMenuAnchorEl, + supportLink, + handleAppMenuOpen, + handleAppMenuClose, + handleUserMenuOpen, + handleUserMenuClose, + handleSettingsClick, + handleLogoutClick + } +} diff --git a/src/components/Menubar/AppListMenu.tsx b/src/components/Menubar/AppListMenu.tsx index db20091..62be0e4 100644 --- a/src/components/Menubar/AppListMenu.tsx +++ b/src/components/Menubar/AppListMenu.tsx @@ -3,15 +3,12 @@ import WidgetsOutlinedIcon from '@mui/icons-material/WidgetsOutlined' import { useI18n } from 'twake-i18n' import { AppIcon, AppIconProps } from './AppIcon' -export function AppListMenu({ - anchorEl, - onAppMenuOpen, - onAppMenuClose -}: { +export const AppListMenu: React.FC<{ anchorEl: HTMLElement | null onAppMenuOpen: (event: React.MouseEvent) => void onAppMenuClose: () => void -}) { + iconSize?: 'inherit' | 'small' | 'medium' | 'large' +}> = ({ anchorEl, onAppMenuOpen, onAppMenuClose, iconSize = 'inherit' }) => { const { t } = useI18n() const applist: AppIconProps[] = window.appList ?? [] @@ -27,7 +24,7 @@ export function AppListMenu({ aria-label={t('menubar.apps')} title={t('menubar.apps')} > - + @@ -14,33 +15,36 @@ export type MainTitleProps = { onDateChange?: (date: Date) => void } -export function MainTitle({ +export const MainTitle: React.FC = ({ calendarRef, currentView, onViewChange, onDateChange -}: MainTitleProps) { +}: MainTitleProps) => { const { t } = useI18n() const dispatch = useAppDispatch() + const { isTablet, isTooSmall: isMobile } = useScreenSizeDetection() - const handleLogoClick = async () => { + const isDesktop = !isTablet && !isMobile + + const handleLogoClick = (): void => { if (!calendarRef.current) return - await dispatch(setView('calendar')) + dispatch(setView('calendar')) - if (currentView !== CALENDAR_VIEWS.timeGridWeek) { + if (currentView !== CALENDAR_VIEWS.timeGridWeek && isDesktop) { calendarRef.current.changeView(CALENDAR_VIEWS.timeGridWeek) - if (onViewChange) { - onViewChange(CALENDAR_VIEWS.timeGridWeek) - } + onViewChange?.(CALENDAR_VIEWS.timeGridWeek) + } + + if (currentView !== CALENDAR_VIEWS.timeGridDay && !isDesktop) { + calendarRef.current.changeView(CALENDAR_VIEWS.timeGridDay) + onViewChange?.(CALENDAR_VIEWS.timeGridDay) } calendarRef.current.today() - if (onDateChange) { - const newDate = calendarRef.current.getDate() - onDateChange(newDate) - } + onDateChange?.(calendarRef.current.getDate()) } return ( @@ -51,7 +55,7 @@ export function MainTitle({ style={{ background: 'none', border: 0, padding: 0, cursor: 'pointer' }} > {t('menubar.logoAlt')} = ({ const { t } = useI18n() // deliberately NOT using f() const user = useAppSelector(state => state.user.userData) - const supportLink = window.SUPPORT_URL - const [anchorEl, setAnchorEl] = useState(null) - const [userMenuAnchorEl, setUserMenuAnchorEl] = useState( - null - ) + + const { + anchorEl, + userMenuAnchorEl, + supportLink, + handleAppMenuOpen, + handleAppMenuClose, + handleUserMenuOpen, + handleUserMenuClose, + handleSettingsClick, + handleLogoutClick + } = useUtilMenus() + const dispatch = useAppDispatch() const { isTablet, isTooSmall: isMobile } = useScreenSizeDetection() - useEffect(() => { - const resetMenuAnchorsOnResize = (): void => { - setAnchorEl(null) - setUserMenuAnchorEl(null) - } - resetMenuAnchorsOnResize() - }, [isTablet]) - useEffect(() => { if (!user) { dispatch(push('/')) @@ -114,30 +113,6 @@ export const Menubar: React.FC = ({ } } - const handleAppMenuOpen = (event: React.MouseEvent): void => - setAnchorEl(event.currentTarget) - - const handleAppMenuClose = (): void => setAnchorEl(null) - - const handleUserMenuOpen = (event: React.MouseEvent): void => - setUserMenuAnchorEl(event.currentTarget) - - const handleUserMenuClose = (): void => { - setUserMenuAnchorEl(null) - } - - const handleSettingsClick = (): void => { - dispatch(setView('settings')) - handleUserMenuClose() - } - - const handleLogoutClick = async (): Promise => { - 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() @@ -174,6 +149,7 @@ export const Menubar: React.FC = ({ currentDate={currentDate} onDateChange={onDateChange} handleNavigation={handleNavigation} + onOpenSidebar={onToggleSidebar} /> ) } diff --git a/src/components/Menubar/MobileMenuBar.tsx b/src/components/Menubar/MobileMenuBar.tsx index f311c40..3f5ecbc 100644 --- a/src/components/Menubar/MobileMenuBar.tsx +++ b/src/components/Menubar/MobileMenuBar.tsx @@ -2,6 +2,7 @@ import { CalendarApi } from '@fullcalendar/core' import { IconButton, Stack } from '@linagora/twake-mui' import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown' import ArrowDropUpIcon from '@mui/icons-material/ArrowDropUp' +import MenuIcon from '@mui/icons-material/Menu' import React, { useState } from 'react' import { useI18n } from 'twake-i18n' import SearchBar from './EventSearchBar' @@ -15,13 +16,15 @@ export type MobileMenubarProps = { currentDate: Date onDateChange?: (date: Date) => void handleNavigation: (action: 'prev' | 'next' | 'today') => void + onOpenSidebar: () => void } export const MobileMenubar: React.FC = ({ calendarRef, currentDate, onDateChange, - handleNavigation + handleNavigation, + onOpenSidebar }) => { const { t } = useI18n() @@ -37,6 +40,9 @@ export const MobileMenubar: React.FC = ({ <>
+ + +
diff --git a/src/components/Menubar/UserMenu.tsx b/src/components/Menubar/UserMenu.tsx index b382f30..3906adf 100644 --- a/src/components/Menubar/UserMenu.tsx +++ b/src/components/Menubar/UserMenu.tsx @@ -25,6 +25,7 @@ export type UserMenuProps = { onUserMenuOpen: (event: MouseEvent) => void user: userData | null isIframe?: boolean + size?: 's' | 'm' | 'l' } export function UserMenu({ @@ -34,7 +35,8 @@ export function UserMenu({ onLogoutClick, onUserMenuOpen, user, - isIframe = false + isIframe = false, + size = 'm' }: UserMenuProps): JSX.Element { const { t } = useI18n() const theme = useTheme() @@ -50,7 +52,7 @@ export function UserMenu({ title={isIframe ? t('menubar.settings') : t('menubar.userProfile')} > {!isIframe ? ( - + {getInitials(displayName)} ) : ( diff --git a/src/components/Menubar/components/MonthSelector.tsx b/src/components/Menubar/components/MonthSelector.tsx index d20a9cd..0e013e7 100644 --- a/src/components/Menubar/components/MonthSelector.tsx +++ b/src/components/Menubar/components/MonthSelector.tsx @@ -49,6 +49,7 @@ export const MonthSelector: React.FC = ({ key={i} size="large" variant={isSelected ? 'contained' : 'outlined'} + aria-pressed={isSelected} sx={{ borderRadius: radius.md }} diff --git a/src/locales/fr.json b/src/locales/fr.json index eb9b4f6..c65d994 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -232,8 +232,8 @@ }, "menubar": { "today": "Aujourd'hui", - "next": "Précédent", - "prev": "Suivant", + "next": "Suivant", + "prev": "Précédent", "refresh": "Actualiser", "viewSelector": "Sélectionner la vue", "languageSelector": "Sélectionner la langue",