[#692] split search result page to mobile and desktop (#823)

Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
Camille Moussu
2026-04-29 09:55:17 +02:00
committed by GitHub
parent 29d82e5ade
commit d66bc59967
7 changed files with 811 additions and 466 deletions
+6 -4
View File
@@ -416,6 +416,10 @@ const CalendarApp: React.FC<CalendarAppProps> = ({
<div className="calendar">
<ImportAlert />
{menubarProps?.isIframe && <Menubar {...menubarProps} />}
{view === 'calendar' && (
<>
{' '}
{(isTablet || isMobile) && (
<Fab
color="primary"
@@ -432,7 +436,6 @@ const CalendarApp: React.FC<CalendarAppProps> = ({
<AddIcon />
</Fab>
)}
{view === 'calendar' && (
<div
ref={calendarWrapperRef}
className={isTablet || isMobile ? 'calendar-swipe-container' : ''}
@@ -445,9 +448,7 @@ const CalendarApp: React.FC<CalendarAppProps> = ({
<FullCalendar
key={hiddenDays.join(',')}
ref={ref => {
if (ref) {
calendarRef.current = ref.getApi()
}
calendarRef.current = ref ? ref.getApi() : null
}}
plugins={[
dayGridPlugin,
@@ -622,6 +623,7 @@ const CalendarApp: React.FC<CalendarAppProps> = ({
eventDidMount={viewHandlers.handleEventDidMount}
/>
</div>
</>
)}
{view === 'search' && <SearchResultsPage />}
<EventPopover
+74 -247
View File
@@ -1,79 +1,23 @@
import { useAppDispatch, useAppSelector } from '@/app/hooks'
import { AppDispatch } from '@/app/store'
import { defaultColors } from '@/utils/defaultColors'
import { browserDefaultTimeZone } from '@/utils/timezone'
import {
Box,
Button,
CircularProgress,
IconButton,
Stack,
Typography
} from '@linagora/twake-mui'
import ArrowBackIcon from '@mui/icons-material/ArrowBack'
import RepeatIcon from '@mui/icons-material/Repeat'
import SquareRoundedIcon from '@mui/icons-material/SquareRounded'
import VideocamIcon from '@mui/icons-material/Videocam'
import { useState } from 'react'
import { useI18n } from 'twake-i18n'
import logo from '../../static/noResult-logo.svg'
import { getEventAsync } from '../Calendars/services'
import EventPreviewModal from '@/features/Events/EventPreview'
import { CalendarEvent } from '../Events/EventsTypes'
import { defaultColors } from '@/utils/defaultColors'
import { Box, IconButton, Typography } from '@linagora/twake-mui'
import ArrowBackIcon from '@mui/icons-material/ArrowBack'
import SquareRoundedIcon from '@mui/icons-material/SquareRounded'
import { useI18n } from 'twake-i18n'
import { setView } from '../Settings/SettingsSlice'
import { ResultsList } from './ResultsList'
import './searchResult.styl'
import {
RenderDate,
RenderText,
RenderOrganizer,
RenderTime,
RenderTitle,
RenderVideoJoin
} from './searchResultsComponents'
import { SearchEventResult } from './types/SearchEventResult'
const styles = {
M3BodyLarge: {
fontFamily: 'Roboto',
fontWeight: 400,
fontStyle: 'normal',
fontSize: '22px',
lineHeight: '28px',
letterSpacing: '0%',
color: '#243B55'
},
M3BodyMedium1: {
fontFamily: 'Inter',
fontWeight: 400,
fontStyle: 'normal',
fontSize: '16px',
lineHeight: '24px',
letterSpacing: '-0.15px',
color: '#243B55'
},
M3BodyMedium: {
fontFamily: 'Roboto',
fontWeight: 400,
fontStyle: 'normal',
fontSize: '14px',
lineHeight: '20px',
letterSpacing: '0.25px',
verticalAlign: 'middle',
color: '#8C9CAF'
},
M3BodyMedium3: {
fontFamily: 'Inter',
fontWeight: 400,
fontSize: '14px',
lineHeight: '20px',
letterSpacing: '0.25px',
verticalAlign: 'middle',
color: '#8C9CAF'
},
M3TitleMedium: {
fontFamily: 'Roboto',
fontWeight: 500,
fontStyle: 'medium',
fontSize: '16px',
lineHeight: '24px',
letterSpacing: '0.15px',
textAlign: 'center',
verticalAlign: 'middle',
color: '#243B55'
}
}
import { useEventPreview } from './useEventPreview'
export default function DesktopSearchResultsPage(): JSX.Element {
const { t } = useI18n()
@@ -82,48 +26,6 @@ export default function DesktopSearchResultsPage(): JSX.Element {
state => state.searchResult
)
let layout
if (loading) {
layout = (
<Box className="loading">
<CircularProgress size={32} />
</Box>
)
} else if (error) {
layout = (
<Box className="error">
<Typography className="error-text">{error}</Typography>
</Box>
)
} else if (!hits) {
layout = (
<Box className="noResults">
<img className="logo" src={logo} alt={t('search.noResults')} />
<Typography sx={styles.M3TitleMedium}>
{t('search.noResults')}
</Typography>
<Typography sx={styles.M3BodyMedium}>
{t('search.noResultsSubtitle')}
</Typography>
</Box>
)
} else {
layout = (
<Box className="search-result-content-body">
<Stack sx={{ mt: 2 }}>
{results.map((r: SearchEventResult, idx: number) => (
<ResultItem
key={`row-${idx}-event-${r.data.uid}`}
eventData={r}
dispatch={dispatch}
/>
))}
</Stack>
</Box>
)
}
return (
<Box className="search-layout">
<Box className="search-result-content-header">
@@ -137,23 +39,39 @@ export default function DesktopSearchResultsPage(): JSX.Element {
<Typography variant="h5">{t('search.resultsTitle')}</Typography>
</Box>
</Box>
{layout}
<ResultsList
loading={loading}
error={error}
hits={hits}
results={results}
renderItem={(result, idx) => (
<DesktopResultItem
key={`row-${idx}-event-${result.data.uid}`}
eventData={result}
/>
)}
noResultsTitleSx={{
fontWeight: 500,
textAlign: 'center',
color: 'text.primary'
}}
noResultsSubtitleSx={{ color: 'text.secondary' }}
stackSx={{ mt: 2 }}
/>
</Box>
)
}
function ResultItem({
eventData,
dispatch
function DesktopResultItem({
eventData
}: {
eventData: SearchEventResult
dispatch: AppDispatch
}): JSX.Element {
const { t } = useI18n()
const startDate = new Date(eventData.data.start)
const endDate = eventData.data.end ? new Date(eventData.data.end) : startDate
const timeZone =
useAppSelector(state => state.settings.timeZone) ?? browserDefaultTimeZone
const calendar = useAppSelector(
state =>
state.calendars.list[
@@ -162,32 +80,10 @@ function ResultItem({
)
const calendarColor = calendar?.color?.light
const [openPreview, setOpenPreview] = useState(false)
const handleOpenResult = async (
eventData: SearchEventResult
): Promise<void> => {
if (calendar) {
const event = {
URL: eventData._links.self.href,
calId: calendar.id,
uid: eventData.data.uid,
start: eventData.data.start,
end: eventData.data.end,
allday: eventData.data.allDay,
attendee: eventData.data.attendees,
class: eventData.data.class,
description: eventData.data.description,
stamp: eventData.data.dtstamp,
location: eventData.data.location,
organizer: eventData.data.organizer,
title: eventData.data.summary,
timezone: timeZone
} as CalendarEvent
await dispatch(getEventAsync(event))
setOpenPreview(true)
}
}
const { openPreview, setOpenPreview, handleOpen, timeZone } = useEventPreview(
eventData,
calendar
)
return (
<>
@@ -197,48 +93,29 @@ function ResultItem({
flexDirection: 'row',
gap: 2,
p: 3,
borderTop: '1px solid #F3F6F9',
borderTop: '1px solid',
borderColor: 'divider',
cursor: 'pointer',
'&:hover': { backgroundColor: '#e7e7e7ff' },
'&:hover': { backgroundColor: 'action.hover' },
alignItems: 'center',
textAlign: 'left',
maxWidth: '80vw'
}}
onClick={() => void handleOpenResult(eventData)}
onClick={() => void handleOpen()}
>
<Typography sx={{ ...styles.M3BodyLarge, minWidth: '90px' }}>
{startDate.toLocaleDateString(t('locale'), {
day: '2-digit',
month: 'short',
timeZone
})}
{startDate.toDateString() !== endDate.toDateString() && (
<>
{' - '}
{endDate.toLocaleDateString(t('locale'), {
day: '2-digit',
month: 'short',
timeZone
})}
</>
)}
</Typography>
{!eventData.data.allDay && (
<Typography sx={{ ...styles.M3BodyMedium1, minWidth: '120px' }}>
{startDate.toLocaleTimeString(t('locale'), {
hour: '2-digit',
minute: '2-digit',
timeZone
})}
-
{endDate.toLocaleTimeString(t('locale'), {
hour: '2-digit',
minute: '2-digit',
timeZone
})}
</Typography>
)}
<RenderDate
startDate={startDate}
endDate={endDate}
t={t}
timeZone={timeZone}
/>
<RenderTime
startDate={startDate}
endDate={endDate}
allDay={!!eventData.data.allDay}
t={t}
timeZone={timeZone}
/>
<SquareRoundedIcon
style={{
color: calendarColor ?? defaultColors[0].light,
@@ -247,72 +124,22 @@ function ResultItem({
flexShrink: 0
}}
/>
<Box display="flex" flexDirection="row" gap={1} sx={{ minWidth: 0 }}>
<Typography sx={styles.M3BodyLarge}>
{eventData.data.summary || t('event.untitled')}
</Typography>
{eventData.data.isRecurrentMaster && <RepeatIcon />}
<RenderTitle
summary={eventData.data.summary}
isRecurrent={!!eventData.data.isRecurrentMaster}
t={t}
/>
<RenderOrganizer organizer={eventData.data.organizer} />
<RenderText text={eventData.data.location} />
<RenderText text={eventData.data.description} />
<RenderVideoJoin
t={t}
url={eventData.data['x-openpaas-videoconference']}
/>
</Box>
{(eventData.data.organizer?.cn || eventData.data.organizer?.email) && (
<Typography
sx={{
...styles.M3BodyMedium1,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
minWidth: '150px',
maxWidth: '150px'
}}
>
{eventData.data.organizer.cn || eventData.data.organizer.email}
</Typography>
)}
{eventData.data?.location && (
<Typography
sx={{
...styles.M3BodyMedium,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
minWidth: '150px',
maxWidth: '250px'
}}
>
{eventData.data.location}
</Typography>
)}
{eventData.data?.description && (
<Typography
sx={{
...styles.M3BodyMedium3,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
flex: 1,
minWidth: 0
}}
>
{eventData.data.description.replace(/\n/g, ' ')}
</Typography>
)}
{eventData.data['x-openpaas-videoconference'] && (
<Button
startIcon={<VideocamIcon />}
sx={{ flexShrink: 0, ml: 'auto' }}
onClick={e => {
e.stopPropagation()
window.open(
eventData.data['x-openpaas-videoconference'],
'_blank',
'noopener,noreferrer'
)
}}
>
{t('eventPreview.joinVideoShort')}
</Button>
)}
</Box>
{calendar && calendar.events[eventData.data.uid] && (
{calendar?.events?.[eventData.data.uid] && (
<EventPreviewModal
eventId={eventData.data.uid}
calId={calendar.id}
@@ -1,10 +1,19 @@
import { useAppSelector } from '@/app/hooks'
import EventPreviewModal from '@/features/Events/EventPreview'
import { Box } from '@linagora/twake-mui'
import { useI18n } from 'twake-i18n'
import { AttendeesFilter } from './AttendeesFilter'
import DesktopSearchResultsPage from './DesktopSearchResultsPage'
import { normalizeCalendars } from './calendarColorUtils'
import { OrganizersFilter } from './OrganizersFilter'
import { ResultsList } from './ResultsList'
import { SearchInFilter } from './SearchInFilter'
import './searchResult.styl'
import {
RenderMobileDate,
RenderMobileEventCard
} from './searchResultsComponents'
import { SearchEventResult } from './types/SearchEventResult'
import { useEventPreview } from './useEventPreview'
const MobileSearchResultsPage: React.FC = () => {
const searchResults = useAppSelector(state => state.searchResult)
@@ -15,21 +24,46 @@ const MobileSearchResultsPage: React.FC = () => {
searchResults.searchParams.filters.attendees.length > 0
const displaySearch =
(!!searchResults.hits || !!searchResults.error || searchResults.loading) &&
!!searchResults.hits ||
!!searchResults.error ||
searchResults.loading ||
hasSearchParams
return (
<>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
height: '100%',
overflow: 'hidden'
}}
>
<FiltersButtons />
{displaySearch && <DesktopSearchResultsPage />}
</>
{displaySearch && (
<Box sx={{ m: 2, flex: 1, minHeight: 0, overflow: 'auto' }}>
<ResultsList
loading={searchResults.loading}
error={searchResults.error}
hits={searchResults.hits}
results={searchResults.results}
renderItem={(result, idx) => (
<MobileResultItem
key={`row-${idx}-event-${result.data.uid}`}
eventData={result}
/>
)}
noResultsTitleSx={{ variant: 'h5' }}
noResultsSubtitleSx={{ variant: 'subtitle1' }}
/>
</Box>
)}
</Box>
)
}
export default MobileSearchResultsPage
const FiltersButtons: React.FC = () => {
return (
const FiltersButtons: React.FC = () => (
<Box
sx={{
display: 'flex',
@@ -48,4 +82,56 @@ const FiltersButtons: React.FC = () => {
<AttendeesFilter mode="mobile" />
</Box>
)
const MobileResultItem: React.FC<{ eventData: SearchEventResult }> = ({
eventData
}) => {
const { t } = useI18n()
const rawCalendars = useAppSelector(state => state.calendars.list)
const calendars = normalizeCalendars(rawCalendars)
const calId = `${eventData.data.userId}/${eventData.data.calendarId}`
const calendar = calendars[calId]
const { openPreview, setOpenPreview, handleOpen, timeZone } = useEventPreview(
eventData,
calendar
)
const startDate = new Date(eventData.data.start)
return (
<>
<Box
sx={{
display: 'grid',
gridTemplateColumns: '10%',
gridAutoFlow: 'column',
gap: 2,
pt: 1,
cursor: 'pointer',
alignItems: 'center',
textAlign: 'left'
}}
onClick={() => void handleOpen()}
>
<RenderMobileDate startDate={startDate} t={t} timeZone={timeZone} />
<RenderMobileEventCard
eventData={eventData}
calendar={calendar}
timeZone={timeZone}
/>
</Box>
{calendar?.events?.[eventData.data.uid] && (
<EventPreviewModal
eventId={eventData.data.uid}
calId={calendar.id}
open={openPreview}
onClose={() => setOpenPreview(false)}
/>
)}
</>
)
}
+73
View File
@@ -0,0 +1,73 @@
import { Box, CircularProgress, Stack, Typography } from '@linagora/twake-mui'
import { useI18n } from 'twake-i18n'
import logo from '../../static/noResult-logo.svg'
import { SearchEventResult } from './types/SearchEventResult'
import { SxProps } from '@mui/material'
interface ResultsListProps {
loading: boolean
error: string | null
hits: number | null
results: SearchEventResult[]
renderItem: (result: SearchEventResult, idx: number) => React.ReactNode
noResultsTitleSx?: SxProps
noResultsSubtitleSx?: SxProps
stackSx?: SxProps
}
export const ResultsList: React.FC<ResultsListProps> = ({
loading,
error,
hits,
results,
renderItem,
noResultsTitleSx,
noResultsSubtitleSx,
stackSx
}) => {
const { t } = useI18n()
if (loading) {
return (
<Box className="loading">
<CircularProgress size={32} />
</Box>
)
}
if (error) {
return (
<Box className="error">
<Typography className="error-text">{error}</Typography>
</Box>
)
}
if (!hits) {
return (
<Box className="noResults">
<img className="logo" src={logo} alt={t('search.noResults')} />
<Typography
sx={noResultsTitleSx}
variant={noResultsTitleSx ? undefined : 'h5'}
>
{t('search.noResults')}
</Typography>
<Typography
sx={noResultsSubtitleSx}
variant={noResultsSubtitleSx ? undefined : 'subtitle1'}
>
{t('search.noResultsSubtitle')}
</Typography>
</Box>
)
}
return (
<Box className="search-result-content-body">
<Stack sx={stackSx}>
{results.map((result, idx) => renderItem(result, idx))}
</Stack>
</Box>
)
}
+39
View File
@@ -0,0 +1,39 @@
import { Calendar } from '@/features/Calendars/CalendarTypes'
export type NormalizedColor = { light: string; dark: string }
export type NormalizedCalendar = Omit<Calendar, 'color'> & {
color: NormalizedColor
}
/**
* EventChip expects calendar.color to be { light: string; dark: string }.
* The Redux store may hold color as a plain string — normalize it here.
*/
export function normalizeCalendarColor(
color: Calendar['color']
): NormalizedColor {
if (
color &&
typeof color === 'object' &&
'light' in color &&
'dark' in color
) {
return color as NormalizedColor
}
const hex = typeof color === 'string' ? color : '#ffffff'
return { light: hex, dark: hex }
}
/**
* Returns a copy of the calendars map with every color normalized to { light, dark }.
*/
export function normalizeCalendars(
calendars: Record<string, Calendar>
): Record<string, NormalizedCalendar> {
return Object.fromEntries(
Object.entries(calendars).map(([key, cal]) => [
key,
{ ...cal, color: normalizeCalendarColor(cal.color) }
])
)
}
@@ -0,0 +1,272 @@
import {
getBestColor,
getTitleStyle
} from '@/components/Event/EventChip/EventChipUtils'
import { Calendar } from '@/features/Calendars/CalendarTypes'
import { defaultColors } from '@/utils/defaultColors'
import { Box, Button, Card, CardHeader, Typography } from '@linagora/twake-mui'
import RepeatIcon from '@mui/icons-material/Repeat'
import VideocamIcon from '@mui/icons-material/Videocam'
import React from 'react'
import { useI18n } from 'twake-i18n'
import { SearchEventResult } from './types/SearchEventResult'
interface DateProps {
startDate: Date
endDate: Date
t: (key: string) => string
timeZone: string
}
interface TimeProps {
startDate: Date
endDate: Date
allDay: boolean
t: (key: string) => string
timeZone: string
}
interface TitleProps {
summary?: string
isRecurrent: boolean
t: (key: string) => string
}
interface OrganizerProps {
organizer?: {
cn?: string
email?: string
}
}
interface VideoJoinProps {
url?: string
t: (key: string) => string
}
interface MobileDateProps {
startDate: Date
t: (key: string) => string
timeZone: string
}
interface MobileEventCardProps {
eventData: SearchEventResult
calendar: Calendar | undefined
timeZone: string
}
export const RenderDate: React.FC<DateProps> = ({
startDate,
endDate,
t,
timeZone
}) => {
const dayKey = (d: Date): string =>
d.toLocaleDateString('en-CA', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
timeZone
})
return (
<Typography variant="h4" sx={{ fontWeight: 400, minWidth: '90px' }}>
{startDate.toLocaleDateString(t('locale'), {
day: '2-digit',
month: 'short',
timeZone
})}
{dayKey(startDate) !== dayKey(endDate) && (
<>
{' - '}
{endDate.toLocaleDateString(t('locale'), {
day: '2-digit',
month: 'short',
timeZone
})}
</>
)}
</Typography>
)
}
export const RenderTime: React.FC<TimeProps> = ({
startDate,
endDate,
allDay,
t,
timeZone
}) => {
if (allDay) return null
return (
<Typography variant="body1" sx={{ minWidth: '120px' }}>
{startDate.toLocaleTimeString(t('locale'), {
hour: '2-digit',
minute: '2-digit',
timeZone
})}
{' - '}
{endDate.toLocaleTimeString(t('locale'), {
hour: '2-digit',
minute: '2-digit',
timeZone
})}
</Typography>
)
}
export const RenderTitle: React.FC<TitleProps> = ({
summary,
isRecurrent,
t
}) => (
<Box display="flex" flexDirection="row" gap={1} sx={{ minWidth: 0 }}>
<Typography variant="h4" sx={{ fontWeight: 400 }}>
{summary || t('event.untitled')}
</Typography>
{isRecurrent && <RepeatIcon />}
</Box>
)
export const RenderOrganizer: React.FC<OrganizerProps> = ({ organizer }) => {
if (!organizer?.cn && !organizer?.email) return null
return (
<Typography
variant="body1"
sx={{
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
minWidth: '150px',
maxWidth: '150px'
}}
>
{organizer.cn || organizer.email}
</Typography>
)
}
export const RenderText: React.FC<{ text?: string }> = ({ text }) => {
if (!text) return null
return (
<Typography
variant="body2"
sx={{
color: 'text.secondary',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
minWidth: '150px',
maxWidth: '250px'
}}
>
{text.replace(/\n/g, ' ')}
</Typography>
)
}
export const RenderVideoJoin: React.FC<VideoJoinProps> = ({ url, t }) => {
if (!url) return null
return (
<Button
startIcon={<VideocamIcon />}
sx={{ flexShrink: 0, ml: 'auto' }}
onClick={e => {
e.stopPropagation()
try {
const parsed = new URL(url)
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
return
window.open(parsed.toString(), '_blank', 'noopener,noreferrer')
} catch {
return
}
}}
>
{t('eventPreview.joinVideoShort')}
</Button>
)
}
export const RenderMobileDate: React.FC<MobileDateProps> = ({
startDate,
t,
timeZone
}) => (
<Box sx={{ width: '100%' }}>
<Typography variant="h4" sx={{ fontWeight: 400 }}>
{startDate.toLocaleDateString(t('locale'), { day: '2-digit', timeZone })}
</Typography>
<Typography variant="caption" color="text.secondary">
{startDate
.toLocaleDateString(t('locale'), { weekday: 'short', timeZone })
.toUpperCase()}
</Typography>
</Box>
)
export const RenderMobileEventCard: React.FC<MobileEventCardProps> = ({
eventData,
calendar,
timeZone
}) => {
const { t } = useI18n()
if (!calendar) return null
const startDate = new Date(eventData.data.start)
const bestColor = calendar.color
? getBestColor(calendar.color as { light: string; dark: string })
: defaultColors[0].dark
const titleStyle = getTitleStyle(bestColor, 'ACCEPTED', calendar, false)
return (
<Card
variant="outlined"
sx={{
height: 'stretch',
width: '100%',
borderRadius: '8px',
p: 1,
boxShadow: 'none',
backgroundColor: calendar?.color?.light,
color: calendar?.color?.dark,
border: '1px solid',
borderColor: 'background.paper',
display: 'flex'
}}
data-testid={`event-card-${eventData.data.uid}`}
>
<CardHeader
sx={{ p: '0px', '& .MuiCardHeader-content': { overflow: 'hidden' } }}
title={
<Typography variant="body2" noWrap style={titleStyle}>
{eventData.data.summary || t('event.untitled')}
</Typography>
}
subheader={
!eventData.data.allDay && (
<Typography
style={{
color: titleStyle.color,
opacity: '70%',
fontFamily: 'Inter',
fontWeight: '500',
fontSize: '10px',
lineHeight: '16px',
letterSpacing: '0%',
verticalAlign: 'middle'
}}
>
{startDate.toLocaleTimeString(t('locale'), {
hour: '2-digit',
minute: '2-digit',
timeZone: timeZone
})}
</Typography>
)
}
/>
</Card>
)
}
+46
View File
@@ -0,0 +1,46 @@
import { useAppDispatch, useAppSelector } from '@/app/hooks'
import { Calendar } from '@/features/Calendars/CalendarTypes'
import { browserDefaultTimeZone } from '@/utils/timezone'
import { useState } from 'react'
import { getEventAsync } from '../Calendars/services'
import { CalendarEvent } from '../Events/EventsTypes'
import { SearchEventResult } from './types/SearchEventResult'
export function useEventPreview(
eventData: SearchEventResult,
calendar: Calendar | undefined
): {
openPreview: boolean
setOpenPreview: (b: boolean) => void
handleOpen: () => Promise<void>
timeZone: string
} {
const dispatch = useAppDispatch()
const timeZone =
useAppSelector(state => state.settings.timeZone) ?? browserDefaultTimeZone
const [openPreview, setOpenPreview] = useState(false)
const handleOpen = async (): Promise<void> => {
if (!calendar) return
const event: CalendarEvent = {
URL: eventData._links.self.href,
calId: calendar.id,
uid: eventData.data.uid,
start: eventData.data.start,
end: eventData.data.end,
allday: eventData.data.allDay,
attendee: eventData.data.attendees,
class: eventData.data.class,
description: eventData.data.description,
stamp: eventData.data.dtstamp,
location: eventData.data.location,
organizer: eventData.data.organizer,
title: eventData.data.summary,
timezone: timeZone
}
await dispatch(getEventAsync(event))
setOpenPreview(true)
}
return { openPreview, setOpenPreview, handleOpen, timeZone }
}