[#691] changed searchbar in menubar to mobile searchbar (#822)

* [#691] changed searchbar in menubar to mobile searchbar
* [#691] add filters to results page for mobile
* [#691] added filter for organizers and attendees
This commit is contained in:
Camille Moussu
2026-04-28 15:07:45 +02:00
committed by GitHub
parent 00e00594a2
commit 234b4c3ae8
18 changed files with 1593 additions and 595 deletions
+9 -316
View File
@@ -1,323 +1,16 @@
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 { setView } from '../Settings/SettingsSlice'
import { useScreenSizeDetection } from '@/useScreenSizeDetection'
import DesktopSearchResultsPage from './DesktopSearchResultsPage'
import MobileSearchResultsPage from './MobileSearchResultsPage'
import './searchResult.styl'
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'
}
}
const SearchResultsPage: React.FC = () => {
const { isTooSmall: isMobile } = useScreenSizeDetection()
export default function SearchResultsPage() {
const { t } = useI18n()
const dispatch = useAppDispatch()
const { error, loading, hits, results } = useAppSelector(
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>
)
if (isMobile) {
return <MobileSearchResultsPage />
} 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 <DesktopSearchResultsPage />
}
return (
<Box className="search-layout">
<Box className="search-result-content-header">
<Box className="back-button">
<IconButton
onClick={() => dispatch(setView('calendar'))}
aria-label={t('settings.back')}
>
<ArrowBackIcon />
</IconButton>
<Typography variant="h5">{t('search.resultsTitle')}</Typography>
</Box>
</Box>
{layout}
</Box>
)
}
function ResultItem({
eventData,
dispatch
}: {
eventData: SearchEventResult
dispatch: AppDispatch
}) {
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[
`${eventData.data.userId}/${eventData.data.calendarId}`
]
)
const calendarColor = calendar?.color?.light
const [openPreview, setOpenPreview] = useState(false)
const handleOpenResult = async (eventData: SearchEventResult) => {
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 (
<>
<Box
sx={{
display: 'flex',
flexDirection: 'row',
gap: 2,
p: 3,
borderTop: '1px solid #F3F6F9',
cursor: 'pointer',
'&:hover': { backgroundColor: '#e7e7e7ff' },
alignItems: 'center',
textAlign: 'left',
maxWidth: '80vw'
}}
onClick={() => handleOpenResult(eventData)}
>
<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>
)}
<SquareRoundedIcon
style={{
color: calendarColor ?? defaultColors[0].light,
width: 24,
height: 24,
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>
)}
</Box>
{calendar && calendar.events[eventData.data.uid] && (
<EventPreviewModal
eventId={eventData.data.uid}
calId={calendar.id}
open={openPreview}
onClose={() => setOpenPreview(false)}
/>
)}
</>
)
}
export default SearchResultsPage