- added search in topbar - added skeleton search results page - added tests - set personal calendars as default search in and allow to select personal and shared as a search in option --------- Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
@@ -45,6 +45,7 @@ import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
|
||||
import frLocale from "@fullcalendar/core/locales/fr";
|
||||
import ruLocale from "@fullcalendar/core/locales/ru";
|
||||
import viLocale from "@fullcalendar/core/locales/vi";
|
||||
import SearchResultsPage from "../../features/Search/SearchResultsPage";
|
||||
|
||||
const localeMap: Record<string, any> = {
|
||||
fr: frLocale,
|
||||
@@ -76,7 +77,7 @@ export default function CalendarApp({
|
||||
dispatch(push("/"));
|
||||
}
|
||||
}, [dispatch, tokens, userId]);
|
||||
|
||||
const view = useAppSelector((state) => state.settings.view);
|
||||
const calendars = useAppSelector((state) => state.calendars.list);
|
||||
const tempcalendars = useAppSelector((state) => state.calendars.templist);
|
||||
const [selectedCalendars, setSelectedCalendars] = useState<string[]>([]);
|
||||
@@ -116,6 +117,14 @@ export default function CalendarApp({
|
||||
const [eventErrors, setEventErrors] = useState<string[]>([]);
|
||||
const errorHandler = useRef(new EventErrorHandler());
|
||||
|
||||
// useEffect(() => {
|
||||
// if (view === "search") {
|
||||
// document.body.classList.add("dialog-expanded");
|
||||
// } else {
|
||||
// document.body.classList.remove("dialog-expanded");
|
||||
// }
|
||||
// }, [view]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = errorHandler.current;
|
||||
handler.setErrorCallback(setEventErrors);
|
||||
@@ -628,149 +637,152 @@ export default function CalendarApp({
|
||||
</Box>
|
||||
<div className="calendar">
|
||||
<ImportAlert />
|
||||
<FullCalendar
|
||||
ref={(ref) => {
|
||||
if (ref) {
|
||||
calendarRef.current = ref.getApi();
|
||||
{view === "calendar" && (
|
||||
<FullCalendar
|
||||
ref={(ref) => {
|
||||
if (ref) {
|
||||
calendarRef.current = ref.getApi();
|
||||
}
|
||||
}}
|
||||
plugins={[
|
||||
dayGridPlugin,
|
||||
timeGridPlugin,
|
||||
interactionPlugin,
|
||||
momentTimezonePlugin,
|
||||
]}
|
||||
initialView="timeGridWeek"
|
||||
firstDay={1}
|
||||
editable={true}
|
||||
locale={localeMap[lang]}
|
||||
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}
|
||||
events={eventToFullCalendarFormat(
|
||||
filteredEvents,
|
||||
filteredTempEvents,
|
||||
userId
|
||||
)}
|
||||
weekNumbers={
|
||||
currentView === "timeGridWeek" || currentView === "timeGridDay"
|
||||
}
|
||||
}}
|
||||
plugins={[
|
||||
dayGridPlugin,
|
||||
timeGridPlugin,
|
||||
interactionPlugin,
|
||||
momentTimezonePlugin,
|
||||
]}
|
||||
initialView="timeGridWeek"
|
||||
firstDay={1}
|
||||
editable={true}
|
||||
locale={localeMap[lang]}
|
||||
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}
|
||||
events={eventToFullCalendarFormat(
|
||||
filteredEvents,
|
||||
filteredTempEvents,
|
||||
userId
|
||||
)}
|
||||
weekNumbers={
|
||||
currentView === "timeGridWeek" || currentView === "timeGridDay"
|
||||
}
|
||||
weekNumberFormat={{ week: "long" }}
|
||||
weekNumberContent={(arg) => {
|
||||
return (
|
||||
<div className="weekSelector">
|
||||
<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",
|
||||
});
|
||||
if (arg.view.type === "dayGridMonth") {
|
||||
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">
|
||||
<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) => {
|
||||
setCurrentView(arg.view.type);
|
||||
const calendarCurrentDate =
|
||||
calendarRef.current?.getDate() || new Date(arg.start);
|
||||
|
||||
if (arg.view.type === "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
|
||||
if (onViewChange) {
|
||||
onViewChange(arg.view.type);
|
||||
}
|
||||
|
||||
// Update slot label visibility when view changes
|
||||
setTimeout(() => {
|
||||
updateSlotLabelVisibility(new Date());
|
||||
}, 100);
|
||||
}}
|
||||
dayHeaderContent={(arg) => {
|
||||
const date = arg.date.getDate();
|
||||
const weekDay = arg.date
|
||||
.toLocaleDateString(t("locale"), { weekday: "short" })
|
||||
.toUpperCase();
|
||||
return (
|
||||
<div className="fc-daygrid-day-top">
|
||||
<small>{weekDay}</small>
|
||||
{arg.view.type !== "dayGridMonth" && (
|
||||
}}
|
||||
dayCellContent={(arg) => {
|
||||
const month = arg.date.toLocaleDateString(t("locale"), {
|
||||
month: "short",
|
||||
});
|
||||
if (arg.view.type === "dayGridMonth") {
|
||||
return (
|
||||
<span
|
||||
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={eventHandlers.handleEventDrop}
|
||||
eventResize={eventHandlers.handleEventResize}
|
||||
eventContent={viewHandlers.handleEventContent}
|
||||
eventDidMount={viewHandlers.handleEventDidMount}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}}
|
||||
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) => {
|
||||
setCurrentView(arg.view.type);
|
||||
const calendarCurrentDate =
|
||||
calendarRef.current?.getDate() || new Date(arg.start);
|
||||
|
||||
if (arg.view.type === "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
|
||||
if (onViewChange) {
|
||||
onViewChange(arg.view.type);
|
||||
}
|
||||
|
||||
// Update slot label visibility when view changes
|
||||
setTimeout(() => {
|
||||
updateSlotLabelVisibility(new Date());
|
||||
}, 100);
|
||||
}}
|
||||
dayHeaderContent={(arg) => {
|
||||
const date = arg.date.getDate();
|
||||
const weekDay = arg.date
|
||||
.toLocaleDateString(t("locale"), { weekday: "short" })
|
||||
.toUpperCase();
|
||||
return (
|
||||
<div className="fc-daygrid-day-top">
|
||||
<small>{weekDay}</small>
|
||||
{arg.view.type !== "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={eventHandlers.handleEventDrop}
|
||||
eventResize={eventHandlers.handleEventResize}
|
||||
eventContent={viewHandlers.handleEventContent}
|
||||
eventDidMount={viewHandlers.handleEventDidMount}
|
||||
/>
|
||||
)}
|
||||
{view === "search" && <SearchResultsPage />}
|
||||
<EventPopover
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
|
||||
@@ -77,7 +77,7 @@ export default function CalendarLayout() {
|
||||
return (
|
||||
<div className="App">
|
||||
<Menubar {...menubarProps} />
|
||||
{view === "calendar" && (
|
||||
{(view === "calendar" || view === "search") && (
|
||||
<CalendarApp
|
||||
calendarRef={calendarRef}
|
||||
onDateChange={handleDateChange}
|
||||
@@ -85,7 +85,6 @@ export default function CalendarLayout() {
|
||||
/>
|
||||
)}
|
||||
{view === "settings" && <SettingsPage />}
|
||||
{view === "search" && <SearchResultsPage />}
|
||||
<ErrorSnackbar error={error} type="calendar" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||
import { getCalendarDetailAsync } from "../../features/Calendars/CalendarSlice";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
|
||||
import { setView } from "../../features/Settings/SettingsSlice";
|
||||
|
||||
export function MiniCalendar({
|
||||
calendarRef,
|
||||
@@ -38,10 +39,11 @@ export function MiniCalendar({
|
||||
>
|
||||
<DateCalendar
|
||||
value={moment(visibleDate)}
|
||||
onChange={(dateMoment, selectionState) => {
|
||||
onChange={async (dateMoment, selectionState) => {
|
||||
if (!dateMoment) return;
|
||||
const date = dateMoment.toDate();
|
||||
if (selectionState === "finish") {
|
||||
await dispatch(setView("calendar"));
|
||||
setSelectedMiniDate(date);
|
||||
calendarRef.current?.gotoDate(date);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
removeTempCal,
|
||||
} from "../../features/Calendars/CalendarSlice";
|
||||
import { Calendars } from "../../features/Calendars/CalendarTypes";
|
||||
import { setView } from "../../features/Settings/SettingsSlice";
|
||||
import { User, PeopleSearch } from "../Attendees/PeopleSearch";
|
||||
import { getAccessiblePair } from "./utils/calendarColorsUtils";
|
||||
|
||||
@@ -69,7 +70,7 @@ export function TempCalendarsInput({
|
||||
light: lightColor,
|
||||
dark: getAccessiblePair(lightColor, theme),
|
||||
};
|
||||
|
||||
dispatch(setView("calendar"));
|
||||
dispatch(
|
||||
getTempCalendarsListAsync(user, { signal: controller.signal })
|
||||
);
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardActions,
|
||||
CardContent,
|
||||
Divider,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
InputLabel,
|
||||
ListSubheader,
|
||||
MenuItem,
|
||||
Popover,
|
||||
Select,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { useRef, useState } from "react";
|
||||
import HighlightOffIcon from "@mui/icons-material/HighlightOff";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
import TuneIcon from "@mui/icons-material/Tune";
|
||||
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
|
||||
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||
import { searchEventsAsync } from "../../features/Search/SearchSlice";
|
||||
import { setView } from "../../features/Settings/SettingsSlice";
|
||||
import { userAttendee } from "../../features/User/userDataTypes";
|
||||
import UserSearch from "../Attendees/AttendeeSearch";
|
||||
import { CalendarItemList } from "../Calendar/CalendarItemList";
|
||||
|
||||
export default function SearchBar() {
|
||||
const { t } = useI18n();
|
||||
const dispatch = useAppDispatch();
|
||||
const calendars = Object.values(
|
||||
useAppSelector((state) => state.calendars.list)
|
||||
);
|
||||
const userId = useAppSelector((state) => state.user.userData.openpaasId);
|
||||
const personnalCalendars = calendars.filter(
|
||||
(c) => c.id.split("/")[0] === userId
|
||||
);
|
||||
const sharedCalendars = calendars.filter(
|
||||
(c) => c.id.split("/")[0] !== userId
|
||||
);
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
const [extended, setExtended] = useState(false);
|
||||
|
||||
const [filters, setFilters] = useState({
|
||||
searchIn: "my-calendars",
|
||||
keywords: "",
|
||||
organizers: [] as userAttendee[],
|
||||
attendees: [] as userAttendee[],
|
||||
});
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||
const filterOpen = Boolean(anchorEl);
|
||||
|
||||
const searchWidth = {
|
||||
xs: "10vw",
|
||||
sm: "20vw",
|
||||
md: "35vw",
|
||||
xl: "35vw",
|
||||
"@media (min-width: 2000px)": {
|
||||
width: "55vw",
|
||||
},
|
||||
};
|
||||
const searchBoxRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
const handleFilterChange = (
|
||||
field: string,
|
||||
value: string | userAttendee[]
|
||||
) => {
|
||||
setFilters((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleClearFilters = () => {
|
||||
setFilters({
|
||||
searchIn: "my-calendars",
|
||||
keywords: "",
|
||||
organizers: [] as userAttendee[],
|
||||
attendees: [] as userAttendee[],
|
||||
});
|
||||
setAnchorEl(null);
|
||||
setExtended(false);
|
||||
};
|
||||
|
||||
const handleSearch = async () => {
|
||||
let searchInCalendars: string[];
|
||||
|
||||
if (filters.searchIn === "" || !filters.searchIn) {
|
||||
searchInCalendars = calendars.map((c) => c.id);
|
||||
} else if (filters.searchIn === "my-calendars") {
|
||||
searchInCalendars = personnalCalendars.map((c) => c.id);
|
||||
} else if (filters.searchIn === "shared-calendars") {
|
||||
searchInCalendars = sharedCalendars.map((c) => c.id);
|
||||
} else {
|
||||
searchInCalendars = [filters.searchIn];
|
||||
}
|
||||
|
||||
const cleanedFilters = {
|
||||
...filters,
|
||||
organizers: filters.organizers.map((u) => u.cal_address),
|
||||
attendees: filters.attendees.map((u) => u.cal_address),
|
||||
searchIn: searchInCalendars,
|
||||
};
|
||||
|
||||
dispatch(
|
||||
searchEventsAsync({
|
||||
search,
|
||||
filters: cleanedFilters,
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(setView("search"));
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
ref={searchBoxRef}
|
||||
sx={{
|
||||
margin: "0 auto",
|
||||
height: "44px",
|
||||
position: "relative",
|
||||
width: extended ? searchWidth : "auto",
|
||||
|
||||
transition: "width 0.25s ease-out",
|
||||
}}
|
||||
>
|
||||
{!extended && (
|
||||
<IconButton onClick={() => setExtended(true)}>
|
||||
<SearchIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
{extended && (
|
||||
<TextField
|
||||
fullWidth
|
||||
placeholder={t("common.search")}
|
||||
value={search}
|
||||
onBlur={() => {
|
||||
if (!search.trim()) {
|
||||
setExtended(false);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value);
|
||||
handleFilterChange("keywords", e.target.value);
|
||||
}}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
borderRadius: "999px",
|
||||
"& .MuiOutlinedInput-root": {
|
||||
borderRadius: "999px",
|
||||
},
|
||||
"& .MuiInputBase-input": { padding: "12px 10px" },
|
||||
animation: "scaleIn 0.25s ease-out",
|
||||
"@keyframes scaleIn": {
|
||||
from: { transform: "scaleX(0)", opacity: 0 },
|
||||
to: { transform: "scaleX(1)", opacity: 1 },
|
||||
},
|
||||
transformOrigin: "right",
|
||||
}}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon sx={{ color: "#605D62" }} />
|
||||
</InputAdornment>
|
||||
),
|
||||
endAdornment: (
|
||||
<>
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
onClick={(e) => setAnchorEl(searchBoxRef.current)}
|
||||
>
|
||||
<TuneIcon />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
{search && (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={() => setSearch("")}>
|
||||
<HighlightOffIcon />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Popover
|
||||
open={filterOpen}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClearFilters}
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
transformOrigin={{ vertical: "top", horizontal: "right" }}
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: { mt: 1.2, width: extended ? searchWidth : "auto" },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Card sx={{ p: 2, pb: 1 }}>
|
||||
<CardContent>
|
||||
<Stack spacing={2}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "140px 1fr",
|
||||
gap: 2,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<InputLabel id="search-in" sx={{ m: 0 }}>
|
||||
{t("search.searchIn")}
|
||||
</InputLabel>
|
||||
<Select
|
||||
displayEmpty
|
||||
value={filters.searchIn}
|
||||
onChange={(e) =>
|
||||
handleFilterChange("searchIn", e.target.value)
|
||||
}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
style: {
|
||||
maxHeight: 300,
|
||||
color: "#8C9CAF",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuItem value="">
|
||||
<Typography
|
||||
sx={{
|
||||
color: "#243B55",
|
||||
font: "Roboto",
|
||||
fontSize: "16px",
|
||||
weight: 400,
|
||||
pointerEvents: "auto",
|
||||
}}
|
||||
>
|
||||
{t("search.filter.allCalendar")}
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem
|
||||
value="my-calendars"
|
||||
sx={{
|
||||
color: "#243B55",
|
||||
font: "Roboto",
|
||||
fontSize: "12px",
|
||||
weight: 400,
|
||||
pointerEvents: "auto",
|
||||
}}
|
||||
>
|
||||
{t("search.filter.myCalendar")}
|
||||
</MenuItem>
|
||||
{CalendarItemList(personnalCalendars)}
|
||||
<Divider />
|
||||
<MenuItem
|
||||
value="shared-calendars"
|
||||
sx={{
|
||||
color: "#243B55",
|
||||
font: "Roboto",
|
||||
fontSize: "12px",
|
||||
weight: 400,
|
||||
pointerEvents: "auto",
|
||||
}}
|
||||
>
|
||||
{t("search.filter.sharedCalendars")}
|
||||
</MenuItem>
|
||||
{CalendarItemList(sharedCalendars)}
|
||||
</Select>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "140px 1fr",
|
||||
gap: 2,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<InputLabel id="keywords" sx={{ m: 0 }}>
|
||||
{t("search.keywords")}
|
||||
</InputLabel>
|
||||
<TextField
|
||||
fullWidth
|
||||
placeholder={t("search.keywordsPlaceholder")}
|
||||
value={filters.keywords}
|
||||
onChange={(e) =>
|
||||
handleFilterChange("keywords", e.target.value)
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "140px 1fr",
|
||||
gap: 2,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<InputLabel id="from" sx={{ m: 0 }}>
|
||||
{t("search.organizers")}
|
||||
</InputLabel>
|
||||
<UserSearch
|
||||
attendees={filters.organizers}
|
||||
setAttendees={(users: userAttendee[]) =>
|
||||
handleFilterChange("organizers", users)
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "140px 1fr",
|
||||
gap: 2,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<InputLabel id="participant" sx={{ m: 0 }}>
|
||||
{t("search.participants")}
|
||||
</InputLabel>
|
||||
<UserSearch
|
||||
attendees={filters.attendees}
|
||||
setAttendees={(users: userAttendee[]) =>
|
||||
handleFilterChange("participants", users)
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
|
||||
<CardActions sx={{ justifyContent: "flex-end", p: 2, gap: 2 }}>
|
||||
<Button variant="text" onClick={handleClearFilters}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button variant="contained" onClick={handleSearch}>
|
||||
{t("common.search")}
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -46,6 +46,9 @@
|
||||
align-items center
|
||||
width calc(330px - 0.5rem)
|
||||
|
||||
.tc-home
|
||||
cursor pointer
|
||||
|
||||
.logo
|
||||
padding 0.5rem 1rem
|
||||
font-size 1.5rem
|
||||
@@ -95,5 +98,9 @@
|
||||
body.fullscreen-view .navigation-controls,
|
||||
body.fullscreen-view .current-date-time,
|
||||
body.fullscreen-view .refresh-button,
|
||||
body.fullscreen-view .select-display
|
||||
body.fullscreen-view .select-display,
|
||||
body.fullscreen-view .search-container
|
||||
display none
|
||||
|
||||
.search-container
|
||||
width 100%
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
ru as ruLocale,
|
||||
vi as viLocale,
|
||||
} from "date-fns/locale";
|
||||
import SearchBar from "./EventSearchBar";
|
||||
const dateLocales = { en: enGB, fr: frLocale, ru: ruLocale, vi: viLocale };
|
||||
|
||||
export type AppIconProps = {
|
||||
@@ -86,9 +87,9 @@ export function Menubar({
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleNavigation = (action: "prev" | "next" | "today") => {
|
||||
const handleNavigation = async (action: "prev" | "next" | "today") => {
|
||||
if (!calendarRef.current) return;
|
||||
|
||||
await dispatch(setView("calendar"));
|
||||
switch (action) {
|
||||
case "prev":
|
||||
calendarRef.current.prev();
|
||||
@@ -108,8 +109,10 @@ export function Menubar({
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewChange = (view: string) => {
|
||||
const handleViewChange = async (view: string) => {
|
||||
if (!calendarRef.current) return;
|
||||
await dispatch(setView("calendar"));
|
||||
|
||||
calendarRef.current.changeView(view);
|
||||
|
||||
// Notify parent about view change
|
||||
@@ -190,6 +193,9 @@ export function Menubar({
|
||||
</div>
|
||||
</div>
|
||||
<div className="right-menu">
|
||||
<div className="search-container">
|
||||
<SearchBar />
|
||||
</div>
|
||||
<div className="menu-items">
|
||||
<IconButton
|
||||
className="refresh-button"
|
||||
@@ -350,9 +356,15 @@ export function Menubar({
|
||||
|
||||
export function MainTitle() {
|
||||
const { t } = useI18n();
|
||||
const dispatch = useAppDispatch();
|
||||
return (
|
||||
<div className="menubar-item tc-home">
|
||||
<img className="logo" src={logo} alt={t("menubar.logoAlt")} />
|
||||
<img
|
||||
className="logo"
|
||||
src={logo}
|
||||
alt={t("menubar.logoAlt")}
|
||||
onClick={() => dispatch(setView("calendar"))}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user