#698 personal settings

This commit is contained in:
lethemanh
2026-04-22 16:15:34 +07:00
parent 7f922cbea1
commit 07efb8ce51
24 changed files with 1127 additions and 453 deletions
@@ -1,15 +1,21 @@
import { Drawer, IconButton, Box } from '@linagora/twake-mui'
import {
Drawer,
Box,
useTheme,
ListItemIcon,
ListItemText,
ListItemButton
} 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 { useAppDispatch, useAppSelector } from '@/app/hooks'
import { useAppDispatch } from '@/app/hooks'
import { useUtilMenus } from '../hooks/useUtilMenus'
import { setIsMobileSearchOpen } from '@/features/Calendars/CalendarSlice'
import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined'
import { UtilButtons } from './UtilButtons'
export const MobileSidebar: React.FC<CalendarSidebarProps> = ({
open,
@@ -26,25 +32,20 @@ export const MobileSidebar: React.FC<CalendarSidebarProps> = ({
onDateChange
}) => {
const { t } = useI18n()
const user = useAppSelector(state => state.user.userData)
const theme = useTheme()
const dispatch = useAppDispatch()
const {
anchorEl,
supportLink,
userMenuAnchorEl,
handleAppMenuOpen,
handleAppMenuClose,
handleUserMenuOpen,
handleUserMenuClose,
handleSettingsClick,
handleLogoutClick
} = useUtilMenus()
const { handleSettingsClick } = useUtilMenus()
const openSearch = (): void => {
dispatch(setIsMobileSearchOpen(true))
}
const openSettings = (): void => {
handleSettingsClick()
onClose()
}
return (
<Drawer
variant="temporary"
@@ -77,39 +78,7 @@ export const MobileSidebar: React.FC<CalendarSidebarProps> = ({
onDateChange={onDateChange}
/>
<Box display="flex" alignItems="center">
{supportLink && (
<IconButton
component="a"
href={supportLink}
target="_blank"
rel="noopener noreferrer"
style={{ marginRight: 4 }}
aria-label={t('menubar.help')}
title={t('menubar.help')}
>
<HelpOutlineIcon fontSize="small" />
</IconButton>
)}
<AppListMenu
anchorEl={anchorEl}
onAppMenuOpen={handleAppMenuOpen}
onAppMenuClose={handleAppMenuClose}
iconSize="small"
/>
<UserMenu
anchorEl={userMenuAnchorEl}
onClose={handleUserMenuClose}
onSettingsClick={handleSettingsClick}
onLogoutClick={() => void handleLogoutClick()}
onUserMenuOpen={handleUserMenuOpen}
isIframe={isIframe}
user={user}
size="s"
/>
</Box>
<UtilButtons isIframe={isIframe} />
</Box>
)}
@@ -127,6 +96,32 @@ export const MobileSidebar: React.FC<CalendarSidebarProps> = ({
setSelectedCalendars={setSelectedCalendars}
openSearchOnMobile={openSearch}
/>
<Box sx={{ marginTop: 1 }}>
<ListItemButton sx={{ padding: 0 }} onClick={openSettings}>
<ListItemIcon sx={{ minWidth: 'unset' }}>
<SettingsOutlinedIcon
sx={{
marginRight: 1,
color: theme.palette.grey.A700,
fontSize: 20
}}
/>
</ListItemIcon>
<ListItemText
primary={t('menubar.settings')}
slotProps={{
primary: {
sx: {
fontSize: '14px',
fontWeight: 500,
lineHeight: '20px'
}
}
}}
/>
</ListItemButton>
</Box>
</Drawer>
)
}
@@ -0,0 +1,60 @@
import { IconButton, Box } from '@linagora/twake-mui'
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 UtilButtons: React.FC<{ isIframe?: boolean }> = ({ isIframe }) => {
const { t } = useI18n()
const user = useAppSelector(state => state.user.userData)
const {
anchorEl,
supportLink,
userMenuAnchorEl,
handleAppMenuOpen,
handleAppMenuClose,
handleUserMenuOpen,
handleUserMenuClose,
handleSettingsClick,
handleLogoutClick
} = useUtilMenus()
return (
<Box display="flex" alignItems="center">
{supportLink && (
<IconButton
component="a"
href={supportLink}
target="_blank"
rel="noopener noreferrer"
style={{ marginRight: 4 }}
aria-label={t('menubar.help')}
title={t('menubar.help')}
>
<HelpOutlineIcon fontSize="small" />
</IconButton>
)}
<AppListMenu
anchorEl={anchorEl}
onAppMenuOpen={handleAppMenuOpen}
onAppMenuClose={handleAppMenuClose}
iconSize="small"
/>
<UserMenu
anchorEl={userMenuAnchorEl}
onClose={handleUserMenuClose}
onSettingsClick={handleSettingsClick}
onLogoutClick={() => void handleLogoutClick()}
onUserMenuOpen={handleUserMenuOpen}
isIframe={isIframe}
user={user}
size="s"
/>
</Box>
)
}
+62 -37
View File
@@ -1,4 +1,5 @@
import { Box } from '@linagora/twake-mui'
import { useScreenSizeDetection } from '@/useScreenSizeDetection'
import { Box, SxProps, Theme } from '@linagora/twake-mui'
import { useI18n } from 'twake-i18n'
interface WeekDaySelectorProps {
@@ -17,27 +18,72 @@ export const FC_DAYS = [
{ fc: 0, ics: 'SU' }
]
export function WeekDaySelector({
const WeekDayStyle = (
isSelected: boolean,
isMobile: boolean,
disabled?: boolean
): SxProps<Theme> => {
const desktopStyle = {
width: 40,
height: 40,
borderRadius: '4px',
border: '1px solid',
borderColor: isSelected ? 'primary.main' : '#AEAEC0',
color: isSelected ? '#fff' : '#8C9CAF',
fontSize: 16,
fontWeight: 400,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
bgcolor: isSelected ? 'primary.main' : 'transparent',
cursor: disabled ? 'default' : 'pointer',
'@media (hover: hover)': {
'&:hover': !disabled
? {
borderColor: 'primary.main',
bgcolor: 'primary.main',
color: '#fff'
}
: undefined
}
} as SxProps<Theme>
if (isMobile) {
return {
...desktopStyle,
width: 200,
justifyContent: 'start',
padding: 1
}
}
return desktopStyle
}
export const WeekDaySelector: React.FC<WeekDaySelectorProps> = ({
selectedDays,
onChange,
disabled
}: WeekDaySelectorProps) {
}) => {
const { t } = useI18n()
const getDayLabel = (ics: string) => {
const { isTooSmall: isMobile } = useScreenSizeDetection()
const getDayLabel = (ics: string, fullLabel: boolean): string => {
const weekdayKey = fullLabel ? 'fullDays' : 'days'
const dayMap: Record<string, string> = {
MO: t('event.repeat.days.monday'),
TU: t('event.repeat.days.tuesday'),
WE: t('event.repeat.days.wednesday'),
TH: t('event.repeat.days.thursday'),
FR: t('event.repeat.days.friday'),
SA: t('event.repeat.days.saturday'),
SU: t('event.repeat.days.sunday')
MO: t(`event.repeat.${weekdayKey}.monday`),
TU: t(`event.repeat.${weekdayKey}.tuesday`),
WE: t(`event.repeat.${weekdayKey}.wednesday`),
TH: t(`event.repeat.${weekdayKey}.thursday`),
FR: t(`event.repeat.${weekdayKey}.friday`),
SA: t(`event.repeat.${weekdayKey}.saturday`),
SU: t(`event.repeat.${weekdayKey}.sunday`)
}
return dayMap[ics] || ics
}
const handleToggle = (fcDay: number) => {
const handleToggle = (fcDay: number): void => {
if (disabled) return
const updated = selectedDays.includes(fcDay)
? selectedDays.filter(d => d !== fcDay)
@@ -46,10 +92,10 @@ export function WeekDaySelector({
}
return (
<Box display="flex" gap={1}>
<Box display="flex" flexDirection={isMobile ? 'column' : 'row'} gap={1}>
{FC_DAYS.map(({ fc, ics }) => {
const isSelected = selectedDays.includes(fc)
const fullLabel = getDayLabel(ics)
const fullLabel = getDayLabel(ics, isMobile)
return (
<Box
@@ -60,30 +106,9 @@ export function WeekDaySelector({
aria-pressed={isSelected}
onClick={() => handleToggle(fc)}
disabled={disabled}
sx={{
width: 40,
height: 40,
borderRadius: '4px',
border: '1px solid',
borderColor: isSelected ? 'primary.main' : '#AEAEC0',
color: isSelected ? '#fff' : '#8C9CAF',
fontSize: 16,
fontWeight: 400,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
bgcolor: isSelected ? 'primary.main' : 'transparent',
cursor: disabled ? 'default' : 'pointer',
'&:hover': !disabled
? {
borderColor: 'primary.main',
bgcolor: 'primary.main',
color: '#fff'
}
: undefined
}}
sx={WeekDayStyle(isSelected, isMobile, disabled)}
>
{fullLabel.charAt(0)}
{isMobile ? fullLabel : fullLabel.charAt(0)}
</Box>
)
})}
+75 -41
View File
@@ -10,6 +10,9 @@ import './Menubar.styl'
import { DatePickerMobile } from './components/DatePickerMobile'
import { Typography } from '@mui/material'
import { SmallNavigationControls } from './components/SmallNavigationControls'
import { useAppDispatch, useAppSelector } from '@/app/hooks'
import { setView } from '@/features/Settings/SettingsSlice'
import ArrowBackIcon from '@mui/icons-material/ArrowBack'
export type MobileMenubarProps = {
calendarRef: React.RefObject<CalendarApi | null>
@@ -27,6 +30,9 @@ export const MobileMenubar: React.FC<MobileMenubarProps> = ({
onOpenSidebar
}) => {
const { t } = useI18n()
const dispatch = useAppDispatch()
const view = useAppSelector(state => state.settings.view)
const [openDatePicker, setOpenDatePicker] = useState(false)
@@ -46,53 +52,81 @@ export const MobileMenubar: React.FC<MobileMenubarProps> = ({
})
}
const handleBackClick = (event: React.MouseEvent): void => {
event.stopPropagation()
event.preventDefault()
dispatch(setView('calendar'))
}
return (
<>
<header className="menubar menubar--mobile">
<div className="left-menu">
<IconButton
onClick={onOpenSidebar}
aria-label={t('menubar.toggleSidebar')}
sx={{ mr: 2 }}
>
<MenuIcon />
</IconButton>
<div className="menu-items">
<SmallNavigationControls onNavigate={handleNavigation} />
</div>
<div className="menu-items">
<Stack direction="row" className="current-date-time">
<Typography
sx={{ lineHeight: 'unset' }}
onClick={onToggleDatePicker}
>
{dateLabel}
</Typography>
{view === 'settings' ? (
<>
<IconButton
onClick={handleBackClick}
aria-label={t('settings.back')}
className="back-button"
>
<ArrowBackIcon />
</IconButton>
<Typography variant="h3" sx={{ width: '100%' }}>
{t('menubar.settings')}
</Typography>
</>
) : (
<>
<div className="left-menu">
<IconButton
size="small"
onClick={onToggleDatePicker}
aria-label={
openDatePicker
? t('menubar.hideDatePicker')
: t('menubar.showDatePicker')
}
title={
openDatePicker
? t('menubar.hideDatePicker')
: t('menubar.showDatePicker')
}
aria-expanded={openDatePicker}
onClick={onOpenSidebar}
aria-label={t('menubar.toggleSidebar')}
sx={{ mr: 2 }}
>
{openDatePicker ? <ArrowDropUpIcon /> : <ArrowDropDownIcon />}
<MenuIcon />
</IconButton>
</Stack>
</div>
</div>
<div className="right-menu">
<div className="search-container">
<SearchBar />
</div>
</div>
<div className="menu-items">
<SmallNavigationControls onNavigate={handleNavigation} />
</div>
<div className="menu-items">
<Stack direction="row" className="current-date-time">
<Typography
sx={{ lineHeight: 'unset' }}
onClick={onToggleDatePicker}
>
{dateLabel}
</Typography>
<IconButton
size="small"
onClick={onToggleDatePicker}
aria-label={
openDatePicker
? t('menubar.hideDatePicker')
: t('menubar.showDatePicker')
}
title={
openDatePicker
? t('menubar.hideDatePicker')
: t('menubar.showDatePicker')
}
aria-expanded={openDatePicker}
>
{openDatePicker ? (
<ArrowDropUpIcon />
) : (
<ArrowDropDownIcon />
)}
</IconButton>
</Stack>
</div>
</div>
<div className="right-menu">
<div className="search-container">
<SearchBar />
</div>
</div>
</>
)}
</header>
{openDatePicker ? (
+107
View File
@@ -0,0 +1,107 @@
import {
Box,
Typography,
styled,
ButtonBase,
SwipeableDrawer
} from '@linagora/twake-mui'
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'
import React, { forwardRef, useImperativeHandle, useState } from 'react'
export interface MobileSelectorHandle {
open: boolean
onClose: () => void
}
const StyledSwipeableDrawer = styled(SwipeableDrawer)(({ theme }) => ({
zIndex: theme.zIndex.modal + 100
}))
const SelectorButton = styled(ButtonBase)(({ theme }) => ({
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: theme.spacing(1.5, 2),
border: `1px solid ${theme.palette.divider}`,
borderRadius: theme.shape.borderRadius,
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.secondary,
textAlign: 'left',
cursor: 'pointer',
transition: 'border-color 0.2s',
'&:hover': {
borderColor: theme.palette.text.primary
}
}))
interface MobileSelectorProps {
displayText: string
children?: React.ReactNode
bottomSheetRef?: React.RefObject<HTMLDivElement>
paperRef?: React.RefObject<HTMLDivElement>
bottomSheetChildren?: (props: {
open: boolean
onClose: () => void
}) => React.ReactNode
}
export const MobileSelector = forwardRef<
MobileSelectorHandle,
MobileSelectorProps
>(
(
{ displayText, children, bottomSheetRef, paperRef, bottomSheetChildren },
ref
) => {
const [open, setOpen] = useState(false)
const onClose = (): void => {
setOpen(false)
}
useImperativeHandle(ref, () => ({
open,
onClose
}))
return (
<>
<SelectorButton onClick={() => setOpen(true)}>
<Typography variant="body1">{displayText}</Typography>
<Box
component={ArrowDropDownIcon}
sx={{
fontSize: 20,
transition: 'transform 0.2s',
transform: open ? 'rotate(180deg)' : 'rotate(0deg)'
}}
/>
</SelectorButton>
{children ? (
<StyledSwipeableDrawer
ref={bottomSheetRef}
anchor="bottom"
open={open}
onClose={onClose}
onOpen={(): void => {}}
disableAutoFocus
slotProps={{
paper: {
ref: paperRef,
sx: { maxHeight: '90dvh' }
}
}}
>
{children}
</StyledSwipeableDrawer>
) : (
bottomSheetChildren?.({ open, onClose })
)}
</>
)
}
)
MobileSelector.displayName = 'MobileSelector'