diff --git a/src/components/Dialog/ResponsiveDialog.tsx b/src/components/Dialog/ResponsiveDialog.tsx index 83959c0..0fc1eb2 100644 --- a/src/components/Dialog/ResponsiveDialog.tsx +++ b/src/components/Dialog/ResponsiveDialog.tsx @@ -10,7 +10,9 @@ import { IconButton, Stack, SxProps, - Theme + Theme, + useMediaQuery, + useTheme } from '@linagora/twake-mui' import ArrowBackIcon from '@mui/icons-material/ArrowBack' import CloseIcon from '@mui/icons-material/Close' @@ -110,35 +112,42 @@ function ResponsiveDialog({ actionsJustifyContent = 'flex-end', sx, ...otherDialogProps -}: ResponsiveDialogProps) { +}: ResponsiveDialogProps): JSX.Element { + const theme = useTheme() + const isMobile = useMediaQuery(theme.breakpoints.down('sm')) + const isInIframe = useMemo(() => new CozyBridge().isInIframe(), []) - const baseSx: SxProps = { - '& .MuiBackdrop-root': { - backgroundColor: 'rgba(0, 0, 0, 0.1)', - opacity: isExpanded ? '0 !important' : undefined, - transition: isExpanded ? 'none !important' : undefined, - pointerEvents: isExpanded ? 'none' : undefined - }, - '& .MuiDialog-paper': { - maxWidth: isExpanded ? '100%' : normalMaxWidth, - width: '100%', - height: isExpanded - ? `calc(100vh - ${isInIframe ? '0px' : headerHeight})` - : undefined, - maxHeight: isExpanded && isInIframe ? '100%' : undefined, - margin: isExpanded ? `${isInIframe ? 0 : headerHeight} 0 0 0` : '32px', - boxShadow: isExpanded ? 'none !important' : undefined, - transition: isExpanded ? 'none !important' : undefined, - zIndex: isExpanded ? 1200 : 1300 - }, - '& .MuiDialogActions-root .MuiBox-root': { - maxWidth: isExpanded ? expandedContentMaxWidth : undefined, - margin: isExpanded ? '0 auto' : undefined, - padding: '0', - width: isExpanded ? '100%' : undefined, - justifyContent: isExpanded ? 'flex-end' : undefined - } - } + + const baseSx: SxProps = isMobile + ? undefined + : { + '& .MuiBackdrop-root': { + opacity: isExpanded ? '0 !important' : undefined, + transition: isExpanded ? 'none !important' : undefined, + pointerEvents: isExpanded ? 'none' : undefined + }, + '& .MuiDialog-paper': { + maxWidth: isExpanded ? '100%' : normalMaxWidth, + width: '100%', + height: isExpanded + ? `calc(100vh - ${isInIframe ? '0px' : headerHeight})` + : undefined, + maxHeight: isExpanded && isInIframe ? '100%' : undefined, + margin: isExpanded + ? `${isInIframe ? 0 : headerHeight} 0 0 0` + : '32px', + boxShadow: isExpanded ? 'none !important' : undefined, + transition: isExpanded ? 'none !important' : undefined, + zIndex: isExpanded ? 1200 : 1300 + }, + '& .MuiDialogActions-root .MuiBox-root': { + maxWidth: isExpanded ? expandedContentMaxWidth : undefined, + margin: isExpanded ? '0 auto' : undefined, + padding: '0', + width: isExpanded ? '100%' : undefined, + justifyContent: isExpanded ? 'flex-end' : undefined + } + } const baseContentSx: SxProps = { width: '100%' @@ -161,7 +170,7 @@ function ResponsiveDialog({ const handleClose = ( event: unknown, reason: 'backdropClick' | 'escapeKeyDown' - ) => { + ): void => { if (isExpanded && reason === 'backdropClick') { return } @@ -175,6 +184,7 @@ function ResponsiveDialog({ open={open} onClose={handleClose} maxWidth={false} + fullScreen={isMobile && open} fullWidth transitionDuration={isExpanded ? 0 : 300} sx={[baseSx, ...(Array.isArray(sx) ? sx : [sx])]} @@ -238,7 +248,7 @@ function ResponsiveDialog({ `1px solid ${theme.palette.divider}` + ? (theme: Theme): string => `1px solid ${theme.palette.divider}` : undefined, justifyContent: actionsJustifyContent }} diff --git a/src/components/Event/InfoRow.tsx b/src/components/Event/InfoRow.tsx index 7985b18..db44f35 100644 --- a/src/components/Event/InfoRow.tsx +++ b/src/components/Event/InfoRow.tsx @@ -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' type InfoRowProps = { @@ -12,14 +18,14 @@ type InfoRowProps = { flexWrap?: React.CSSProperties['flexWrap'] } -function detectUrls(text: string) { +function detectUrls(text: string): JSX.Element[] { // Simple regex that captures whole URLs without splitting them apart const urlRegex = /(https?:\/\/[^\s]+|www\.[^\s]+)/gi const parts = [] let lastIndex = 0 - text.replace(urlRegex, (match, _, offset) => { + text.replace(urlRegex, (match, _, offset: number) => { // Push the text before the match if (lastIndex < offset) { parts.push( @@ -66,10 +72,12 @@ export function InfoRow({ style, alignItems = 'center', flexWrap = 'nowrap' -}: InfoRowProps) { +}: InfoRowProps): JSX.Element { + const theme = useTheme() + const isMobile = useMediaQuery(theme.breakpoints.down('sm')) return ( = ({ onStartTimeChange, onEndDateChange, onEndTimeChange -}) => { +}): JSX.Element => { const { t, lang } = useI18n() + const theme = useTheme() + const isMobile = useMediaQuery(theme.breakpoints.down('sm')) const initialDurationRef = React.useRef(null) const isUserActionRef = React.useRef(false) @@ -139,7 +148,7 @@ export const DateTimeFields: React.FC = ({ ? t('dateTimeFields.date') : t('dateTimeFields.startDate') - const handleStartDateChange = (value: PickerValue) => { + const handleStartDateChange = (value: PickerValue): void => { if (!value || !value.isValid()) return isUserActionRef.current = true @@ -169,7 +178,7 @@ export const DateTimeFields: React.FC = ({ } } - const handleStartTimeChange = (value: PickerValue) => { + const handleStartTimeChange = (value: PickerValue): void => { if (!value || !value.isValid()) return isUserActionRef.current = true @@ -196,7 +205,7 @@ export const DateTimeFields: React.FC = ({ } } - const handleEndDateChange = (value: PickerValue) => { + const handleEndDateChange = (value: PickerValue): void => { if (!value || !value.isValid()) return isUserActionRef.current = true @@ -233,7 +242,7 @@ export const DateTimeFields: React.FC = ({ onEndDateChange(newDateStr) } - const handleEndTimeChange = (value: PickerValue) => { + const handleEndTimeChange = (value: PickerValue): void => { if (!value || !value.isValid()) return isUserActionRef.current = true @@ -270,7 +279,7 @@ export const DateTimeFields: React.FC = ({ testId: string, hasError = false, testLabel?: string - ) => ({ + ): Partial> => ({ textField: { size: 'small' as const, margin: 'dense' as const, @@ -360,7 +369,12 @@ export const DateTimeFields: React.FC = ({ > {isExpanded || shouldShowFullFieldsInNormal ? ( <> - + = ({ )} - + = ({ ) : shouldShowEndDateNormal ? ( - + = ({ ) : ( - - + + = ({ }} /> - - null - }} - slotProps={{ - openPickerButton: { sx: { display: 'none' } }, - popper: { sx: timePickerPopperSx }, - field: getTimeFieldSlotProps( - 'start-time-input', - false, - t('dateTimeFields.startTime') - ) - }} - /> - - {!allday && ( - - - - - )} - - null - }} - slotProps={{ - openPickerButton: { sx: { display: 'none' } }, - popper: { sx: timePickerPopperSx }, - field: getTimeFieldSlotProps( - 'end-time-input', - !!validation.errors.dateTime, - t('dateTimeFields.endTime') - ) - }} - /> + + + null + }} + slotProps={{ + openPickerButton: { sx: { display: 'none' } }, + popper: { sx: timePickerPopperSx }, + field: getTimeFieldSlotProps( + 'start-time-input', + false, + t('dateTimeFields.startTime') + ) + }} + /> + + {!allday && ( + + - + + )} + + null + }} + slotProps={{ + openPickerButton: { sx: { display: 'none' } }, + popper: { sx: timePickerPopperSx }, + field: getTimeFieldSlotProps( + 'end-time-input', + !!validation.errors.dateTime, + t('dateTimeFields.endTime') + ) + }} + /> + )} diff --git a/src/features/Events/AttendanceValidation/AttendanceValidation.tsx b/src/features/Events/AttendanceValidation/AttendanceValidation.tsx index b94779b..20947d1 100644 --- a/src/features/Events/AttendanceValidation/AttendanceValidation.tsx +++ b/src/features/Events/AttendanceValidation/AttendanceValidation.tsx @@ -1,6 +1,6 @@ import { PartStat } from '@/features/User/models/attendee' 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 { useI18n } from 'twake-i18n' import { ContextualizedEvent } from '../EventsTypes' @@ -21,8 +21,10 @@ export function AttendanceValidation({ user, setAfterChoiceFunc, setOpenEditModePopup -}: AttendanceValidationProps) { +}: AttendanceValidationProps): JSX.Element | null { const { currentUserAttendee, isOwn, calendar } = contextualizedEvent + const theme = useTheme() + const isMobile = useMediaQuery(theme.breakpoints.down('sm')) const { t } = useI18n() const [isLoading, setIsLoading] = useState(false) const [loadingValue, setLoadingValue] = useState(null) @@ -50,7 +52,7 @@ export function AttendanceValidation({ return null } - const handleLoadingChange = (loading: boolean, value?: PartStat) => { + const handleLoadingChange = (loading: boolean, value?: PartStat): void => { setIsLoading(loading) setLoadingValue(loading && value ? value : null) } @@ -66,16 +68,34 @@ export function AttendanceValidation({ } return ( - <> - - {calendar.owner?.resource - ? t('eventPreview.authorizeQuestion') - : t('eventPreview.attendingQuestion')} - - - - - + + + {!isMobile && ( + + {calendar.owner?.resource + ? t('eventPreview.authorizeQuestion') + : t('eventPreview.attendingQuestion')} + + )} + + + + + {!contextualizedEvent.isOrganizer && ( - + ) } diff --git a/src/features/Events/AttendanceValidation/EventCounterModal.tsx b/src/features/Events/AttendanceValidation/EventCounterModal.tsx index 9dd1676..e32efd1 100644 --- a/src/features/Events/AttendanceValidation/EventCounterModal.tsx +++ b/src/features/Events/AttendanceValidation/EventCounterModal.tsx @@ -20,7 +20,7 @@ export function EventCounterModal({ open: boolean setOpen: (b: boolean) => void contextualizedEvent: ContextualizedEvent -}) { +}): JSX.Element { const { t } = useI18n() const [isSubmitting, setIsSubmitting] = useState(false) const [showSuccessToast, setShowSuccessToast] = useState(false) @@ -56,7 +56,7 @@ export function EventCounterModal({ errors: { dateTime: '' } }) - const handleStartDateChange = (value: string) => { + const handleStartDateChange = (value: string): void => { setStartDate(value) if (value > endDate) { setEndDate(value) @@ -65,18 +65,18 @@ export function EventCounterModal({ setValidation({ errors: { dateTime: '' } }) } - const handleStartTimeChange = (value: string) => { + const handleStartTimeChange = (value: string): void => { setStartTime(value) setValidation({ errors: { dateTime: '' } }) } - const handleEndDateChange = (value: string) => { + const handleEndDateChange = (value: string): void => { setEndDate(value) setHasEndDateChanged(true) setValidation({ errors: { dateTime: '' } }) } - const handleEndTimeChange = (value: string) => { + const handleEndTimeChange = (value: string): void => { setEndTime(value) setValidation({ errors: { dateTime: '' } }) } @@ -107,7 +107,7 @@ export function EventCounterModal({ return true } - const handleSubmit = async () => { + const handleSubmit = async (): Promise => { if (!validate()) return if ( !contextualizedEvent.currentUserAttendee?.cal_address || @@ -163,6 +163,21 @@ export function EventCounterModal({ open={open} onClose={() => setOpen(false)} title={t('eventPreview.proposeNewTime')} + actions={ + + + + + } > {/* Event title */} @@ -235,21 +250,6 @@ export function EventCounterModal({ }} /> - - {/* Actions */} - - - - ) diff --git a/src/features/Events/EventPreview/EventPreviewAttendees.tsx b/src/features/Events/EventPreview/EventPreviewAttendees.tsx index 6bf4b54..4cd05a7 100644 --- a/src/features/Events/EventPreview/EventPreviewAttendees.tsx +++ b/src/features/Events/EventPreview/EventPreviewAttendees.tsx @@ -1,8 +1,9 @@ import { useAppSelector } from '@/app/hooks' import { useAttendeesFreeBusy } from '@/components/Attendees/useFreeBusy' import { renderAttendeeBadge } from '@/components/Event/utils/eventUtils' +import { useScreenSizeDetection } from '@/useScreenSizeDetection' 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 { alpha, useTheme } from '@mui/material/styles' import { useState } from 'react' @@ -30,17 +31,22 @@ export function EventPreviewAttendees({ end, timezone, eventUid -}: EventPreviewAttendeesProps) { +}: EventPreviewAttendeesProps): JSX.Element { const { t } = useI18n() const theme = useTheme() + const { isTooSmall: isMobile } = useScreenSizeDetection() const infoIconColor = alpha(theme.palette.grey[900], 0.9) 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 [showAllAttendees, setShowAllAttendees] = useState(false) const attendeePreview = makeAttendeePreview(allAttendees, t) - const toFreeBusyAttendee = (a: userAttendee) => ({ + const toFreeBusyAttendee = ( + a: userAttendee + ): { email: string; userId: null } => ({ email: a.cal_address, userId: null }) @@ -60,7 +66,7 @@ export function EventPreviewAttendees({ enabled: !!(start && end && showAllAttendees) }) - const busyCaption = (a: userAttendee) => + const busyCaption = (a: userAttendee): string | undefined => freeBusyMap[a.cal_address] === 'busy' ? a.cal_address === userEmail ? t('event.freeBusy.busyCalOwner') @@ -73,44 +79,100 @@ export function EventPreviewAttendees({ - - - - {t('eventPreview.guests', { count: allAttendees.length })} - - - {attendeePreview} - - - {!showAllAttendees && ( - - {organizer && - renderAttendeeBadge( - organizer, - 'org', - t, - showAllAttendees, - true - )} - {attendees.map((a, idx) => - renderAttendeeBadge(a, idx.toString(), t, showAllAttendees) - )} - - )} - + setShowAllAttendees(prev => !prev)} > - {showAllAttendees - ? t('eventPreview.showLess') - : t('eventPreview.showMore')} - + + + {t('eventPreview.guests', { count: allAttendees.length })} + + + {attendeePreview} + + + {isMobile && showAllAttendees && ( + + )} + + + {!showAllAttendees && ( + + + {organizer && + renderAttendeeBadge( + organizer, + 'org', + t, + showAllAttendees, + true + )} + {attendees.map((a, idx) => + renderAttendeeBadge(a, idx.toString(), t, showAllAttendees) + )} + + + + )} + + {!isMobile && showAllAttendees && ( + + )} diff --git a/src/features/Events/EventPreview/EventPreviewDetails.tsx b/src/features/Events/EventPreview/EventPreviewDetails.tsx index fca8fad..2578da6 100644 --- a/src/features/Events/EventPreview/EventPreviewDetails.tsx +++ b/src/features/Events/EventPreview/EventPreviewDetails.tsx @@ -33,7 +33,14 @@ export function EventPreviewDetails({ const { t } = useI18n() const theme = useTheme() 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( () => @@ -87,13 +94,18 @@ export function EventPreviewDetails({ } return ( - <> + {/* Video */} {event.x_openpass_videoconference && ( + } @@ -136,7 +148,6 @@ export function EventPreviewDetails({ {/* Location */} {event.location && ( @@ -149,7 +160,6 @@ export function EventPreviewDetails({ {/* Resource */} {resources?.length > 0 && ( @@ -203,7 +213,13 @@ export function EventPreviewDetails({ + } @@ -214,7 +230,6 @@ export function EventPreviewDetails({ {/* Alarm */} {event.alarm && ( @@ -237,7 +252,6 @@ export function EventPreviewDetails({ {/* Repetition */} {event.repetition && ( @@ -267,6 +281,6 @@ export function EventPreviewDetails({ error /> )} - + ) } diff --git a/src/features/Events/EventPreview/EventPreviewHeader.tsx b/src/features/Events/EventPreview/EventPreviewHeader.tsx index 29cc2e3..da17806 100644 --- a/src/features/Events/EventPreview/EventPreviewHeader.tsx +++ b/src/features/Events/EventPreview/EventPreviewHeader.tsx @@ -37,7 +37,7 @@ export function EventPreviewHeader({ display="flex" justifyContent="flex-end" alignItems="center" - gap={0.5} + gap={2} width="100%" > {window.DEBUG && ( diff --git a/src/features/Events/EventPreview/EventPreviewModal.tsx b/src/features/Events/EventPreview/EventPreviewModal.tsx index ce12abb..70fb083 100644 --- a/src/features/Events/EventPreview/EventPreviewModal.tsx +++ b/src/features/Events/EventPreview/EventPreviewModal.tsx @@ -92,6 +92,7 @@ export default function EventPreviewModal({ } actionsJustifyContent="center" style={{ overflow: 'auto' }} + titleSx={{ backgroundColor: '#FCFCFC' }} title={