Merge pull request #36 from linagora/30-temporal-navigation

temporal navigation
This commit is contained in:
Camille Moussu
2025-07-16 16:16:27 +02:00
committed by GitHub
12 changed files with 171 additions and 78 deletions
-9
View File
@@ -19,12 +19,3 @@
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
+11 -1
View File
@@ -8,7 +8,7 @@ main {
height: 100%;
}
.sidebar{
.sidebar {
border-right: 2px gray;
}
@@ -182,3 +182,13 @@ main {
.calendar-list li.personal::before {
background-color: #d93025;
}
.event-dot {
margin-top: 2px;
width: 6px;
height: 6px;
background-color: #d93025; /* adjust to your theme */
border-radius: 50%;
margin-left: auto;
margin-right: auto;
}
+78 -30
View File
@@ -14,6 +14,11 @@ import { CalendarEvent } from "../../features/Events/EventsTypes";
import CalendarSelection from "./CalendarSelection";
import { getCalendarDetailAsync } from "../../features/Calendars/CalendarSlice";
import ImportAlert from "../../features/Events/ImportAlert";
import {
formatDateToYYYYMMDDTHHMMSS,
getCalendarRange,
} from "../../utils/dateUtils";
import { Calendars } from "../../features/Calendars/CalendarTypes";
export default function CalendarApp() {
const calendarRef = useRef<CalendarApi | null>(null);
@@ -24,51 +29,63 @@ export default function CalendarApp() {
const pending = useAppSelector((state) => state.calendars.pending);
const userId = useAppSelector((state) => state.user.userData.openpaasId);
const userPersonnalCalendars: Calendars[] = useAppSelector((state) =>
Object.keys(state.calendars.list).map((id) => {
if (id.split("/")[0] === userId) {
return state.calendars.list[id];
}
return {} as Calendars;
})
);
let personnalEvents: CalendarEvent[] = [];
Object.keys(userPersonnalCalendars).forEach((value, id) => {
if (userPersonnalCalendars[id].events) {
personnalEvents = personnalEvents.concat(
Object.keys(userPersonnalCalendars[id].events).map(
(eventid) => userPersonnalCalendars[id].events[eventid]
)
);
}
});
const [selectedCalendars, setSelectedCalendars] = useState<string[]>(
Object.keys(calendars).filter((id) => id.split("/")[0] === userId)
);
const fetchedIdsRef = useRef<Set<string>>(new Set());
const calendarRange = getCalendarRange(selectedDate);
// Create a stable string key for the range
const rangeKey = `${formatDateToYYYYMMDDTHHMMSS(
calendarRange.start
)}_${formatDateToYYYYMMDDTHHMMSS(calendarRange.end)}`;
let filteredEvents: CalendarEvent[] = [];
selectedCalendars.forEach((id) => {
filteredEvents = filteredEvents.concat(calendars[id].events);
filteredEvents = filteredEvents
.concat(
Object.keys(calendars[id].events).map(
(eventid) => calendars[id].events[eventid]
)
)
.filter((event) => !(event.status === "CANCELLED"));
});
useEffect(() => {
selectedCalendars.forEach((id) => {
const events = calendars[id]?.events ?? [];
const isEmpty = events.length === 0;
const notFetched = !fetchedIdsRef.current.has(id);
if (isEmpty && notFetched && !pending) {
if (!pending && rangeKey) {
dispatch(
getCalendarDetailAsync({
access_token: tokens.access_token,
calId: id,
match: {
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
},
})
);
fetchedIdsRef.current.add(id);
}
});
}, [selectedCalendars, calendars, pending, tokens.access_token, dispatch]);
const [date, setDate] = useState(new Date());
const months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
}, [rangeKey, selectedCalendars]);
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
const [anchorElCal, setAnchorElCal] = useState<HTMLElement | null>(null);
@@ -92,25 +109,39 @@ export default function CalendarApp() {
<div className="calendar-label">
<div className="calendar-label">
<span>
{months[date.getMonth()]} {date.getFullYear()}
{selectedDate.toLocaleDateString("us-us", {
month: "long",
year: "numeric",
})}
</span>
</div>
<button
onClick={() =>
setDate(new Date(date.getFullYear(), date.getMonth() - 1))
setSelectedDate(
new Date(
selectedDate.getFullYear(),
selectedDate.getMonth() - 1
)
)
}
>
&lt;
</button>
<button
onClick={() =>
setDate(new Date(date.getFullYear(), date.getMonth() + 1))
setSelectedDate(
new Date(
selectedDate.getFullYear(),
selectedDate.getMonth() + 1
)
)
}
>
&gt;
</button>
</div>
<ReactCalendar
key={selectedDate.toDateString()}
showNeighboringMonth={false}
calendarType="hebrew"
formatShortWeekday={(locale, date) =>
@@ -124,6 +155,17 @@ export default function CalendarApp() {
prevLabel={null}
nextLabel={null}
showNavigation={false}
tileContent={({ date }) => {
const hasEvents = personnalEvents.some((event) => {
const eventDate = new Date(event.start);
return (
eventDate.getFullYear() === date.getFullYear() &&
eventDate.getMonth() === date.getMonth() &&
eventDate.getDate() === date.getDate()
);
});
return hasEvents ? <div className="event-dot" /> : null;
}}
/>
<CalendarSelection
selectedCalendars={selectedCalendars}
@@ -156,13 +198,19 @@ export default function CalendarApp() {
weekNumberFormat={{ week: "long" }}
slotDuration={"00:30:00"}
slotLabelInterval={"00:30:00"}
scrollTime={"07:00:00"}
scrollTime={"09:00:00"}
allDayText=""
slotLabelFormat={{
hour: "2-digit",
minute: "2-digit",
hour12: false,
}}
datesSet={(arg) => {
const today = new Date();
setSelectedDate(
today > arg.start && today < arg.end ? today : arg.start
);
}}
dayHeaderContent={(arg) => {
const date = arg.date.getDate();
const weekDay = arg.date
-4
View File
@@ -79,7 +79,3 @@
left: 1511px;
gap: 24px;
}
/* Responsive for mobile */
@media (max-width: 768px) {
}
+6 -2
View File
@@ -14,7 +14,11 @@ export async function getCalendars(userId: string, opaque_token: string) {
return calendars;
}
export async function getCalendar(id: string, opaque_token: string) {
export async function getCalendar(
id: string,
opaque_token: string,
match: { start: string; end: string }
) {
const response = await fetch(
`${(window as any).CALENDAR_BASE_URL}/dav/calendars/${id}.json`,
{
@@ -24,7 +28,7 @@ export async function getCalendar(id: string, opaque_token: string) {
Authorization: `Bearer ${opaque_token}`,
},
body: JSON.stringify({
match: { start: "20250525T000000", end: "20250708T000000" },
match,
}),
}
);
+23 -21
View File
@@ -24,7 +24,7 @@ export const getCalendarsListAsync = createAsyncThunk<
: cal._links.self.href.replace("/calendars/", "").replace(".json", "");
const color = cal["apple:color"];
importedCalendars[id] = { id, name, description, color, events: [] };
importedCalendars[id] = { id, name, description, color, events: {} };
}
return importedCalendars;
@@ -32,14 +32,16 @@ export const getCalendarsListAsync = createAsyncThunk<
export const getCalendarDetailAsync = createAsyncThunk<
{ calId: string; events: CalendarEvent[] }, // Return type
{ access_token: string; calId: string } // Arg type
>("calendars/getCalendarDetails", async ({ access_token, calId }) => {
const calendar = await getCalendar(calId, access_token);
{ access_token: string; calId: string; match: { start: string; end: string } } // Arg type
>("calendars/getCalendarDetails", async ({ access_token, calId, match }) => {
const calendar = await getCalendar(calId, access_token, match);
const color = calendar["apple:color"];
const events: CalendarEvent[] = calendar._embedded["dav:item"].map(
const events: CalendarEvent[] = calendar._embedded["dav:item"].flatMap(
(eventdata: any) => {
const datas = eventdata.data[2][0][1];
return parseCalendarEvent(datas, color, calId);
const vevents = eventdata.data[2] as any[][]; // array of ['vevent', RawEntry[], []]
return vevents.map((vevent: any[]) => {
return parseCalendarEvent(vevent[1], color, calId);
});
}
);
@@ -56,28 +58,25 @@ const CalendarSlice = createSlice({
state.list[id].name = action.payload.name;
state.list[id].color = action.payload.color;
state.list[id].description = action.payload.description;
state.list[id].events = [];
state.list[id].events = {};
},
addEvent: (
state,
action: PayloadAction<{ calendarUid: string; event: CalendarEvent }>
) => {
if (!state.list[action.payload.calendarUid].events) {
state.list[action.payload.calendarUid].events = [];
state.list[action.payload.calendarUid].events = {};
}
state.list[action.payload.calendarUid].events.push(action.payload.event);
state.list[action.payload.calendarUid].events[action.payload.event.uid] =
action.payload.event;
},
removeEvent: (
state,
action: PayloadAction<{ calendarUid: string; eventUid: string }>
) => {
state.list[action.payload.calendarUid as string].events = state.list[
action.payload.calendarUid
].events.filter((event) => {
if (event.uid !== action.payload.calendarUid) {
return event;
}
});
delete state.list[action.payload.calendarUid].events[
action.payload.eventUid
];
},
},
extraReducers: (builder) => {
@@ -101,12 +100,15 @@ const CalendarSlice = createSlice({
if (!state.list[action.payload.calId]) {
state.list[action.payload.calId] = {
id: action.payload.calId,
events: [] as CalendarEvent[],
events: {},
} as Calendars;
}
state.list[action.payload.calId].events = action.payload.events;
state.list[action.payload.calId].events.forEach((event) => {
event.color = state.list[action.payload.calId].color;
action.payload.events.forEach((event) => {
state.list[action.payload.calId].events[event.uid] = event;
});
Object.keys(state.list[action.payload.calId].events).forEach((id) => {
state.list[action.payload.calId].events[id].color =
state.list[action.payload.calId].color;
});
}
)
+1 -1
View File
@@ -8,5 +8,5 @@ export interface Calendars {
description?: string;
calscale?: string;
version?: string;
events: CalendarEvent[];
events: Record<string, CalendarEvent>;
}
+1
View File
@@ -17,4 +17,5 @@ export interface CalendarEvent {
color?: string;
allday?: Boolean;
error?: string;
status?: string;
}
+13 -7
View File
@@ -18,18 +18,24 @@ export default function ImportAlert() {
return (
<>
{Object.keys(calendars).map((calendarId) =>
calendars[calendarId].events
.filter((event) => event.error)
.map((event) => {
const isVisible = visibleAlerts[event.uid] ?? true; // default to visible
Object.keys(calendars[calendarId].events)
.filter((id) => calendars[calendarId].events[id].error)
.map((id) => {
const isVisible =
visibleAlerts[calendars[calendarId].events[id].uid] ?? true; // default to visible
return (
<Collapse in={isVisible} key={event.uid}>
<Collapse
in={isVisible}
key={calendars[calendarId].events[id].uid}
>
<Alert
severity="error"
onClose={() => toggleEventAlert(event.uid)}
onClose={() =>
toggleEventAlert(calendars[calendarId].events[id].uid)
}
>
{event.error}
{calendars[calendarId].events[id].error}
</Alert>
</Collapse>
);
+9
View File
@@ -9,6 +9,7 @@ export function parseCalendarEvent(
calendarid: string
): CalendarEvent {
const event: Partial<CalendarEvent> = { color, attendee: [] };
let recurrenceId;
for (const [key, params, type, value] of data) {
switch (key.toLowerCase()) {
@@ -61,8 +62,16 @@ export function parseCalendarEvent(
case "sequence":
event.sequence = Number(value);
break;
case "recurrence-id":
recurrenceId = value;
break;
case "status":
event.status = String(value);
}
}
if (recurrenceId && event.uid) {
event.uid = `${event.uid}/${recurrenceId}`;
}
if (!event.uid || !event.start) {
console.error(
-3
View File
@@ -4,7 +4,6 @@ import { Provider } from "react-redux";
import App from "./App";
import { store } from "./app/store";
import reportWebVitals from "./reportWebVitals";
const container = document.getElementById("root")!;
const root = createRoot(container);
@@ -15,5 +14,3 @@ root.render(
</Provider>
</React.StrictMode>
);
reportWebVitals();
+29
View File
@@ -0,0 +1,29 @@
export function formatDateToYYYYMMDDTHHMMSS(date: Date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}${month}${day}T000000`;
}
export function getCalendarRange(date = new Date()) {
const year = date.getFullYear();
const month = date.getMonth();
const firstOfMonth = new Date(year, month, 1);
const dayOfWeekStart = firstOfMonth.getDay();
const diffToMonday = (dayOfWeekStart + 6) % 7;
const startDate = new Date(firstOfMonth);
startDate.setDate(firstOfMonth.getDate() - diffToMonday);
const lastOfMonth = new Date(year, month + 1, 0);
const dayOfWeekEnd = lastOfMonth.getDay();
const diffToSunday = (7 - dayOfWeekEnd) % 7;
const endDate = new Date(lastOfMonth);
endDate.setDate(lastOfMonth.getDate() + diffToSunday);
return {
start: startDate,
end: endDate,
};
}