[#682] responsiveness for event preview (#736)

Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
Camille Moussu
2026-04-09 02:35:38 +02:00
committed by GitHub
parent 509b467024
commit 91f0a5526f
9 changed files with 340 additions and 192 deletions
+41 -31
View File
@@ -10,7 +10,9 @@ import {
IconButton, IconButton,
Stack, Stack,
SxProps, SxProps,
Theme Theme,
useMediaQuery,
useTheme
} from '@linagora/twake-mui' } from '@linagora/twake-mui'
import ArrowBackIcon from '@mui/icons-material/ArrowBack' import ArrowBackIcon from '@mui/icons-material/ArrowBack'
import CloseIcon from '@mui/icons-material/Close' import CloseIcon from '@mui/icons-material/Close'
@@ -110,35 +112,42 @@ function ResponsiveDialog({
actionsJustifyContent = 'flex-end', actionsJustifyContent = 'flex-end',
sx, sx,
...otherDialogProps ...otherDialogProps
}: ResponsiveDialogProps) { }: ResponsiveDialogProps): JSX.Element {
const theme = useTheme()
const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
const isInIframe = useMemo(() => new CozyBridge().isInIframe(), []) const isInIframe = useMemo(() => new CozyBridge().isInIframe(), [])
const baseSx: SxProps<Theme> = {
'& .MuiBackdrop-root': { const baseSx: SxProps<Theme> = isMobile
backgroundColor: 'rgba(0, 0, 0, 0.1)', ? undefined
opacity: isExpanded ? '0 !important' : undefined, : {
transition: isExpanded ? 'none !important' : undefined, '& .MuiBackdrop-root': {
pointerEvents: isExpanded ? 'none' : undefined opacity: isExpanded ? '0 !important' : undefined,
}, transition: isExpanded ? 'none !important' : undefined,
'& .MuiDialog-paper': { pointerEvents: isExpanded ? 'none' : undefined
maxWidth: isExpanded ? '100%' : normalMaxWidth, },
width: '100%', '& .MuiDialog-paper': {
height: isExpanded maxWidth: isExpanded ? '100%' : normalMaxWidth,
? `calc(100vh - ${isInIframe ? '0px' : headerHeight})` width: '100%',
: undefined, height: isExpanded
maxHeight: isExpanded && isInIframe ? '100%' : undefined, ? `calc(100vh - ${isInIframe ? '0px' : headerHeight})`
margin: isExpanded ? `${isInIframe ? 0 : headerHeight} 0 0 0` : '32px', : undefined,
boxShadow: isExpanded ? 'none !important' : undefined, maxHeight: isExpanded && isInIframe ? '100%' : undefined,
transition: isExpanded ? 'none !important' : undefined, margin: isExpanded
zIndex: isExpanded ? 1200 : 1300 ? `${isInIframe ? 0 : headerHeight} 0 0 0`
}, : '32px',
'& .MuiDialogActions-root .MuiBox-root': { boxShadow: isExpanded ? 'none !important' : undefined,
maxWidth: isExpanded ? expandedContentMaxWidth : undefined, transition: isExpanded ? 'none !important' : undefined,
margin: isExpanded ? '0 auto' : undefined, zIndex: isExpanded ? 1200 : 1300
padding: '0', },
width: isExpanded ? '100%' : undefined, '& .MuiDialogActions-root .MuiBox-root': {
justifyContent: isExpanded ? 'flex-end' : undefined maxWidth: isExpanded ? expandedContentMaxWidth : undefined,
} margin: isExpanded ? '0 auto' : undefined,
} padding: '0',
width: isExpanded ? '100%' : undefined,
justifyContent: isExpanded ? 'flex-end' : undefined
}
}
const baseContentSx: SxProps<Theme> = { const baseContentSx: SxProps<Theme> = {
width: '100%' width: '100%'
@@ -161,7 +170,7 @@ function ResponsiveDialog({
const handleClose = ( const handleClose = (
event: unknown, event: unknown,
reason: 'backdropClick' | 'escapeKeyDown' reason: 'backdropClick' | 'escapeKeyDown'
) => { ): void => {
if (isExpanded && reason === 'backdropClick') { if (isExpanded && reason === 'backdropClick') {
return return
} }
@@ -175,6 +184,7 @@ function ResponsiveDialog({
open={open} open={open}
onClose={handleClose} onClose={handleClose}
maxWidth={false} maxWidth={false}
fullScreen={isMobile && open}
fullWidth fullWidth
transitionDuration={isExpanded ? 0 : 300} transitionDuration={isExpanded ? 0 : 300}
sx={[baseSx, ...(Array.isArray(sx) ? sx : [sx])]} sx={[baseSx, ...(Array.isArray(sx) ? sx : [sx])]}
@@ -238,7 +248,7 @@ function ResponsiveDialog({
<DialogActions <DialogActions
sx={{ sx={{
borderTop: actionsBorderTop borderTop: actionsBorderTop
? theme => `1px solid ${theme.palette.divider}` ? (theme: Theme): string => `1px solid ${theme.palette.divider}`
: undefined, : undefined,
justifyContent: actionsJustifyContent justifyContent: actionsJustifyContent
}} }}
+15 -7
View File
@@ -1,4 +1,10 @@
import { Box, Link, Typography } from '@linagora/twake-mui' import {
Box,
Link,
Typography,
useTheme,
useMediaQuery
} from '@linagora/twake-mui'
import React from 'react' import React from 'react'
type InfoRowProps = { type InfoRowProps = {
@@ -12,14 +18,14 @@ type InfoRowProps = {
flexWrap?: React.CSSProperties['flexWrap'] flexWrap?: React.CSSProperties['flexWrap']
} }
function detectUrls(text: string) { function detectUrls(text: string): JSX.Element[] {
// Simple regex that captures whole URLs without splitting them apart // Simple regex that captures whole URLs without splitting them apart
const urlRegex = /(https?:\/\/[^\s]+|www\.[^\s]+)/gi const urlRegex = /(https?:\/\/[^\s]+|www\.[^\s]+)/gi
const parts = [] const parts = []
let lastIndex = 0 let lastIndex = 0
text.replace(urlRegex, (match, _, offset) => { text.replace(urlRegex, (match, _, offset: number) => {
// Push the text before the match // Push the text before the match
if (lastIndex < offset) { if (lastIndex < offset) {
parts.push( parts.push(
@@ -66,10 +72,12 @@ export function InfoRow({
style, style,
alignItems = 'center', alignItems = 'center',
flexWrap = 'nowrap' flexWrap = 'nowrap'
}: InfoRowProps) { }: InfoRowProps): JSX.Element {
const theme = useTheme()
const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
return ( return (
<Box <Box
style={{ sx={{
display: 'flex', display: 'flex',
alignItems, alignItems,
gap: 1, gap: 1,
@@ -87,8 +95,8 @@ export function InfoRow({
sx={{ sx={{
wordBreak: 'break-word', wordBreak: 'break-word',
whiteSpace: 'pre-line', whiteSpace: 'pre-line',
maxHeight: '33vh', maxHeight: isMobile ? 'none' : '33vh',
overflowY: 'auto', overflowY: isMobile ? undefined : 'auto',
width: '100%', width: '100%',
...style ...style
}} }}
@@ -1,8 +1,15 @@
import { Box, TextFieldProps, Typography } from '@linagora/twake-mui' import {
Box,
TextFieldProps,
Typography,
useMediaQuery,
useTheme
} from '@linagora/twake-mui'
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
import { import {
DatePicker, DatePicker,
DatePickerFieldProps DatePickerFieldProps,
DatePickerSlotProps
} from '@mui/x-date-pickers/DatePicker' } from '@mui/x-date-pickers/DatePicker'
import { PickerValue } from '@mui/x-date-pickers/internals' import { PickerValue } from '@mui/x-date-pickers/internals'
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider' import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'
@@ -88,8 +95,10 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
onStartTimeChange, onStartTimeChange,
onEndDateChange, onEndDateChange,
onEndTimeChange onEndTimeChange
}) => { }): JSX.Element => {
const { t, lang } = useI18n() const { t, lang } = useI18n()
const theme = useTheme()
const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
const initialDurationRef = React.useRef<number | null>(null) const initialDurationRef = React.useRef<number | null>(null)
const isUserActionRef = React.useRef(false) const isUserActionRef = React.useRef(false)
@@ -139,7 +148,7 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
? t('dateTimeFields.date') ? t('dateTimeFields.date')
: t('dateTimeFields.startDate') : t('dateTimeFields.startDate')
const handleStartDateChange = (value: PickerValue) => { const handleStartDateChange = (value: PickerValue): void => {
if (!value || !value.isValid()) return if (!value || !value.isValid()) return
isUserActionRef.current = true isUserActionRef.current = true
@@ -169,7 +178,7 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
} }
} }
const handleStartTimeChange = (value: PickerValue) => { const handleStartTimeChange = (value: PickerValue): void => {
if (!value || !value.isValid()) return if (!value || !value.isValid()) return
isUserActionRef.current = true isUserActionRef.current = true
@@ -196,7 +205,7 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
} }
} }
const handleEndDateChange = (value: PickerValue) => { const handleEndDateChange = (value: PickerValue): void => {
if (!value || !value.isValid()) return if (!value || !value.isValid()) return
isUserActionRef.current = true isUserActionRef.current = true
@@ -233,7 +242,7 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
onEndDateChange(newDateStr) onEndDateChange(newDateStr)
} }
const handleEndTimeChange = (value: PickerValue) => { const handleEndTimeChange = (value: PickerValue): void => {
if (!value || !value.isValid()) return if (!value || !value.isValid()) return
isUserActionRef.current = true isUserActionRef.current = true
@@ -270,7 +279,7 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
testId: string, testId: string,
hasError = false, hasError = false,
testLabel?: string testLabel?: string
) => ({ ): Partial<DatePickerSlotProps<true>> => ({
textField: { textField: {
size: 'small' as const, size: 'small' as const,
margin: 'dense' as const, margin: 'dense' as const,
@@ -360,7 +369,12 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
> >
{isExpanded || shouldShowFullFieldsInNormal ? ( {isExpanded || shouldShowFullFieldsInNormal ? (
<> <>
<Box display="flex" gap={1} flexDirection="row" alignItems="center"> <Box
display="flex"
gap={1}
flexDirection={isMobile ? 'column' : 'row'}
alignItems="center"
>
<Box sx={{ maxWidth: '300px', width: '48%' }}> <Box sx={{ maxWidth: '300px', width: '48%' }}>
<DatePicker <DatePicker
format={LONG_DATE_FORMAT} format={LONG_DATE_FORMAT}
@@ -407,7 +421,12 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
</Box> </Box>
)} )}
</Box> </Box>
<Box display="flex" gap={1} flexDirection="row" alignItems="center"> <Box
display="flex"
gap={1}
flexDirection={isMobile ? 'column' : 'row'}
alignItems={isMobile ? 'stretch' : 'center'}
>
<Box sx={{ maxWidth: '300px', width: '48%' }}> <Box sx={{ maxWidth: '300px', width: '48%' }}>
<DatePicker <DatePicker
format={LONG_DATE_FORMAT} format={LONG_DATE_FORMAT}
@@ -456,7 +475,12 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
</Box> </Box>
</> </>
) : shouldShowEndDateNormal ? ( ) : shouldShowEndDateNormal ? (
<Box display="flex" gap={1} flexDirection="row" alignItems="center"> <Box
display="flex"
gap={1}
flexDirection={isMobile ? 'column' : 'row'}
alignItems={isMobile ? 'stretch' : 'center'}
>
<Box sx={{ maxWidth: '300px', width: '48%' }}> <Box sx={{ maxWidth: '300px', width: '48%' }}>
<DatePicker <DatePicker
format={LONG_DATE_FORMAT} format={LONG_DATE_FORMAT}
@@ -501,8 +525,15 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
</Box> </Box>
</Box> </Box>
) : ( ) : (
<Box display="flex" gap={1} flexDirection="row" alignItems="center"> <Box
<Box sx={{ maxWidth: '300px', width: '48%' }}> display="flex"
gap={1}
flexDirection={isMobile ? 'column' : 'row'}
alignItems={isMobile ? 'stretch' : 'center'}
>
<Box
sx={isMobile ? undefined : { maxWidth: '300px', width: '48%' }}
>
<DatePicker <DatePicker
format={LONG_DATE_FORMAT} format={LONG_DATE_FORMAT}
value={startDateValue} value={startDateValue}
@@ -519,62 +550,64 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
}} }}
/> />
</Box> </Box>
<Box sx={{ maxWidth: '110px' }}> <Box display="flex" gap={1} flexDirection="row">
<TimePicker <Box sx={{ width: isMobile ? '100%' : '110px' }}>
ampm={false} <TimePicker
value={startTimeValue} ampm={false}
onChange={handleStartTimeChange} value={startTimeValue}
disabled={allday} onChange={handleStartTimeChange}
thresholdToRenderTimeInASingleColumn={48} disabled={allday}
timeSteps={{ minutes: 30 }} thresholdToRenderTimeInASingleColumn={48}
slots={{ timeSteps={{ minutes: 30 }}
field: EditableTimeField, slots={{
actionBar: () => null field: EditableTimeField,
}} actionBar: () => null
slotProps={{ }}
openPickerButton: { sx: { display: 'none' } }, slotProps={{
popper: { sx: timePickerPopperSx }, openPickerButton: { sx: { display: 'none' } },
field: getTimeFieldSlotProps( popper: { sx: timePickerPopperSx },
'start-time-input', field: getTimeFieldSlotProps(
false, 'start-time-input',
t('dateTimeFields.startTime') false,
) t('dateTimeFields.startTime')
}} )
/> }}
</Box> />
{!allday && ( </Box>
<Typography {!allday && (
sx={{ <Typography
alignSelf: 'center', sx={{
mx: 0.5, alignSelf: 'center',
mt: 0.5 mx: 0.5,
}} mt: 0.5
> }}
- >
</Typography> -
)} </Typography>
<Box sx={{ maxWidth: '110px' }}> )}
<TimePicker <Box sx={{ width: isMobile ? '100%' : '110px' }}>
ampm={false} <TimePicker
value={endTimeValue} ampm={false}
onChange={handleEndTimeChange} value={endTimeValue}
disabled={allday} onChange={handleEndTimeChange}
thresholdToRenderTimeInASingleColumn={48} disabled={allday}
timeSteps={{ minutes: 30 }} thresholdToRenderTimeInASingleColumn={48}
slots={{ timeSteps={{ minutes: 30 }}
field: EditableTimeField, slots={{
actionBar: () => null field: EditableTimeField,
}} actionBar: () => null
slotProps={{ }}
openPickerButton: { sx: { display: 'none' } }, slotProps={{
popper: { sx: timePickerPopperSx }, openPickerButton: { sx: { display: 'none' } },
field: getTimeFieldSlotProps( popper: { sx: timePickerPopperSx },
'end-time-input', field: getTimeFieldSlotProps(
!!validation.errors.dateTime, 'end-time-input',
t('dateTimeFields.endTime') !!validation.errors.dateTime,
) t('dateTimeFields.endTime')
}} )
/> }}
/>
</Box>
</Box> </Box>
</Box> </Box>
)} )}
@@ -1,6 +1,6 @@
import { PartStat } from '@/features/User/models/attendee' import { PartStat } from '@/features/User/models/attendee'
import { userData } from '@/features/User/userDataTypes' import { userData } from '@/features/User/userDataTypes'
import { Box, Typography } from '@linagora/twake-mui' import { Box, Typography, useTheme, useMediaQuery } from '@linagora/twake-mui'
import { Dispatch, SetStateAction, useState } from 'react' import { Dispatch, SetStateAction, useState } from 'react'
import { useI18n } from 'twake-i18n' import { useI18n } from 'twake-i18n'
import { ContextualizedEvent } from '../EventsTypes' import { ContextualizedEvent } from '../EventsTypes'
@@ -21,8 +21,10 @@ export function AttendanceValidation({
user, user,
setAfterChoiceFunc, setAfterChoiceFunc,
setOpenEditModePopup setOpenEditModePopup
}: AttendanceValidationProps) { }: AttendanceValidationProps): JSX.Element | null {
const { currentUserAttendee, isOwn, calendar } = contextualizedEvent const { currentUserAttendee, isOwn, calendar } = contextualizedEvent
const theme = useTheme()
const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
const { t } = useI18n() const { t } = useI18n()
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [loadingValue, setLoadingValue] = useState<PartStat | null>(null) const [loadingValue, setLoadingValue] = useState<PartStat | null>(null)
@@ -50,7 +52,7 @@ export function AttendanceValidation({
return null return null
} }
const handleLoadingChange = (loading: boolean, value?: PartStat) => { const handleLoadingChange = (loading: boolean, value?: PartStat): void => {
setIsLoading(loading) setIsLoading(loading)
setLoadingValue(loading && value ? value : null) setLoadingValue(loading && value ? value : null)
} }
@@ -66,16 +68,34 @@ export function AttendanceValidation({
} }
return ( return (
<> <Box
<Typography variant="body2" sx={{ marginRight: 1 }}> sx={{
{calendar.owner?.resource display: 'flex',
? t('eventPreview.authorizeQuestion') flexDirection: isMobile ? 'column' : 'row',
: t('eventPreview.attendingQuestion')} gap: isMobile ? '16px' : undefined,
</Typography> alignItems: 'center'
<Box display="flex" gap={1} mx={1} alignItems="center"> }}
<RSVPButton rsvpValue="ACCEPTED" {...commonButtonProps} /> >
<RSVPButton rsvpValue="DECLINED" {...commonButtonProps} /> <Box
<RSVPButton rsvpValue="TENTATIVE" {...commonButtonProps} /> sx={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap'
}}
>
{!isMobile && (
<Typography variant="body2" sx={{ marginRight: 1 }}>
{calendar.owner?.resource
? t('eventPreview.authorizeQuestion')
: t('eventPreview.attendingQuestion')}
</Typography>
)}
<Box display="flex" gap={1} mx={1} alignItems="center">
<RSVPButton rsvpValue="ACCEPTED" {...commonButtonProps} />
<RSVPButton rsvpValue="DECLINED" {...commonButtonProps} />
<RSVPButton rsvpValue="TENTATIVE" {...commonButtonProps} />
</Box>
</Box> </Box>
{!contextualizedEvent.isOrganizer && ( {!contextualizedEvent.isOrganizer && (
<Typography <Typography
@@ -91,6 +111,6 @@ export function AttendanceValidation({
setOpen={setOpenCounterModal} setOpen={setOpenCounterModal}
contextualizedEvent={contextualizedEvent} contextualizedEvent={contextualizedEvent}
/> />
</> </Box>
) )
} }
@@ -20,7 +20,7 @@ export function EventCounterModal({
open: boolean open: boolean
setOpen: (b: boolean) => void setOpen: (b: boolean) => void
contextualizedEvent: ContextualizedEvent contextualizedEvent: ContextualizedEvent
}) { }): JSX.Element {
const { t } = useI18n() const { t } = useI18n()
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
const [showSuccessToast, setShowSuccessToast] = useState(false) const [showSuccessToast, setShowSuccessToast] = useState(false)
@@ -56,7 +56,7 @@ export function EventCounterModal({
errors: { dateTime: '' } errors: { dateTime: '' }
}) })
const handleStartDateChange = (value: string) => { const handleStartDateChange = (value: string): void => {
setStartDate(value) setStartDate(value)
if (value > endDate) { if (value > endDate) {
setEndDate(value) setEndDate(value)
@@ -65,18 +65,18 @@ export function EventCounterModal({
setValidation({ errors: { dateTime: '' } }) setValidation({ errors: { dateTime: '' } })
} }
const handleStartTimeChange = (value: string) => { const handleStartTimeChange = (value: string): void => {
setStartTime(value) setStartTime(value)
setValidation({ errors: { dateTime: '' } }) setValidation({ errors: { dateTime: '' } })
} }
const handleEndDateChange = (value: string) => { const handleEndDateChange = (value: string): void => {
setEndDate(value) setEndDate(value)
setHasEndDateChanged(true) setHasEndDateChanged(true)
setValidation({ errors: { dateTime: '' } }) setValidation({ errors: { dateTime: '' } })
} }
const handleEndTimeChange = (value: string) => { const handleEndTimeChange = (value: string): void => {
setEndTime(value) setEndTime(value)
setValidation({ errors: { dateTime: '' } }) setValidation({ errors: { dateTime: '' } })
} }
@@ -107,7 +107,7 @@ export function EventCounterModal({
return true return true
} }
const handleSubmit = async () => { const handleSubmit = async (): Promise<void> => {
if (!validate()) return if (!validate()) return
if ( if (
!contextualizedEvent.currentUserAttendee?.cal_address || !contextualizedEvent.currentUserAttendee?.cal_address ||
@@ -163,6 +163,21 @@ export function EventCounterModal({
open={open} open={open}
onClose={() => setOpen(false)} onClose={() => setOpen(false)}
title={t('eventPreview.proposeNewTime')} title={t('eventPreview.proposeNewTime')}
actions={
<Box display="flex" justifyContent="flex-end">
<Button variant="text" onClick={() => setOpen(false)}>
{t('common.cancel')}
</Button>
<Button
variant="contained"
color="primary"
onClick={() => void handleSubmit()}
disabled={isSubmitting}
>
{t('eventPreview.sendProposal')}
</Button>
</Box>
}
> >
{/* Event title */} {/* Event title */}
<Box display="flex" alignItems="center" gap={1} mb={2}> <Box display="flex" alignItems="center" gap={1} mb={2}>
@@ -235,21 +250,6 @@ export function EventCounterModal({
}} }}
/> />
</Box> </Box>
{/* Actions */}
<Box display="flex" justifyContent="flex-end" gap={2} mt={3}>
<Button variant="text" onClick={() => setOpen(false)}>
{t('common.cancel')}
</Button>
<Button
variant="contained"
color="primary"
onClick={handleSubmit}
disabled={isSubmitting}
>
{t('eventPreview.sendProposal')}
</Button>
</Box>
</ResponsiveDialog> </ResponsiveDialog>
</> </>
) )
@@ -1,8 +1,9 @@
import { useAppSelector } from '@/app/hooks' import { useAppSelector } from '@/app/hooks'
import { useAttendeesFreeBusy } from '@/components/Attendees/useFreeBusy' import { useAttendeesFreeBusy } from '@/components/Attendees/useFreeBusy'
import { renderAttendeeBadge } from '@/components/Event/utils/eventUtils' import { renderAttendeeBadge } from '@/components/Event/utils/eventUtils'
import { useScreenSizeDetection } from '@/useScreenSizeDetection'
import { extractEventBaseUuid } from '@/utils/extractEventBaseUuid' import { extractEventBaseUuid } from '@/utils/extractEventBaseUuid'
import { AvatarGroup, Box, Typography } from '@linagora/twake-mui' import { AvatarGroup, Box, Button, Typography } from '@linagora/twake-mui'
import PeopleAltOutlinedIcon from '@mui/icons-material/PeopleAltOutlined' import PeopleAltOutlinedIcon from '@mui/icons-material/PeopleAltOutlined'
import { alpha, useTheme } from '@mui/material/styles' import { alpha, useTheme } from '@mui/material/styles'
import { useState } from 'react' import { useState } from 'react'
@@ -30,17 +31,22 @@ export function EventPreviewAttendees({
end, end,
timezone, timezone,
eventUid eventUid
}: EventPreviewAttendeesProps) { }: EventPreviewAttendeesProps): JSX.Element {
const { t } = useI18n() const { t } = useI18n()
const theme = useTheme() const theme = useTheme()
const { isTooSmall: isMobile } = useScreenSizeDetection()
const infoIconColor = alpha(theme.palette.grey[900], 0.9) const infoIconColor = alpha(theme.palette.grey[900], 0.9)
const infoIconSx = { minWidth: '25px', marginRight: 2, color: infoIconColor } const infoIconSx = { minWidth: '25px', marginRight: 2, color: infoIconColor }
// Icon takes 25px width + 16px (mr: 2) = 41px, use negative margin to align avatars
const mobileAvatarOffset = '-42px'
const userEmail = useAppSelector(state => state.user.userData.email) const userEmail = useAppSelector(state => state.user.userData.email)
const [showAllAttendees, setShowAllAttendees] = useState(false) const [showAllAttendees, setShowAllAttendees] = useState(false)
const attendeePreview = makeAttendeePreview(allAttendees, t) const attendeePreview = makeAttendeePreview(allAttendees, t)
const toFreeBusyAttendee = (a: userAttendee) => ({ const toFreeBusyAttendee = (
a: userAttendee
): { email: string; userId: null } => ({
email: a.cal_address, email: a.cal_address,
userId: null userId: null
}) })
@@ -60,7 +66,7 @@ export function EventPreviewAttendees({
enabled: !!(start && end && showAllAttendees) enabled: !!(start && end && showAllAttendees)
}) })
const busyCaption = (a: userAttendee) => const busyCaption = (a: userAttendee): string | undefined =>
freeBusyMap[a.cal_address] === 'busy' freeBusyMap[a.cal_address] === 'busy'
? a.cal_address === userEmail ? a.cal_address === userEmail
? t('event.freeBusy.busyCalOwner') ? t('event.freeBusy.busyCalOwner')
@@ -73,44 +79,100 @@ export function EventPreviewAttendees({
<Box sx={{ ...infoIconSx, mt: 1 }}> <Box sx={{ ...infoIconSx, mt: 1 }}>
<PeopleAltOutlinedIcon /> <PeopleAltOutlinedIcon />
</Box> </Box>
<Box style={{ marginBottom: 1, display: 'flex', flexDirection: 'row' }}> <Box
<Box sx={{ marginRight: 2 }}> sx={{
<Typography> marginBottom: 1,
{t('eventPreview.guests', { count: allAttendees.length })} display: 'flex',
</Typography> flexDirection: isMobile ? 'column' : 'row',
<Typography sx={{ fontSize: '13px', color: 'text.secondary' }}> gap: isMobile ? 2 : undefined
{attendeePreview} }}
</Typography> >
</Box> <Box
{!showAllAttendees && (
<AvatarGroup max={ATTENDEE_DISPLAY_LIMIT}>
{organizer &&
renderAttendeeBadge(
organizer,
'org',
t,
showAllAttendees,
true
)}
{attendees.map((a, idx) =>
renderAttendeeBadge(a, idx.toString(), t, showAllAttendees)
)}
</AvatarGroup>
)}
<Typography
sx={{ sx={{
cursor: 'pointer', marginRight: 2,
marginLeft: 2, display: 'flex',
fontSize: '14px', alignItems: 'center',
color: 'text.secondary', justifyContent: 'space-between'
alignSelf: 'center'
}} }}
onClick={() => setShowAllAttendees(prev => !prev)}
> >
{showAllAttendees <Box>
? t('eventPreview.showLess') <Typography>
: t('eventPreview.showMore')} {t('eventPreview.guests', { count: allAttendees.length })}
</Typography> </Typography>
<Typography sx={{ fontSize: '13px', color: 'text.secondary' }}>
{attendeePreview}
</Typography>
</Box>
{isMobile && showAllAttendees && (
<Button
variant="text"
size="small"
sx={{
marginLeft: 2,
fontSize: '14px',
color: 'text.secondary',
alignSelf: 'center'
}}
onClick={() => setShowAllAttendees(false)}
>
{t('eventPreview.showLess')}
</Button>
)}
</Box>
{!showAllAttendees && (
<Box
sx={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
ml: isMobile ? mobileAvatarOffset : undefined
}}
>
<AvatarGroup max={ATTENDEE_DISPLAY_LIMIT}>
{organizer &&
renderAttendeeBadge(
organizer,
'org',
t,
showAllAttendees,
true
)}
{attendees.map((a, idx) =>
renderAttendeeBadge(a, idx.toString(), t, showAllAttendees)
)}
</AvatarGroup>
<Button
variant="text"
size="small"
sx={{
marginLeft: 2,
fontSize: '14px',
color: 'text.secondary',
alignSelf: 'center'
}}
onClick={() => setShowAllAttendees(true)}
>
{t('eventPreview.showMore')}
</Button>
</Box>
)}
{!isMobile && showAllAttendees && (
<Button
variant="text"
size="small"
sx={{
marginLeft: 2,
fontSize: '14px',
color: 'text.secondary',
alignSelf: 'center'
}}
onClick={() => setShowAllAttendees(false)}
>
{t('eventPreview.showLess')}
</Button>
)}
</Box> </Box>
</Box> </Box>
@@ -33,7 +33,14 @@ export function EventPreviewDetails({
const { t } = useI18n() const { t } = useI18n()
const theme = useTheme() const theme = useTheme()
const infoIconColor = alpha(theme.palette.grey[900], 0.9) const infoIconColor = alpha(theme.palette.grey[900], 0.9)
const infoIconSx = { minWidth: '25px', marginRight: 2, color: infoIconColor } const infoIconSx = {
minWidth: '25px',
marginRight: 2,
color: infoIconColor,
display: 'flex',
alignItems: 'center',
alignSelf: 'center'
}
const resources = useMemo( const resources = useMemo(
() => () =>
@@ -87,13 +94,18 @@ export function EventPreviewDetails({
} }
return ( return (
<> <Box
sx={{
display: 'flex',
gap: '16px',
flexDirection: 'column'
}}
>
{/* Video */} {/* Video */}
{event.x_openpass_videoconference && ( {event.x_openpass_videoconference && (
<InfoRow <InfoRow
alignItems="flex-start"
icon={ icon={
<Box sx={{ ...infoIconSx, mt: 1 }}> <Box sx={infoIconSx}>
<VideocamOutlinedIcon /> <VideocamOutlinedIcon />
</Box> </Box>
} }
@@ -136,7 +148,6 @@ export function EventPreviewDetails({
{/* Location */} {/* Location */}
{event.location && ( {event.location && (
<InfoRow <InfoRow
alignItems="flex-start"
icon={ icon={
<Box sx={infoIconSx}> <Box sx={infoIconSx}>
<LocationOnOutlinedIcon /> <LocationOnOutlinedIcon />
@@ -149,7 +160,6 @@ export function EventPreviewDetails({
{/* Resource */} {/* Resource */}
{resources?.length > 0 && ( {resources?.length > 0 && (
<InfoRow <InfoRow
alignItems="flex-start"
flexWrap="wrap" flexWrap="wrap"
icon={ icon={
<Box sx={infoIconSx}> <Box sx={infoIconSx}>
@@ -203,7 +213,13 @@ export function EventPreviewDetails({
<InfoRow <InfoRow
alignItems="flex-start" alignItems="flex-start"
icon={ icon={
<Box sx={infoIconSx}> <Box
sx={{
...infoIconSx,
alignItems: 'flex-start',
alignSelf: 'flex-start'
}}
>
<SubjectIcon /> <SubjectIcon />
</Box> </Box>
} }
@@ -214,7 +230,6 @@ export function EventPreviewDetails({
{/* Alarm */} {/* Alarm */}
{event.alarm && ( {event.alarm && (
<InfoRow <InfoRow
alignItems="flex-start"
icon={ icon={
<Box sx={infoIconSx}> <Box sx={infoIconSx}>
<NotificationsNoneIcon /> <NotificationsNoneIcon />
@@ -237,7 +252,6 @@ export function EventPreviewDetails({
{/* Repetition */} {/* Repetition */}
{event.repetition && ( {event.repetition && (
<InfoRow <InfoRow
alignItems="flex-start"
icon={ icon={
<Box sx={infoIconSx}> <Box sx={infoIconSx}>
<RepeatIcon /> <RepeatIcon />
@@ -267,6 +281,6 @@ export function EventPreviewDetails({
error error
/> />
)} )}
</> </Box>
) )
} }
@@ -37,7 +37,7 @@ export function EventPreviewHeader({
display="flex" display="flex"
justifyContent="flex-end" justifyContent="flex-end"
alignItems="center" alignItems="center"
gap={0.5} gap={2}
width="100%" width="100%"
> >
{window.DEBUG && ( {window.DEBUG && (
@@ -92,6 +92,7 @@ export default function EventPreviewModal({
} }
actionsJustifyContent="center" actionsJustifyContent="center"
style={{ overflow: 'auto' }} style={{ overflow: 'auto' }}
titleSx={{ backgroundColor: '#FCFCFC' }}
title={ title={
<EventPreviewHeader <EventPreviewHeader
event={event} event={event}