[1] added state to add event to calendars

This commit is contained in:
Camille Moussu
2025-06-30 17:15:24 +02:00
parent 624c1b43ca
commit 4f5d596b59
16 changed files with 305 additions and 3197 deletions
-3143
View File
File diff suppressed because it is too large Load Diff
-3
View File
@@ -14,9 +14,6 @@
"@mui/icons-material": "^7.1.2", "@mui/icons-material": "^7.1.2",
"@mui/material": "^7.1.2", "@mui/material": "^7.1.2",
"@reduxjs/toolkit": "^2.8.2", "@reduxjs/toolkit": "^2.8.2",
"@schedule-x/events-service": "^2.34.0",
"@schedule-x/react": "^2.34.0",
"@schedule-x/theme-default": "^2.34.0",
"@types/node": "^16.18.126", "@types/node": "^16.18.126",
"@types/react": "^19.1.8", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.1.6",
+5
View File
@@ -0,0 +1,5 @@
declare namespace Intl {
type Key = 'calendar' | 'collation' | 'currency' | 'numberingSystem' | 'timeZone' | 'unit';
function supportedValuesOf(input: Key): string[];
}
+2
View File
@@ -1,6 +1,7 @@
import { combineReducers, configureStore } from "@reduxjs/toolkit"; import { combineReducers, configureStore } from "@reduxjs/toolkit";
import userReducer from "../features/User/userSlice"; import userReducer from "../features/User/userSlice";
import eventsReducer from "../features/Events/EventsSlice"; import eventsReducer from "../features/Events/EventsSlice";
import eventsCalendar from "../features/Calendars/CalendarSlice";
import { createReduxHistoryContext } from "redux-first-history"; import { createReduxHistoryContext } from "redux-first-history";
import { createBrowserHistory } from "history"; import { createBrowserHistory } from "history";
@@ -11,6 +12,7 @@ const rootReducer = combineReducers({
router: routerReducer, router: routerReducer,
user: userReducer, user: userReducer,
events: eventsReducer, events: eventsReducer,
calendars: eventsCalendar,
}); });
export const setupStore = (preloadedState?: Partial<RootState>) => { export const setupStore = (preloadedState?: Partial<RootState>) => {
+42 -13
View File
@@ -8,14 +8,16 @@ import ReactCalendar from "react-calendar";
import "./Calendar.css"; import "./Calendar.css";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { useAppSelector } from "../../app/hooks"; import { useAppSelector } from "../../app/hooks";
import EventPopover from "../../features/Events/EventModal";
import CalendarPopover from "../../features/Calendars/CalendarModal";
export default function CalendarApp() { export default function CalendarApp() {
const calendarRef = useRef<CalendarApi | null>(null); const calendarRef = useRef<CalendarApi | null>(null);
const [selectedDate, setSelectedDate] = useState(new Date()); const [selectedDate, setSelectedDate] = useState(new Date());
const [selectedCalendars, setSelectedCalendars] = useState([ const calendars = useAppSelector((state) => state.calendars);
"Work", const [selectedCalendars, setSelectedCalendars] = useState(
"Personnal", Object.keys(calendars).map((id) => calendars[id].name)
]); );
const events = useAppSelector((state) => state.events); const events = useAppSelector((state) => state.events);
const handleCalendarToggle = (name: string) => { const handleCalendarToggle = (name: string) => {
@@ -23,9 +25,6 @@ export default function CalendarApp() {
prev.includes(name) ? prev.filter((n) => n !== name) : [...prev, name] prev.includes(name) ? prev.filter((n) => n !== name) : [...prev, name]
); );
}; };
const filteredEvent = events.filter((e) =>
selectedCalendars.includes(e.calendar)
);
const [date, setDate] = useState(new Date()); const [date, setDate] = useState(new Date());
@@ -44,6 +43,22 @@ export default function CalendarApp() {
"December", "December",
]; ];
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
const [anchorElCal, setAnchorElCal] = useState<HTMLElement | null>(null);
const [selectedRange, setSelectedRange] = useState<DateSelectArg | null>(
null
);
const handleDateSelect = (selectInfo: DateSelectArg) => {
setSelectedRange(selectInfo);
setAnchorEl(document.body); // fallback: we could use selectInfo.jsEvent.target if from a click
};
const handleClosePopover = () => {
setAnchorEl(null);
setSelectedRange(null);
};
return ( return (
<main> <main>
<div className="sidebar"> <div className="sidebar">
@@ -83,18 +98,20 @@ export default function CalendarApp() {
nextLabel={null} nextLabel={null}
showNavigation={false} showNavigation={false}
/> />
{["Work", "Personnal", "Holidays"].map((name) => ( {Object.keys(calendars).map((id) => (
<div key={name}> <div key={id}>
<label> <label>
<input <input
type="checkbox" type="checkbox"
checked={selectedCalendars.includes(name)} style={{backgroundColor:calendars[id].color}}
onChange={() => handleCalendarToggle(name)} checked={selectedCalendars.includes(calendars[id].name)}
onChange={() => handleCalendarToggle(calendars[id].name)}
/> />
{name} {calendars[id].name}
</label> </label>
</div> </div>
))} ))}
<button onClick={() => setAnchorElCal(document.body)}>+</button>
</div> </div>
<div className="calendar"> <div className="calendar">
<FullCalendar <FullCalendar
@@ -108,13 +125,14 @@ export default function CalendarApp() {
weekends={false} weekends={false}
editable={true} editable={true}
selectable={true} selectable={true}
select={handleDateSelect}
nowIndicator nowIndicator
selectMirror={true} selectMirror={true}
views={{ views={{
timeGridWeek: { titleFormat: { month: "long", year: "numeric" } }, timeGridWeek: { titleFormat: { month: "long", year: "numeric" } },
}} }}
dayMaxEvents={true} dayMaxEvents={true}
events={filteredEvent} events={events}
weekNumbers weekNumbers
weekNumberFormat={{ week: "long" }} weekNumberFormat={{ week: "long" }}
slotDuration={"00:30:00"} slotDuration={"00:30:00"}
@@ -159,6 +177,17 @@ export default function CalendarApp() {
right: "dayGridMonth,timeGridWeek,timeGridDay", right: "dayGridMonth,timeGridWeek,timeGridDay",
}} }}
/> />
<EventPopover
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleClosePopover}
selectedRange={selectedRange}
/>
<CalendarPopover
anchorEl={anchorElCal}
open={Boolean(anchorElCal)}
onClose={() => setAnchorElCal(null)}
/>
</div> </div>
</main> </main>
); );
+3
View File
@@ -0,0 +1,3 @@
export default async function getCalendars() {
return {}
}
+122
View File
@@ -0,0 +1,122 @@
import { useState } from "react";
import { createCalendar } from "./CalendarSlice";
import { useAppDispatch } from "../../app/hooks";
import {
Popover,
TextField,
Button,
Box,
Typography,
Select,
ButtonGroup,
} from "@mui/material";
function CalendarPopover({
anchorEl,
open,
onClose,
}: {
anchorEl: HTMLElement | null;
open: boolean;
onClose: (Calendar: {}, reason: "backdropClick" | "escapeKeyDown") => void;
}) {
const dispatch = useAppDispatch();
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [color, setColor] = useState("");
const [timeZone, setTimeZone] = useState("");
const timezones = Intl.supportedValuesOf?.("timeZone") || [];
const handleSave = () => {
dispatch(createCalendar({ name, description, color }));
onClose({}, "backdropClick");
// Reset
setName("");
setDescription("");
};
const palette = [
"#D50000",
"#E67C73",
"#F4511E",
"#F6BF26",
"#33B679",
"#0B8043",
"#039BE5",
"#3F51B5",
"#7986CB",
"#8E24AA",
"#616161",
];
return (
<Popover
open={open}
anchorEl={anchorEl}
onClose={onClose}
anchorOrigin={{
vertical: "center",
horizontal: "center",
}}
transformOrigin={{
vertical: "center",
horizontal: "center",
}}
>
<Box p={2}>
<Typography variant="h6" gutterBottom style={{backgroundColor:color}}>
Create a Calendar
</Typography>
<TextField
fullWidth
label="Name"
value={name}
onChange={(e) => setName(e.target.value)}
size="small"
margin="dense"
/>
<TextField
fullWidth
label="Description"
value={description}
onChange={(e) => setDescription(e.target.value)}
size="small"
margin="dense"
multiline
rows={2}
/>
<ButtonGroup>
{palette.map((color) => (
<Button
style={{ backgroundColor: color }}
onClick={() => setColor(color)}
/>
))}
</ButtonGroup>
<Select
fullWidth
label="timeZone"
value={timeZone}
onChange={(e) => setTimeZone(e.target.value)}
>
{timezones.map((zone: string) => (
<div>{zone}</div>
))}
</Select>
<Box mt={2} display="flex" justifyContent="flex-end" gap={1}>
<Button
variant="outlined"
onClick={() => onClose({}, "backdropClick")}
>
Cancel
</Button>
<Button variant="contained" onClick={handleSave}>
Save
</Button>
</Box>
</Box>
</Popover>
);
}
export default CalendarPopover;
+39
View File
@@ -0,0 +1,39 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { Calendars } from "./CalendarTypes";
import { CalendarEvent } from "../Events/EventsTypes";
const CalendarSlice = createSlice({
name: "calendars",
initialState: {} as Record<string, Calendars>,
reducers: {
createCalendar: (state, action: PayloadAction<Record<string, string>>) => {
const id = Date.now().toString(36);
state[id] = {} as Calendars;
state[id].name = action.payload.name;
state[id].color = action.payload.color;
state[id].description = action.payload.description;
state[id].events = [];
},
addEvent: (
state,
action: PayloadAction<{ calendarUid: string; event: CalendarEvent }>
) => {
state[action.payload.calendarUid].events.push(action.payload.event);
},
removeEvent: (
state,
action: PayloadAction<{ calendarUid: string; eventUid: string }>
) => {
state[action.payload.calendarUid as string].events = state[
action.payload.calendarUid
].events.filter((event) => {
if (event.uid !== action.payload.calendarUid) {
return event;
}
});
},
},
});
export const { addEvent, removeEvent, createCalendar } = CalendarSlice.actions;
export default CalendarSlice.reducer;
+12
View File
@@ -0,0 +1,12 @@
import { CalendarEvent } from "../Events/EventsTypes";
export interface Calendars {
id: string;
name: string;
prodid: string;
color: string;
description: string;
calscale?: string;
version?: string;
events: CalendarEvent[];
}
+42 -18
View File
@@ -1,10 +1,17 @@
// components/EventModal.tsx
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { addEvent } from "./EventsSlice"; import { addEvent } from "./EventsSlice";
import { CalendarEvent } from "./EventsTypes"; import { CalendarEvent } from "./EventsTypes";
import { DateSelectArg } from "@fullcalendar/core"; import { DateSelectArg } from "@fullcalendar/core";
import { useAppDispatch } from "../../app/hooks"; import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { Popover, TextField, Button, Box, Typography } from "@mui/material"; import {
Popover,
TextField,
Button,
Box,
Typography,
Select,
MenuItem,
} from "@mui/material";
function EventPopover({ function EventPopover({
anchorEl, anchorEl,
@@ -19,12 +26,14 @@ function EventPopover({
}) { }) {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [title, setTitle] = useState(""); const organizer = useAppSelector((state) => state.user.organiserData);
const calendars = useAppSelector((state) => state.calendars);
const [summary, setSummary] = useState("");
const [description, setDescription] = useState(""); const [description, setDescription] = useState("");
const [location, setLocation] = useState(""); const [location, setLocation] = useState("");
const [calendar, setCalendar] = useState("");
const [start, setStart] = useState(""); const [start, setStart] = useState("");
const [end, setEnd] = useState(""); const [end, setEnd] = useState("");
const [calendarid, setCalendarid] = useState("");
useEffect(() => { useEffect(() => {
if (selectedRange) { if (selectedRange) {
@@ -35,21 +44,31 @@ function EventPopover({
const handleSave = () => { const handleSave = () => {
const newEvent: CalendarEvent = { const newEvent: CalendarEvent = {
title, summary,
start, start: new Date(start),
end, end: new Date(end),
calendar, uid: Date.now().toString(36),
extendedProps: { description,
description, location,
location, organizer,
}, attendee: [
{
cn: organizer.cn,
cal_address: organizer.cal_address,
partstat: "ACCEPTED",
rsvp: "FALSE",
role: "CHAIR",
cutype: "INDIVIDUAL",
},
],
transp: "OPAQUE",
}; };
dispatch(addEvent(newEvent)); dispatch(addEvent(newEvent));
console.log(newEvent) console.log(newEvent);
onClose({}, "backdropClick"); onClose({}, "backdropClick");
// Reset // Reset
setTitle(""); setSummary("");
setDescription(""); setDescription("");
setLocation(""); setLocation("");
}; };
@@ -72,11 +91,16 @@ function EventPopover({
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
Create Event Create Event
</Typography> </Typography>
<Select onClick={(e) => console.log(e.target)}>
{Object.keys(calendars).map((calendar) => (
<MenuItem value={calendar}>{calendars[calendar].name}</MenuItem>
))}
</Select>
<TextField <TextField
fullWidth fullWidth
label="Title" label="summary"
value={title} value={summary}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setSummary(e.target.value)}
size="small" size="small"
margin="dense" margin="dense"
/> />
-1
View File
@@ -1,4 +1,3 @@
// store/eventsSlice.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { CalendarEvent } from "./EventsTypes"; import { CalendarEvent } from "./EventsTypes";
+15 -9
View File
@@ -1,11 +1,17 @@
// types/Event.ts import { userAttendee, userOrganiser } from "../User/userDataTypes";
export interface CalendarEvent { export interface CalendarEvent {
title: string; uid: string;
start: string; // ISO date transp: string;
end?: string; start: Date; // ISO date
calendar:string; end?: Date;
extendedProps?: { class?: string;
description?: string; x_openpass_videoconference?: unknown;
location?: string; summary?: string;
}; description?: string;
location?: string;
organizer: userOrganiser;
attendee: userAttendee[];
stamp?: Date;
sequence?: Number;
} }
+2 -8
View File
@@ -1,7 +1,6 @@
import React, { useEffect, useRef, useState } from "react"; import { useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom";
import { Callback } from "./oidcAuth"; import { Callback } from "./oidcAuth";
import { useAppDispatch, useAppSelector } from "../../app/hooks"; import { useAppDispatch } from "../../app/hooks";
import { push } from "redux-first-history"; import { push } from "redux-first-history";
import { setUserData } from "./userSlice"; import { setUserData } from "./userSlice";
import { Loading } from "../../components/Loading/Loading"; import { Loading } from "../../components/Loading/Loading";
@@ -12,9 +11,6 @@ export function CallbackResume() {
const saved = sessionStorage.getItem("redirectState") const saved = sessionStorage.getItem("redirectState")
? JSON.parse(sessionStorage.getItem("redirectState")!) ? JSON.parse(sessionStorage.getItem("redirectState")!)
: null; : null;
const [tokens, setTokens] = useState<any>();
const [userInfo, setUserInfo] = useState<any>();
const location = useAppSelector((state) => state.router.location);
useEffect(() => { useEffect(() => {
if (hasRun.current) { if (hasRun.current) {
return; return;
@@ -24,8 +20,6 @@ export function CallbackResume() {
try { try {
const data = await Callback(saved?.code_verifier, saved?.state); const data = await Callback(saved?.code_verifier, saved?.state);
console.log("data:", data); console.log("data:", data);
setTokens(data?.tokenSet);
setUserInfo(data?.userinfo);
dispatch(setUserData(data?.userinfo)); dispatch(setUserData(data?.userinfo));
sessionStorage.removeItem("redirectState"); sessionStorage.removeItem("redirectState");
// Redirect to main page after successful callback // Redirect to main page after successful callback
-1
View File
@@ -22,7 +22,6 @@ export async function getClientConfig() {
} }
export async function Auth() { export async function Auth() {
console.log("bap");
let code_verifier = client.randomPKCECodeVerifier(); let code_verifier = client.randomPKCECodeVerifier();
let code_challenge = await client.calculatePKCECodeChallenge(code_verifier); let code_challenge = await client.calculatePKCECodeChallenge(code_verifier);
const openIdClientConfig = await getClientConfig(); const openIdClientConfig = await getClientConfig();
+14
View File
@@ -3,3 +3,17 @@ export interface userData {
sid: string; sid: string;
sub: string; sub: string;
} }
export interface userOrganiser {
cn: string;
cal_address: string;
}
export interface userAttendee {
cn: string;
cal_address: string;
partstat: string;
rsvp: string;
role: string;
cutype: string;
}
+7 -1
View File
@@ -1,14 +1,20 @@
import { createSlice } from "@reduxjs/toolkit"; import { createSlice } from "@reduxjs/toolkit";
import { userData } from "./userDataTypes"; import { userData, userOrganiser } from "./userDataTypes";
export const userSlice = createSlice({ export const userSlice = createSlice({
name: "user", name: "user",
initialState: { initialState: {
userData: null as unknown as userData, userData: null as unknown as userData,
organiserData: null as unknown as userOrganiser,
}, },
reducers: { reducers: {
setUserData: (state, action) => { setUserData: (state, action) => {
state.userData = action.payload; state.userData = action.payload;
if (!state.organiserData) {
state.organiserData = {} as userOrganiser;
}
state.organiserData.cn = action.payload.sub;
state.organiserData.cal_address = `mailto:${action.payload.email}`;
}, },
}, },
}); });