Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
@@ -416,212 +416,214 @@ const CalendarApp: React.FC<CalendarAppProps> = ({
|
||||
<div className="calendar">
|
||||
<ImportAlert />
|
||||
{menubarProps?.isIframe && <Menubar {...menubarProps} />}
|
||||
{(isTablet || isMobile) && (
|
||||
<Fab
|
||||
color="primary"
|
||||
aria-label={t('event.createEvent')}
|
||||
onClick={() => eventHandlers.handleDateSelect(null)}
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
bottom: 24,
|
||||
right: 24,
|
||||
zIndex: 20,
|
||||
borderRadius: '16px'
|
||||
}}
|
||||
>
|
||||
<AddIcon />
|
||||
</Fab>
|
||||
)}
|
||||
|
||||
{view === 'calendar' && (
|
||||
<div
|
||||
ref={calendarWrapperRef}
|
||||
className={isTablet || isMobile ? 'calendar-swipe-container' : ''}
|
||||
style={{
|
||||
height: '100%',
|
||||
...(isTablet || isMobile ? { touchAction: 'pan-y' } : {})
|
||||
}}
|
||||
{...(isTablet || isMobile ? handlers : {})}
|
||||
>
|
||||
<FullCalendar
|
||||
key={hiddenDays.join(',')}
|
||||
ref={ref => {
|
||||
if (ref) {
|
||||
calendarRef.current = ref.getApi()
|
||||
<>
|
||||
{' '}
|
||||
{(isTablet || isMobile) && (
|
||||
<Fab
|
||||
color="primary"
|
||||
aria-label={t('event.createEvent')}
|
||||
onClick={() => eventHandlers.handleDateSelect(null)}
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
bottom: 24,
|
||||
right: 24,
|
||||
zIndex: 20,
|
||||
borderRadius: '16px'
|
||||
}}
|
||||
>
|
||||
<AddIcon />
|
||||
</Fab>
|
||||
)}
|
||||
<div
|
||||
ref={calendarWrapperRef}
|
||||
className={isTablet || isMobile ? 'calendar-swipe-container' : ''}
|
||||
style={{
|
||||
height: '100%',
|
||||
...(isTablet || isMobile ? { touchAction: 'pan-y' } : {})
|
||||
}}
|
||||
{...(isTablet || isMobile ? handlers : {})}
|
||||
>
|
||||
<FullCalendar
|
||||
key={hiddenDays.join(',')}
|
||||
ref={ref => {
|
||||
calendarRef.current = ref ? ref.getApi() : null
|
||||
}}
|
||||
plugins={[
|
||||
dayGridPlugin,
|
||||
timeGridPlugin,
|
||||
interactionPlugin,
|
||||
momentTimezonePlugin
|
||||
]}
|
||||
initialView={
|
||||
currentView ||
|
||||
(isTablet
|
||||
? CALENDAR_VIEWS.timeGridDay
|
||||
: CALENDAR_VIEWS.timeGridWeek)
|
||||
}
|
||||
}}
|
||||
plugins={[
|
||||
dayGridPlugin,
|
||||
timeGridPlugin,
|
||||
interactionPlugin,
|
||||
momentTimezonePlugin
|
||||
]}
|
||||
initialView={
|
||||
currentView ||
|
||||
(isTablet
|
||||
? CALENDAR_VIEWS.timeGridDay
|
||||
: CALENDAR_VIEWS.timeGridWeek)
|
||||
}
|
||||
initialDate={selectedDate}
|
||||
firstDay={1}
|
||||
editable={true}
|
||||
locale={localeMap[lang]}
|
||||
hiddenDays={hiddenDays}
|
||||
selectable={true}
|
||||
timeZone={timezone}
|
||||
height="100%"
|
||||
select={eventHandlers.handleDateSelect}
|
||||
nowIndicator
|
||||
slotLabelClassNames={arg => [
|
||||
updateSlotLabelVisibility(new Date(), arg, timezone)
|
||||
]}
|
||||
nowIndicatorContent={viewHandlers.handleNowIndicatorContent}
|
||||
headerToolbar={false}
|
||||
views={{
|
||||
timeGridWeek: {
|
||||
titleFormat: { month: 'long', year: 'numeric' }
|
||||
initialDate={selectedDate}
|
||||
firstDay={1}
|
||||
editable={true}
|
||||
locale={localeMap[lang]}
|
||||
hiddenDays={hiddenDays}
|
||||
selectable={true}
|
||||
timeZone={timezone}
|
||||
height="100%"
|
||||
select={eventHandlers.handleDateSelect}
|
||||
nowIndicator
|
||||
slotLabelClassNames={arg => [
|
||||
updateSlotLabelVisibility(new Date(), arg, timezone)
|
||||
]}
|
||||
nowIndicatorContent={viewHandlers.handleNowIndicatorContent}
|
||||
headerToolbar={false}
|
||||
views={{
|
||||
timeGridWeek: {
|
||||
titleFormat: { month: 'long', year: 'numeric' }
|
||||
}
|
||||
}}
|
||||
dayMaxEvents={true}
|
||||
moreLinkClick={handleMoreLinkClick}
|
||||
events={eventToFullCalendarFormat(
|
||||
filteredEvents,
|
||||
filteredTempEvents,
|
||||
userId,
|
||||
userData?.email,
|
||||
isPending,
|
||||
calendars
|
||||
)}
|
||||
eventOrder={(a: EventApi, b: EventApi) =>
|
||||
a.extendedProps.priority - b.extendedProps.priority
|
||||
}
|
||||
}}
|
||||
dayMaxEvents={true}
|
||||
moreLinkClick={handleMoreLinkClick}
|
||||
events={eventToFullCalendarFormat(
|
||||
filteredEvents,
|
||||
filteredTempEvents,
|
||||
userId,
|
||||
userData?.email,
|
||||
isPending,
|
||||
calendars
|
||||
)}
|
||||
eventOrder={(a: EventApi, b: EventApi) =>
|
||||
a.extendedProps.priority - b.extendedProps.priority
|
||||
}
|
||||
weekNumbers={
|
||||
currentView === CALENDAR_VIEWS.timeGridWeek ||
|
||||
currentView === CALENDAR_VIEWS.timeGridDay
|
||||
}
|
||||
weekNumberFormat={{ week: 'long' }}
|
||||
weekNumberContent={arg => {
|
||||
return (
|
||||
<div className="weekSelector">
|
||||
{displayWeekNumbers && (
|
||||
<>
|
||||
<div>
|
||||
{t('menubar.views.week')} {arg.num}
|
||||
</div>
|
||||
<TimezoneSelector
|
||||
value={timezone}
|
||||
referenceDate={
|
||||
calendarRef.current?.getDate() ?? new Date()
|
||||
}
|
||||
onChange={(newTimezone: string) =>
|
||||
dispatch(setTimeZone(newTimezone))
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
dayCellContent={arg => {
|
||||
const month = arg.date.toLocaleDateString(t('locale'), {
|
||||
month: 'short',
|
||||
timeZone: timezone
|
||||
})
|
||||
if (arg.view.type === CALENDAR_VIEWS.dayGridMonth) {
|
||||
weekNumbers={
|
||||
currentView === CALENDAR_VIEWS.timeGridWeek ||
|
||||
currentView === CALENDAR_VIEWS.timeGridDay
|
||||
}
|
||||
weekNumberFormat={{ week: 'long' }}
|
||||
weekNumberContent={arg => {
|
||||
return (
|
||||
<span
|
||||
className={`fc-daygrid-day-number ${
|
||||
arg.isToday ? 'current-date' : ''
|
||||
}`}
|
||||
>
|
||||
{arg.dayNumberText === '1' ? month : ''}{' '}
|
||||
{arg.dayNumberText}
|
||||
</span>
|
||||
<div className="weekSelector">
|
||||
{displayWeekNumbers && (
|
||||
<>
|
||||
<div>
|
||||
{t('menubar.views.week')} {arg.num}
|
||||
</div>
|
||||
<TimezoneSelector
|
||||
value={timezone}
|
||||
referenceDate={
|
||||
calendarRef.current?.getDate() ?? new Date()
|
||||
}
|
||||
onChange={(newTimezone: string) =>
|
||||
dispatch(setTimeZone(newTimezone))
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}}
|
||||
slotDuration="00:30:00"
|
||||
slotLabelInterval="01:00:00"
|
||||
scrollTime="12:00:00"
|
||||
unselectAuto={false}
|
||||
allDayText=""
|
||||
slotLabelFormat={{
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
}}
|
||||
datesSet={arg => {
|
||||
onViewChange?.(arg.view.type)
|
||||
const calendarCurrentDate =
|
||||
calendarRef.current?.getDate() || new Date(arg.start)
|
||||
setDisplayedDateAndRange(calendarCurrentDate)
|
||||
|
||||
if (arg.view.type === CALENDAR_VIEWS.dayGridMonth) {
|
||||
const start = new Date(arg.start).getTime()
|
||||
const end = new Date(arg.end).getTime()
|
||||
const middle = start + (end - start) / 2
|
||||
setSelectedDate(new Date(middle))
|
||||
setSelectedMiniDate(calendarCurrentDate)
|
||||
} else {
|
||||
setSelectedDate(calendarCurrentDate)
|
||||
setSelectedMiniDate(calendarCurrentDate)
|
||||
}
|
||||
|
||||
// Always use the calendar's current date for consistency
|
||||
if (onDateChange) {
|
||||
onDateChange(calendarCurrentDate)
|
||||
}
|
||||
|
||||
// Notify parent about view change
|
||||
setCurrentView(arg.view.type)
|
||||
|
||||
// Update slot label visibility when view changes
|
||||
setTimeout(() => {
|
||||
updateSlotLabelVisibility(new Date())
|
||||
}, 100)
|
||||
}}
|
||||
dayHeaderContent={arg => {
|
||||
const m = moment.tz(arg.date, timezone)
|
||||
|
||||
const date = m.date()
|
||||
const weekDay = m
|
||||
.toDate()
|
||||
.toLocaleDateString(t('locale'), {
|
||||
weekday: 'short',
|
||||
}}
|
||||
dayCellContent={arg => {
|
||||
const month = arg.date.toLocaleDateString(t('locale'), {
|
||||
month: 'short',
|
||||
timeZone: timezone
|
||||
})
|
||||
.toUpperCase()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fc-daygrid-day-top ${isTablet || isMobile ? 'fc-daygrid-day-top--mobile' : ''}`}
|
||||
>
|
||||
<small>{weekDay}</small>
|
||||
{arg.view.type !== CALENDAR_VIEWS.dayGridMonth && (
|
||||
if (arg.view.type === CALENDAR_VIEWS.dayGridMonth) {
|
||||
return (
|
||||
<span
|
||||
className={`fc-daygrid-day-number ${arg.isToday ? 'current-date' : ''}`}
|
||||
className={`fc-daygrid-day-number ${
|
||||
arg.isToday ? 'current-date' : ''
|
||||
}`}
|
||||
>
|
||||
{date}
|
||||
{arg.dayNumberText === '1' ? month : ''}{' '}
|
||||
{arg.dayNumberText}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
dayHeaderDidMount={viewHandlers.handleDayHeaderDidMount}
|
||||
dayHeaderWillUnmount={viewHandlers.handleDayHeaderWillUnmount}
|
||||
viewDidMount={viewHandlers.handleViewDidMount}
|
||||
viewWillUnmount={viewHandlers.handleViewWillUnmount}
|
||||
eventClick={eventHandlers.handleEventClick}
|
||||
eventAllow={eventHandlers.handleEventAllow}
|
||||
eventDrop={arg => {
|
||||
void eventHandlers.handleEventDrop(arg)
|
||||
}}
|
||||
eventResize={arg => {
|
||||
void eventHandlers.handleEventResize(arg)
|
||||
}}
|
||||
eventContent={viewHandlers.handleEventContent}
|
||||
eventDidMount={viewHandlers.handleEventDidMount}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}}
|
||||
slotDuration="00:30:00"
|
||||
slotLabelInterval="01:00:00"
|
||||
scrollTime="12:00:00"
|
||||
unselectAuto={false}
|
||||
allDayText=""
|
||||
slotLabelFormat={{
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
}}
|
||||
datesSet={arg => {
|
||||
onViewChange?.(arg.view.type)
|
||||
const calendarCurrentDate =
|
||||
calendarRef.current?.getDate() || new Date(arg.start)
|
||||
setDisplayedDateAndRange(calendarCurrentDate)
|
||||
|
||||
if (arg.view.type === CALENDAR_VIEWS.dayGridMonth) {
|
||||
const start = new Date(arg.start).getTime()
|
||||
const end = new Date(arg.end).getTime()
|
||||
const middle = start + (end - start) / 2
|
||||
setSelectedDate(new Date(middle))
|
||||
setSelectedMiniDate(calendarCurrentDate)
|
||||
} else {
|
||||
setSelectedDate(calendarCurrentDate)
|
||||
setSelectedMiniDate(calendarCurrentDate)
|
||||
}
|
||||
|
||||
// Always use the calendar's current date for consistency
|
||||
if (onDateChange) {
|
||||
onDateChange(calendarCurrentDate)
|
||||
}
|
||||
|
||||
// Notify parent about view change
|
||||
setCurrentView(arg.view.type)
|
||||
|
||||
// Update slot label visibility when view changes
|
||||
setTimeout(() => {
|
||||
updateSlotLabelVisibility(new Date())
|
||||
}, 100)
|
||||
}}
|
||||
dayHeaderContent={arg => {
|
||||
const m = moment.tz(arg.date, timezone)
|
||||
|
||||
const date = m.date()
|
||||
const weekDay = m
|
||||
.toDate()
|
||||
.toLocaleDateString(t('locale'), {
|
||||
weekday: 'short',
|
||||
timeZone: timezone
|
||||
})
|
||||
.toUpperCase()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fc-daygrid-day-top ${isTablet || isMobile ? 'fc-daygrid-day-top--mobile' : ''}`}
|
||||
>
|
||||
<small>{weekDay}</small>
|
||||
{arg.view.type !== CALENDAR_VIEWS.dayGridMonth && (
|
||||
<span
|
||||
className={`fc-daygrid-day-number ${arg.isToday ? 'current-date' : ''}`}
|
||||
>
|
||||
{date}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
dayHeaderDidMount={viewHandlers.handleDayHeaderDidMount}
|
||||
dayHeaderWillUnmount={viewHandlers.handleDayHeaderWillUnmount}
|
||||
viewDidMount={viewHandlers.handleViewDidMount}
|
||||
viewWillUnmount={viewHandlers.handleViewWillUnmount}
|
||||
eventClick={eventHandlers.handleEventClick}
|
||||
eventAllow={eventHandlers.handleEventAllow}
|
||||
eventDrop={arg => {
|
||||
void eventHandlers.handleEventDrop(arg)
|
||||
}}
|
||||
eventResize={arg => {
|
||||
void eventHandlers.handleEventResize(arg)
|
||||
}}
|
||||
eventContent={viewHandlers.handleEventContent}
|
||||
eventDidMount={viewHandlers.handleEventDidMount}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{view === 'search' && <SearchResultsPage />}
|
||||
<EventPopover
|
||||
|
||||
@@ -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 />}
|
||||
</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>
|
||||
)}
|
||||
|
||||
<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>
|
||||
{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,37 +24,114 @@ 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 = () => {
|
||||
const FiltersButtons: React.FC = () => (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
overflowX: 'auto',
|
||||
gap: 2,
|
||||
px: 2,
|
||||
py: 1,
|
||||
scrollbarWidth: 'none',
|
||||
'&::-webkit-scrollbar': { display: 'none' },
|
||||
backgroundColor: '#FFF',
|
||||
minHeight: '48px'
|
||||
}}
|
||||
>
|
||||
<SearchInFilter mode="mobile" />
|
||||
<OrganizersFilter mode="mobile" />
|
||||
<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: 'flex',
|
||||
overflowX: 'auto',
|
||||
gap: 2,
|
||||
px: 2,
|
||||
py: 1,
|
||||
scrollbarWidth: 'none',
|
||||
'&::-webkit-scrollbar': { display: 'none' },
|
||||
backgroundColor: '#FFF',
|
||||
minHeight: '48px'
|
||||
}}
|
||||
>
|
||||
<SearchInFilter mode="mobile" />
|
||||
<OrganizersFilter mode="mobile" />
|
||||
<AttendeesFilter mode="mobile" />
|
||||
</Box>
|
||||
<>
|
||||
<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)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
Reference in New Issue
Block a user