[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/material": "^7.1.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/react": "^19.1.8",
"@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 userReducer from "../features/User/userSlice";
import eventsReducer from "../features/Events/EventsSlice";
import eventsCalendar from "../features/Calendars/CalendarSlice";
import { createReduxHistoryContext } from "redux-first-history";
import { createBrowserHistory } from "history";
@@ -11,6 +12,7 @@ const rootReducer = combineReducers({
router: routerReducer,
user: userReducer,
events: eventsReducer,
calendars: eventsCalendar,
});
export const setupStore = (preloadedState?: Partial<RootState>) => {
+42 -13
View File
@@ -8,14 +8,16 @@ import ReactCalendar from "react-calendar";
import "./Calendar.css";
import { useRef, useState } from "react";
import { useAppSelector } from "../../app/hooks";
import EventPopover from "../../features/Events/EventModal";
import CalendarPopover from "../../features/Calendars/CalendarModal";
export default function CalendarApp() {
const calendarRef = useRef<CalendarApi | null>(null);
const [selectedDate, setSelectedDate] = useState(new Date());
const [selectedCalendars, setSelectedCalendars] = useState([
"Work",
"Personnal",
]);
const calendars = useAppSelector((state) => state.calendars);
const [selectedCalendars, setSelectedCalendars] = useState(
Object.keys(calendars).map((id) => calendars[id].name)
);
const events = useAppSelector((state) => state.events);
const handleCalendarToggle = (name: string) => {
@@ -23,9 +25,6 @@ export default function CalendarApp() {
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());
@@ -44,6 +43,22 @@ export default function CalendarApp() {
"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 (
<main>
<div className="sidebar">
@@ -83,18 +98,20 @@ export default function CalendarApp() {
nextLabel={null}
showNavigation={false}
/>
{["Work", "Personnal", "Holidays"].map((name) => (
<div key={name}>
{Object.keys(calendars).map((id) => (
<div key={id}>
<label>
<input
type="checkbox"
checked={selectedCalendars.includes(name)}
onChange={() => handleCalendarToggle(name)}
style={{backgroundColor:calendars[id].color}}
checked={selectedCalendars.includes(calendars[id].name)}
onChange={() => handleCalendarToggle(calendars[id].name)}
/>
{name}
{calendars[id].name}
</label>
</div>
))}
<button onClick={() => setAnchorElCal(document.body)}>+</button>
</div>
<div className="calendar">
<FullCalendar
@@ -108,13 +125,14 @@ export default function CalendarApp() {
weekends={false}
editable={true}
selectable={true}
select={handleDateSelect}
nowIndicator
selectMirror={true}
views={{
timeGridWeek: { titleFormat: { month: "long", year: "numeric" } },
}}
dayMaxEvents={true}
events={filteredEvent}
events={events}
weekNumbers
weekNumberFormat={{ week: "long" }}
slotDuration={"00:30:00"}
@@ -159,6 +177,17 @@ export default function CalendarApp() {
right: "dayGridMonth,timeGridWeek,timeGridDay",
}}
/>
<EventPopover
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleClosePopover}
selectedRange={selectedRange}
/>
<CalendarPopover
anchorEl={anchorElCal}
open={Boolean(anchorElCal)}
onClose={() => setAnchorElCal(null)}
/>
</div>
</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 { addEvent } from "./EventsSlice";
import { CalendarEvent } from "./EventsTypes";
import { DateSelectArg } from "@fullcalendar/core";
import { useAppDispatch } from "../../app/hooks";
import { Popover, TextField, Button, Box, Typography } from "@mui/material";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import {
Popover,
TextField,
Button,
Box,
Typography,
Select,
MenuItem,
} from "@mui/material";
function EventPopover({
anchorEl,
@@ -19,12 +26,14 @@ function EventPopover({
}) {
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 [location, setLocation] = useState("");
const [calendar, setCalendar] = useState("");
const [start, setStart] = useState("");
const [end, setEnd] = useState("");
const [calendarid, setCalendarid] = useState("");
useEffect(() => {
if (selectedRange) {
@@ -35,21 +44,31 @@ function EventPopover({
const handleSave = () => {
const newEvent: CalendarEvent = {
title,
start,
end,
calendar,
extendedProps: {
description,
location,
},
summary,
start: new Date(start),
end: new Date(end),
uid: Date.now().toString(36),
description,
location,
organizer,
attendee: [
{
cn: organizer.cn,
cal_address: organizer.cal_address,
partstat: "ACCEPTED",
rsvp: "FALSE",
role: "CHAIR",
cutype: "INDIVIDUAL",
},
],
transp: "OPAQUE",
};
dispatch(addEvent(newEvent));
console.log(newEvent)
console.log(newEvent);
onClose({}, "backdropClick");
// Reset
setTitle("");
setSummary("");
setDescription("");
setLocation("");
};
@@ -72,11 +91,16 @@ function EventPopover({
<Typography variant="h6" gutterBottom>
Create Event
</Typography>
<Select onClick={(e) => console.log(e.target)}>
{Object.keys(calendars).map((calendar) => (
<MenuItem value={calendar}>{calendars[calendar].name}</MenuItem>
))}
</Select>
<TextField
fullWidth
label="Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
label="summary"
value={summary}
onChange={(e) => setSummary(e.target.value)}
size="small"
margin="dense"
/>
-1
View File
@@ -1,4 +1,3 @@
// store/eventsSlice.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { CalendarEvent } from "./EventsTypes";
+15 -9
View File
@@ -1,11 +1,17 @@
// types/Event.ts
import { userAttendee, userOrganiser } from "../User/userDataTypes";
export interface CalendarEvent {
title: string;
start: string; // ISO date
end?: string;
calendar:string;
extendedProps?: {
description?: string;
location?: string;
};
uid: string;
transp: string;
start: Date; // ISO date
end?: Date;
class?: string;
x_openpass_videoconference?: unknown;
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 { useNavigate } from "react-router-dom";
import { useEffect, useRef } from "react";
import { Callback } from "./oidcAuth";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { useAppDispatch } from "../../app/hooks";
import { push } from "redux-first-history";
import { setUserData } from "./userSlice";
import { Loading } from "../../components/Loading/Loading";
@@ -12,9 +11,6 @@ export function CallbackResume() {
const saved = sessionStorage.getItem("redirectState")
? JSON.parse(sessionStorage.getItem("redirectState")!)
: null;
const [tokens, setTokens] = useState<any>();
const [userInfo, setUserInfo] = useState<any>();
const location = useAppSelector((state) => state.router.location);
useEffect(() => {
if (hasRun.current) {
return;
@@ -24,8 +20,6 @@ export function CallbackResume() {
try {
const data = await Callback(saved?.code_verifier, saved?.state);
console.log("data:", data);
setTokens(data?.tokenSet);
setUserInfo(data?.userinfo);
dispatch(setUserData(data?.userinfo));
sessionStorage.removeItem("redirectState");
// Redirect to main page after successful callback
-1
View File
@@ -22,7 +22,6 @@ export async function getClientConfig() {
}
export async function Auth() {
console.log("bap");
let code_verifier = client.randomPKCECodeVerifier();
let code_challenge = await client.calculatePKCECodeChallenge(code_verifier);
const openIdClientConfig = await getClientConfig();
+14
View File
@@ -3,3 +3,17 @@ export interface userData {
sid: 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 { userData } from "./userDataTypes";
import { userData, userOrganiser } from "./userDataTypes";
export const userSlice = createSlice({
name: "user",
initialState: {
userData: null as unknown as userData,
organiserData: null as unknown as userOrganiser,
},
reducers: {
setUserData: (state, action) => {
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}`;
},
},
});