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">
|
<div className="calendar">
|
||||||
<ImportAlert />
|
<ImportAlert />
|
||||||
{menubarProps?.isIframe && <Menubar {...menubarProps} />}
|
{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' && (
|
{view === 'calendar' && (
|
||||||
<div
|
<>
|
||||||
ref={calendarWrapperRef}
|
{' '}
|
||||||
className={isTablet || isMobile ? 'calendar-swipe-container' : ''}
|
{(isTablet || isMobile) && (
|
||||||
style={{
|
<Fab
|
||||||
height: '100%',
|
color="primary"
|
||||||
...(isTablet || isMobile ? { touchAction: 'pan-y' } : {})
|
aria-label={t('event.createEvent')}
|
||||||
}}
|
onClick={() => eventHandlers.handleDateSelect(null)}
|
||||||
{...(isTablet || isMobile ? handlers : {})}
|
sx={{
|
||||||
>
|
position: 'fixed',
|
||||||
<FullCalendar
|
bottom: 24,
|
||||||
key={hiddenDays.join(',')}
|
right: 24,
|
||||||
ref={ref => {
|
zIndex: 20,
|
||||||
if (ref) {
|
borderRadius: '16px'
|
||||||
calendarRef.current = ref.getApi()
|
}}
|
||||||
|
>
|
||||||
|
<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)
|
||||||
}
|
}
|
||||||
}}
|
initialDate={selectedDate}
|
||||||
plugins={[
|
firstDay={1}
|
||||||
dayGridPlugin,
|
editable={true}
|
||||||
timeGridPlugin,
|
locale={localeMap[lang]}
|
||||||
interactionPlugin,
|
hiddenDays={hiddenDays}
|
||||||
momentTimezonePlugin
|
selectable={true}
|
||||||
]}
|
timeZone={timezone}
|
||||||
initialView={
|
height="100%"
|
||||||
currentView ||
|
select={eventHandlers.handleDateSelect}
|
||||||
(isTablet
|
nowIndicator
|
||||||
? CALENDAR_VIEWS.timeGridDay
|
slotLabelClassNames={arg => [
|
||||||
: CALENDAR_VIEWS.timeGridWeek)
|
updateSlotLabelVisibility(new Date(), arg, timezone)
|
||||||
}
|
]}
|
||||||
initialDate={selectedDate}
|
nowIndicatorContent={viewHandlers.handleNowIndicatorContent}
|
||||||
firstDay={1}
|
headerToolbar={false}
|
||||||
editable={true}
|
views={{
|
||||||
locale={localeMap[lang]}
|
timeGridWeek: {
|
||||||
hiddenDays={hiddenDays}
|
titleFormat: { month: 'long', year: 'numeric' }
|
||||||
selectable={true}
|
}
|
||||||
timeZone={timezone}
|
}}
|
||||||
height="100%"
|
dayMaxEvents={true}
|
||||||
select={eventHandlers.handleDateSelect}
|
moreLinkClick={handleMoreLinkClick}
|
||||||
nowIndicator
|
events={eventToFullCalendarFormat(
|
||||||
slotLabelClassNames={arg => [
|
filteredEvents,
|
||||||
updateSlotLabelVisibility(new Date(), arg, timezone)
|
filteredTempEvents,
|
||||||
]}
|
userId,
|
||||||
nowIndicatorContent={viewHandlers.handleNowIndicatorContent}
|
userData?.email,
|
||||||
headerToolbar={false}
|
isPending,
|
||||||
views={{
|
calendars
|
||||||
timeGridWeek: {
|
)}
|
||||||
titleFormat: { month: 'long', year: 'numeric' }
|
eventOrder={(a: EventApi, b: EventApi) =>
|
||||||
|
a.extendedProps.priority - b.extendedProps.priority
|
||||||
}
|
}
|
||||||
}}
|
weekNumbers={
|
||||||
dayMaxEvents={true}
|
currentView === CALENDAR_VIEWS.timeGridWeek ||
|
||||||
moreLinkClick={handleMoreLinkClick}
|
currentView === CALENDAR_VIEWS.timeGridDay
|
||||||
events={eventToFullCalendarFormat(
|
}
|
||||||
filteredEvents,
|
weekNumberFormat={{ week: 'long' }}
|
||||||
filteredTempEvents,
|
weekNumberContent={arg => {
|
||||||
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) {
|
|
||||||
return (
|
return (
|
||||||
<span
|
<div className="weekSelector">
|
||||||
className={`fc-daygrid-day-number ${
|
{displayWeekNumbers && (
|
||||||
arg.isToday ? 'current-date' : ''
|
<>
|
||||||
}`}
|
<div>
|
||||||
>
|
{t('menubar.views.week')} {arg.num}
|
||||||
{arg.dayNumberText === '1' ? month : ''}{' '}
|
</div>
|
||||||
{arg.dayNumberText}
|
<TimezoneSelector
|
||||||
</span>
|
value={timezone}
|
||||||
|
referenceDate={
|
||||||
|
calendarRef.current?.getDate() ?? new Date()
|
||||||
|
}
|
||||||
|
onChange={(newTimezone: string) =>
|
||||||
|
dispatch(setTimeZone(newTimezone))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}}
|
||||||
}}
|
dayCellContent={arg => {
|
||||||
slotDuration="00:30:00"
|
const month = arg.date.toLocaleDateString(t('locale'), {
|
||||||
slotLabelInterval="01:00:00"
|
month: 'short',
|
||||||
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
|
timeZone: timezone
|
||||||
})
|
})
|
||||||
.toUpperCase()
|
if (arg.view.type === CALENDAR_VIEWS.dayGridMonth) {
|
||||||
|
return (
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`fc-daygrid-day-top ${isTablet || isMobile ? 'fc-daygrid-day-top--mobile' : ''}`}
|
|
||||||
>
|
|
||||||
<small>{weekDay}</small>
|
|
||||||
{arg.view.type !== CALENDAR_VIEWS.dayGridMonth && (
|
|
||||||
<span
|
<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>
|
</span>
|
||||||
)}
|
)
|
||||||
</div>
|
}
|
||||||
)
|
}}
|
||||||
}}
|
slotDuration="00:30:00"
|
||||||
dayHeaderDidMount={viewHandlers.handleDayHeaderDidMount}
|
slotLabelInterval="01:00:00"
|
||||||
dayHeaderWillUnmount={viewHandlers.handleDayHeaderWillUnmount}
|
scrollTime="12:00:00"
|
||||||
viewDidMount={viewHandlers.handleViewDidMount}
|
unselectAuto={false}
|
||||||
viewWillUnmount={viewHandlers.handleViewWillUnmount}
|
allDayText=""
|
||||||
eventClick={eventHandlers.handleEventClick}
|
slotLabelFormat={{
|
||||||
eventAllow={eventHandlers.handleEventAllow}
|
hour: '2-digit',
|
||||||
eventDrop={arg => {
|
minute: '2-digit',
|
||||||
void eventHandlers.handleEventDrop(arg)
|
hour12: false
|
||||||
}}
|
}}
|
||||||
eventResize={arg => {
|
datesSet={arg => {
|
||||||
void eventHandlers.handleEventResize(arg)
|
onViewChange?.(arg.view.type)
|
||||||
}}
|
const calendarCurrentDate =
|
||||||
eventContent={viewHandlers.handleEventContent}
|
calendarRef.current?.getDate() || new Date(arg.start)
|
||||||
eventDidMount={viewHandlers.handleEventDidMount}
|
setDisplayedDateAndRange(calendarCurrentDate)
|
||||||
/>
|
|
||||||
</div>
|
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 />}
|
{view === 'search' && <SearchResultsPage />}
|
||||||
<EventPopover
|
<EventPopover
|
||||||
|
|||||||
@@ -1,79 +1,23 @@
|
|||||||
import { useAppDispatch, useAppSelector } from '@/app/hooks'
|
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 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 { setView } from '../Settings/SettingsSlice'
|
||||||
|
import { ResultsList } from './ResultsList'
|
||||||
import './searchResult.styl'
|
import './searchResult.styl'
|
||||||
|
import {
|
||||||
|
RenderDate,
|
||||||
|
RenderText,
|
||||||
|
RenderOrganizer,
|
||||||
|
RenderTime,
|
||||||
|
RenderTitle,
|
||||||
|
RenderVideoJoin
|
||||||
|
} from './searchResultsComponents'
|
||||||
import { SearchEventResult } from './types/SearchEventResult'
|
import { SearchEventResult } from './types/SearchEventResult'
|
||||||
|
import { useEventPreview } from './useEventPreview'
|
||||||
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'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DesktopSearchResultsPage(): JSX.Element {
|
export default function DesktopSearchResultsPage(): JSX.Element {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -82,48 +26,6 @@ export default function DesktopSearchResultsPage(): JSX.Element {
|
|||||||
state => state.searchResult
|
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 (
|
return (
|
||||||
<Box className="search-layout">
|
<Box className="search-layout">
|
||||||
<Box className="search-result-content-header">
|
<Box className="search-result-content-header">
|
||||||
@@ -137,23 +39,39 @@ export default function DesktopSearchResultsPage(): JSX.Element {
|
|||||||
<Typography variant="h5">{t('search.resultsTitle')}</Typography>
|
<Typography variant="h5">{t('search.resultsTitle')}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</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>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ResultItem({
|
function DesktopResultItem({
|
||||||
eventData,
|
eventData
|
||||||
dispatch
|
|
||||||
}: {
|
}: {
|
||||||
eventData: SearchEventResult
|
eventData: SearchEventResult
|
||||||
dispatch: AppDispatch
|
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const startDate = new Date(eventData.data.start)
|
const startDate = new Date(eventData.data.start)
|
||||||
const endDate = eventData.data.end ? new Date(eventData.data.end) : startDate
|
const endDate = eventData.data.end ? new Date(eventData.data.end) : startDate
|
||||||
const timeZone =
|
|
||||||
useAppSelector(state => state.settings.timeZone) ?? browserDefaultTimeZone
|
|
||||||
const calendar = useAppSelector(
|
const calendar = useAppSelector(
|
||||||
state =>
|
state =>
|
||||||
state.calendars.list[
|
state.calendars.list[
|
||||||
@@ -162,32 +80,10 @@ function ResultItem({
|
|||||||
)
|
)
|
||||||
const calendarColor = calendar?.color?.light
|
const calendarColor = calendar?.color?.light
|
||||||
|
|
||||||
const [openPreview, setOpenPreview] = useState(false)
|
const { openPreview, setOpenPreview, handleOpen, timeZone } = useEventPreview(
|
||||||
|
eventData,
|
||||||
const handleOpenResult = async (
|
calendar
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -197,48 +93,29 @@ function ResultItem({
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
gap: 2,
|
gap: 2,
|
||||||
p: 3,
|
p: 3,
|
||||||
borderTop: '1px solid #F3F6F9',
|
borderTop: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
'&:hover': { backgroundColor: '#e7e7e7ff' },
|
'&:hover': { backgroundColor: 'action.hover' },
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
maxWidth: '80vw'
|
maxWidth: '80vw'
|
||||||
}}
|
}}
|
||||||
onClick={() => void handleOpenResult(eventData)}
|
onClick={() => void handleOpen()}
|
||||||
>
|
>
|
||||||
<Typography sx={{ ...styles.M3BodyLarge, minWidth: '90px' }}>
|
<RenderDate
|
||||||
{startDate.toLocaleDateString(t('locale'), {
|
startDate={startDate}
|
||||||
day: '2-digit',
|
endDate={endDate}
|
||||||
month: 'short',
|
t={t}
|
||||||
timeZone
|
timeZone={timeZone}
|
||||||
})}
|
/>
|
||||||
{startDate.toDateString() !== endDate.toDateString() && (
|
<RenderTime
|
||||||
<>
|
startDate={startDate}
|
||||||
{' - '}
|
endDate={endDate}
|
||||||
{endDate.toLocaleDateString(t('locale'), {
|
allDay={!!eventData.data.allDay}
|
||||||
day: '2-digit',
|
t={t}
|
||||||
month: 'short',
|
timeZone={timeZone}
|
||||||
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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<SquareRoundedIcon
|
<SquareRoundedIcon
|
||||||
style={{
|
style={{
|
||||||
color: calendarColor ?? defaultColors[0].light,
|
color: calendarColor ?? defaultColors[0].light,
|
||||||
@@ -247,72 +124,22 @@ function ResultItem({
|
|||||||
flexShrink: 0
|
flexShrink: 0
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Box display="flex" flexDirection="row" gap={1} sx={{ minWidth: 0 }}>
|
|
||||||
<Typography sx={styles.M3BodyLarge}>
|
<RenderTitle
|
||||||
{eventData.data.summary || t('event.untitled')}
|
summary={eventData.data.summary}
|
||||||
</Typography>
|
isRecurrent={!!eventData.data.isRecurrentMaster}
|
||||||
{eventData.data.isRecurrentMaster && <RepeatIcon />}
|
t={t}
|
||||||
</Box>
|
/>
|
||||||
{(eventData.data.organizer?.cn || eventData.data.organizer?.email) && (
|
<RenderOrganizer organizer={eventData.data.organizer} />
|
||||||
<Typography
|
<RenderText text={eventData.data.location} />
|
||||||
sx={{
|
<RenderText text={eventData.data.description} />
|
||||||
...styles.M3BodyMedium1,
|
<RenderVideoJoin
|
||||||
whiteSpace: 'nowrap',
|
t={t}
|
||||||
overflow: 'hidden',
|
url={eventData.data['x-openpaas-videoconference']}
|
||||||
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>
|
</Box>
|
||||||
{calendar && calendar.events[eventData.data.uid] && (
|
|
||||||
|
{calendar?.events?.[eventData.data.uid] && (
|
||||||
<EventPreviewModal
|
<EventPreviewModal
|
||||||
eventId={eventData.data.uid}
|
eventId={eventData.data.uid}
|
||||||
calId={calendar.id}
|
calId={calendar.id}
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
import { useAppSelector } from '@/app/hooks'
|
import { useAppSelector } from '@/app/hooks'
|
||||||
|
import EventPreviewModal from '@/features/Events/EventPreview'
|
||||||
import { Box } from '@linagora/twake-mui'
|
import { Box } from '@linagora/twake-mui'
|
||||||
|
import { useI18n } from 'twake-i18n'
|
||||||
import { AttendeesFilter } from './AttendeesFilter'
|
import { AttendeesFilter } from './AttendeesFilter'
|
||||||
import DesktopSearchResultsPage from './DesktopSearchResultsPage'
|
import { normalizeCalendars } from './calendarColorUtils'
|
||||||
import { OrganizersFilter } from './OrganizersFilter'
|
import { OrganizersFilter } from './OrganizersFilter'
|
||||||
|
import { ResultsList } from './ResultsList'
|
||||||
import { SearchInFilter } from './SearchInFilter'
|
import { SearchInFilter } from './SearchInFilter'
|
||||||
import './searchResult.styl'
|
import './searchResult.styl'
|
||||||
|
import {
|
||||||
|
RenderMobileDate,
|
||||||
|
RenderMobileEventCard
|
||||||
|
} from './searchResultsComponents'
|
||||||
|
import { SearchEventResult } from './types/SearchEventResult'
|
||||||
|
import { useEventPreview } from './useEventPreview'
|
||||||
|
|
||||||
const MobileSearchResultsPage: React.FC = () => {
|
const MobileSearchResultsPage: React.FC = () => {
|
||||||
const searchResults = useAppSelector(state => state.searchResult)
|
const searchResults = useAppSelector(state => state.searchResult)
|
||||||
@@ -15,37 +24,114 @@ const MobileSearchResultsPage: React.FC = () => {
|
|||||||
searchResults.searchParams.filters.attendees.length > 0
|
searchResults.searchParams.filters.attendees.length > 0
|
||||||
|
|
||||||
const displaySearch =
|
const displaySearch =
|
||||||
(!!searchResults.hits || !!searchResults.error || searchResults.loading) &&
|
!!searchResults.hits ||
|
||||||
|
!!searchResults.error ||
|
||||||
|
searchResults.loading ||
|
||||||
hasSearchParams
|
hasSearchParams
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '100%',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}
|
||||||
|
>
|
||||||
<FiltersButtons />
|
<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
|
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 (
|
return (
|
||||||
<Box
|
<>
|
||||||
sx={{
|
<Box
|
||||||
display: 'flex',
|
sx={{
|
||||||
overflowX: 'auto',
|
display: 'grid',
|
||||||
gap: 2,
|
gridTemplateColumns: '10%',
|
||||||
px: 2,
|
gridAutoFlow: 'column',
|
||||||
py: 1,
|
gap: 2,
|
||||||
scrollbarWidth: 'none',
|
pt: 1,
|
||||||
'&::-webkit-scrollbar': { display: 'none' },
|
cursor: 'pointer',
|
||||||
backgroundColor: '#FFF',
|
alignItems: 'center',
|
||||||
minHeight: '48px'
|
textAlign: 'left'
|
||||||
}}
|
}}
|
||||||
>
|
onClick={() => void handleOpen()}
|
||||||
<SearchInFilter mode="mobile" />
|
>
|
||||||
<OrganizersFilter mode="mobile" />
|
<RenderMobileDate startDate={startDate} t={t} timeZone={timeZone} />
|
||||||
<AttendeesFilter mode="mobile" />
|
<RenderMobileEventCard
|
||||||
</Box>
|
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