[#1] added base calendar and started to tweak ui

This commit is contained in:
Camille Moussu
2025-06-27 18:11:23 +02:00
parent cceb83519f
commit 624c1b43ca
19 changed files with 1593 additions and 238 deletions
+2 -4
View File
@@ -2,12 +2,10 @@ import React from "react";
import { screen } from "@testing-library/react";
import App from "../src/App";
import { JSX } from "react/jsx-runtime";
import { renderWithProviders } from "./utils/Renderwithproviders";
test("renders learn react link", () => {
test("renders app", () => {
renderWithProviders(<App />);
const linkElement = screen.getByText("Twake");
expect(linkElement).toBeInTheDocument();
});
function renderWithProviders(arg0: JSX.Element) {
throw new Error("Function not implemented.");
}
+1 -1
View File
@@ -5,7 +5,7 @@ import { Menubar } from "../../src/components/Menubar/Menubar";
import { renderWithProviders } from "../utils/Renderwithproviders";
describe("Calendar App Component Display Tests", () => {
test("renders the Navbar component", () => {
test("renders the Menubar component", () => {
renderWithProviders(<Menubar />);
const navbarElement = screen.getByText("Twake");
expect(navbarElement).toBeInTheDocument();
+976 -149
View File
File diff suppressed because it is too large Load Diff
+14
View File
@@ -3,13 +3,27 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@fullcalendar/core": "^6.1.17",
"@fullcalendar/daygrid": "^6.1.17",
"@fullcalendar/interaction": "^6.1.17",
"@fullcalendar/list": "^6.1.17",
"@fullcalendar/react": "^6.1.17",
"@fullcalendar/timegrid": "^6.1.17",
"@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",
"i18next": "^23.7.9",
"openid-client": "^6.5.3",
"react": "^19.1.0",
"react-calendar": "^6.0.0",
"react-dom": "^19.1.0",
"react-i18next": "^13.5.0",
"react-redux": "^9.2.0",
+6 -2
View File
@@ -24,8 +24,12 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<link href="https://fonts.googleapis.com/css2?family=Cal+Sans&display=swap" rel="stylesheet">
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Cal+Sans&family=Roboto:ital,wght@0,100..900;1,100..900&display=swap"
rel="stylesheet"
/>
<title>React App</title>
</head>
<body>
+1 -9
View File
@@ -1,17 +1,9 @@
.App {
text-align: center;
font-family:"Roboto";
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
+9 -6
View File
@@ -4,19 +4,22 @@ import { HistoryRouter as Router } from "redux-first-history/rr6";
import { Menubar } from "./components/Menubar/Menubar";
import { CallbackResume } from "./features/User/LoginCallback";
import { history } from "./app/store";
import './App.css'
import "./App.css";
import { Loading } from "./components/Loading/Loading";
import HandleLogin from "./features/User/HandleLogin";
import CalendarApp from "./components/Calendar/Calendar";
function App() {
return (
<Suspense fallback="loading">
<Suspense fallback={<Loading />}>
<Router history={history}>
<Routes>
<Route path="/" element={<HandleLogin />} />
<Route
path="/"
path="/calendar"
element={
<div className="App">
<header className="App-header">
<Menubar />
</header>
<Menubar />
<CalendarApp />
</div>
}
/>
+2 -1
View File
@@ -1,6 +1,6 @@
import { combineReducers, configureStore } from "@reduxjs/toolkit";
import userReducer from "../features/User/userSlice";
import eventsReducer from "../features/Events/EventsSlice";
import { createReduxHistoryContext } from "redux-first-history";
import { createBrowserHistory } from "history";
@@ -10,6 +10,7 @@ const { createReduxHistory, routerMiddleware, routerReducer } =
const rootReducer = combineReducers({
router: routerReducer,
user: userReducer,
events: eventsReducer,
});
export const setupStore = (preloadedState?: Partial<RootState>) => {
+184
View File
@@ -0,0 +1,184 @@
main {
display: flex;
flex-direction: row;
}
.calendar {
width: 100%;
height: 100%;
}
.sidebar{
border-right: 2px gray;
}
.fc .fc-daygrid-day {
min-height: 10px !important; /* Default is ~40px */
height: 10px !important;
font-size: 0.75rem;
}
.fc .fc-timegrid-slot {
height: 60px !important; /* Default is ~40px */
}
#calendar {
flex: 1;
overflow: hidden; /* Important: let FullCalendar handle internal scroll */
}
.fc-scroller-liquid-absolute {
overflow-y: auto !important; /* This targets the scrollable part */
max-height: calc(100vh - 100px); /* Adjust based on your header size */
}
.scrollgrid-section-header {
border: white;
}
/* Base container */
.fc-daygrid-day-top {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.5rem 0;
font-family: "Inter", sans-serif;
color: #1e293b; /* dark navy */
}
/* Day number */
.fc-daygrid-day-top .fc-daygrid-day-number {
font-size: 1rem;
font-weight: 600;
line-height: 2;
}
/* Day name (e.g., MON) */
.fc-daygrid-day-top small {
font-size: 0.75rem;
letter-spacing: 0.05em;
color: #94a3b8; /* slate gray */
text-transform: uppercase;
}
/* Todays date highlight */
.current-date {
background-color: orange;
color: white;
border-radius: 50%;
padding: 50px;
}
.fc .fc-col-header-cell,
.fc .fc-scrollgrid table,
.fc-theme-standard .fc-scrollgrid {
background: none;
border: none;
}
/* Container de la sidebar */
.sidebar-calendar {
width: 280px;
background-color: #fff;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
padding: 20px;
display: flex;
flex-direction: column;
gap: 30px;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}
/* Style du mini calendrier */
.react-calendar {
border: none;
font-size: 14px;
width: 100%;
user-select: none;
}
/* Jours sélectionnés et hover */
.react-calendar__tile {
transition: background-color 0.3s ease, color 0.3s ease;
border-radius: 50%;
border: none;
background-color: white;
width: 12px;
height: 37.52px;
}
.react-calendar__tile:hover {
background-color: rgb(224, 224, 224);
border: none;
border-radius: 50%;
cursor: pointer;
}
.react-calendar__tile--active {
background-color: orange;
color: white;
}
/* Titre ou header de la sidebar */
.sidebar-calendar h2 {
font-size: 1.3rem;
font-weight: 700;
color: #333;
margin: 0 0 10px 0;
}
/* Liste des calendriers */
.calendar-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 15px;
}
.calendar-list li {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
color: #444;
padding: 10px 15px;
border-radius: 10px;
transition: background-color 0.2s ease;
cursor: pointer;
}
/* Couleurs distinctes pour chaque type de calendrier */
.calendar-list li.work {
background-color: #d0e8ff;
color: #1a73e8;
}
.calendar-list li.work:hover {
background-color: #b0d4ff;
}
.calendar-list li.personal {
background-color: #ffe0e0;
color: #d93025;
}
.calendar-list li.personal:hover {
background-color: #ffb3b3;
}
/* Petit indicateur couleur à gauche */
.calendar-list li::before {
content: "";
width: 12px;
height: 12px;
border-radius: 50%;
flex-shrink: 0;
}
.calendar-list li.work::before {
background-color: #1a73e8;
}
.calendar-list li.personal::before {
background-color: #d93025;
}
+165
View File
@@ -0,0 +1,165 @@
import FullCalendar from "@fullcalendar/react";
import dayGridPlugin from "@fullcalendar/daygrid";
import timeGridPlugin from "@fullcalendar/timegrid";
import interactionPlugin from "@fullcalendar/interaction";
import { CalendarApi, DateSelectArg } from "@fullcalendar/core";
import ReactCalendar from "react-calendar";
import "./Calendar.css";
import { useRef, useState } from "react";
import { useAppSelector } from "../../app/hooks";
export default function CalendarApp() {
const calendarRef = useRef<CalendarApi | null>(null);
const [selectedDate, setSelectedDate] = useState(new Date());
const [selectedCalendars, setSelectedCalendars] = useState([
"Work",
"Personnal",
]);
const events = useAppSelector((state) => state.events);
const handleCalendarToggle = (name: string) => {
setSelectedCalendars((prev) =>
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 months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
return (
<main>
<div className="sidebar">
<div className="calendar-label">
<div className="calendar-label">
<span>
{months[date.getMonth()]} {date.getFullYear()}
</span>
</div>
<button
onClick={() =>
setDate(new Date(date.getFullYear(), date.getMonth() - 1))
}
>
&lt;
</button>
<button
onClick={() =>
setDate(new Date(date.getFullYear(), date.getMonth() + 1))
}
>
&gt;
</button>
</div>
<ReactCalendar
showNeighboringMonth={false}
calendarType="hebrew"
formatShortWeekday={(locale, date) =>
date.toLocaleDateString(locale, { weekday: "narrow" })
}
value={selectedDate}
onClickDay={(date) => {
setSelectedDate(date);
calendarRef.current?.gotoDate(date);
}}
prevLabel={null}
nextLabel={null}
showNavigation={false}
/>
{["Work", "Personnal", "Holidays"].map((name) => (
<div key={name}>
<label>
<input
type="checkbox"
checked={selectedCalendars.includes(name)}
onChange={() => handleCalendarToggle(name)}
/>
{name}
</label>
</div>
))}
</div>
<div className="calendar">
<FullCalendar
ref={(ref) => {
if (ref) {
calendarRef.current = ref.getApi();
}
}}
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
initialView="timeGridWeek"
weekends={false}
editable={true}
selectable={true}
nowIndicator
selectMirror={true}
views={{
timeGridWeek: { titleFormat: { month: "long", year: "numeric" } },
}}
dayMaxEvents={true}
events={filteredEvent}
weekNumbers
weekNumberFormat={{ week: "long" }}
slotDuration={"00:30:00"}
slotLabelInterval={"00:30:00"}
scrollTime={"07:00:00"}
allDayText=""
slotLabelFormat={{
hour: "2-digit",
minute: "2-digit",
hour12: false,
}}
dayHeaderContent={(arg) => {
const date = arg.date.getDate();
const weekDay = arg.date
.toLocaleDateString("en-US", { weekday: "short" })
.toUpperCase();
return (
<div className="fc-daygrid-day-top">
<small>{weekDay}</small>
<span
className={`fc-daygrid-day-number ${
arg.isToday ? "current-date" : ""
}`}
>
{date}
</span>
</div>
);
}}
eventClick={(info) => {
info.jsEvent.preventDefault(); // don't let the browser navigate
if (info.event.url) {
window.open(info.event.url);
} else {
console.log(info.event);
}
}}
headerToolbar={{
left: "title",
center: "prev,today,next",
right: "dayGridMonth,timeGridWeek,timeGridDay",
}}
/>
</div>
</main>
);
}
+6
View File
@@ -0,0 +1,6 @@
import React from "react";
import logo from "../../static/images/calendar.svg";
export function Error() {
return <p>Error</p>;
}
+6
View File
@@ -0,0 +1,6 @@
import React from "react";
import logo from "../../static/images/calendar.svg";
export function Loading() {
return <img src={logo} />;
}
+1 -5
View File
@@ -16,7 +16,7 @@
padding: 0.2rem;
}
.calendar {
.calendar-text {
font-family: "Cal Sans";
padding: 0.2rem;
font-weight: 400;
@@ -39,12 +39,8 @@
display: flex;
justify-content: space-between;
align-items: center;
background-color: #1a1a1a62;
padding: 0.5rem;
color: white;
position: absolute;
width: 100%;
top: 0;
z-index: 1000;
}
+15 -42
View File
@@ -1,54 +1,27 @@
import React from "react";
import { Auth } from "../../features/User/oidcAuth";
import logo from "../../static/images/calendar.svg";
import "./Menubar.css";
import { useAppSelector } from "../../app/hooks";
export function Menubar() {
return (
<div className="menubar">
<div className="menubar-item tc-home">
<img className="logo" src={logo} />
<p>
<span className="twake">Twake</span>
<span className="calendar">Calendar</span>
</p>
</div>
<div className="menubar-item nav-month">
<p>Current Month</p>
<p className="day-selector"> droite today gauche</p>
</div>
<header className="menubar">
<MainTitle/>
<div className="menubar-item search-bar">
<p>big search bar</p>
</div>
<div className="menubar-item menu-tools">
<HandleLogin />
<div className="menubar-item">
<p>Account stuff</p>
</div>
</header>
);
}
export function MainTitle() {
return (
<div className="menubar-item tc-home">
<img className="logo" src={logo} />
<p>
<span className="twake">Twake</span>
<span className="calendar-text">Calendar</span>
</p>
</div>
);
}
function HandleLogin() {
const userData = useAppSelector((state) => state.user.userData);
if (!userData) {
return (
<button
onClick={async () => {
const loginurl = await Auth();
sessionStorage.setItem(
"redirectState",
JSON.stringify({
code_verifier: loginurl.code_verifier,
state: loginurl.state,
})
);
window.location.assign(loginurl.redirectTo);
}}
>
login
</button>
);
} else {
return <p>{userData.sub}</p>;
}
}
+137
View File
@@ -0,0 +1,137 @@
// 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";
function EventPopover({
anchorEl,
open,
onClose,
selectedRange,
}: {
anchorEl: HTMLElement | null;
open: boolean;
onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void;
selectedRange: any;
}) {
const dispatch = useAppDispatch();
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [location, setLocation] = useState("");
const [calendar, setCalendar] = useState("");
const [start, setStart] = useState("");
const [end, setEnd] = useState("");
useEffect(() => {
if (selectedRange) {
setStart(selectedRange.startStr);
setEnd(selectedRange.endStr ?? "");
}
}, [selectedRange]);
const handleSave = () => {
const newEvent: CalendarEvent = {
title,
start,
end,
calendar,
extendedProps: {
description,
location,
},
};
dispatch(addEvent(newEvent));
console.log(newEvent)
onClose({}, "backdropClick");
// Reset
setTitle("");
setDescription("");
setLocation("");
};
return (
<Popover
open={open}
anchorEl={anchorEl}
onClose={onClose}
anchorOrigin={{
vertical: "top",
horizontal: "left",
}}
transformOrigin={{
vertical: "top",
horizontal: "left",
}}
>
<Box p={2} width={300}>
<Typography variant="h6" gutterBottom>
Create Event
</Typography>
<TextField
fullWidth
label="Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
size="small"
margin="dense"
/>
<TextField
fullWidth
label="Start"
type="datetime-local"
value={start}
onChange={(e) => setStart(e.target.value)}
size="small"
margin="dense"
InputLabelProps={{ shrink: true }}
/>
<TextField
fullWidth
label="End"
type="datetime-local"
value={end}
onChange={(e) => setEnd(e.target.value)}
size="small"
margin="dense"
InputLabelProps={{ shrink: true }}
/>
<TextField
fullWidth
label="Description"
value={description}
onChange={(e) => setDescription(e.target.value)}
size="small"
margin="dense"
multiline
rows={2}
/>
<TextField
fullWidth
label="Location"
value={location}
onChange={(e) => setLocation(e.target.value)}
size="small"
margin="dense"
/>
<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 EventPopover;
+17
View File
@@ -0,0 +1,17 @@
// store/eventsSlice.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { CalendarEvent } from "./EventsTypes";
const eventsSlice = createSlice({
name: "events",
initialState: [] as CalendarEvent[],
reducers: {
addEvent: (state, action: PayloadAction<CalendarEvent>) => {
state.push(action.payload);
},
},
});
export const { addEvent } = eventsSlice.actions;
export default eventsSlice.reducer;
+11
View File
@@ -0,0 +1,11 @@
// types/Event.ts
export interface CalendarEvent {
title: string;
start: string; // ISO date
end?: string;
calendar:string;
extendedProps?: {
description?: string;
location?: string;
};
}
+38
View File
@@ -0,0 +1,38 @@
import React, { useEffect } from "react";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { Auth } from "./oidcAuth";
import { Loading } from "../../components/Loading/Loading";
import { Error } from "../../components/Error/Error";
import { push } from "redux-first-history";
export function HandleLogin() {
const userData = useAppSelector((state) => state.user.userData);
const dispatch = useAppDispatch();
useEffect(() => {
const initiateLogin = async () => {
if (!userData) {
const loginurl = await Auth();
sessionStorage.setItem(
"redirectState",
JSON.stringify({
code_verifier: loginurl.code_verifier,
state: loginurl.state,
})
);
window.location.assign(loginurl.redirectTo);
}
};
initiateLogin();
}, [userData]);
if (!userData) {
return <Error />;
}
dispatch(push("/calendar"));
return <Loading />;
}
export default HandleLogin;
+2 -19
View File
@@ -4,6 +4,7 @@ import { Callback } from "./oidcAuth";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { push } from "redux-first-history";
import { setUserData } from "./userSlice";
import { Loading } from "../../components/Loading/Loading";
export function CallbackResume() {
const dispatch = useAppDispatch();
@@ -43,23 +44,5 @@ export function CallbackResume() {
}
}, [dispatch, saved]);
return (
<div>
<p>Processing OIDC callback...</p>
{/* Optionally show loading or debug info */}
{tokens && (
<ul>
<li>
ID_Token: <pre>{JSON.stringify(tokens?.id_token)}</pre>
</li>
<li>
Access_Token: <pre>{JSON.stringify(tokens?.access_token)}</pre>
</li>
<li>
User info: <pre>{JSON.stringify(userInfo, null, 2)}</pre>
</li>
</ul>
)}
</div>
);
return <Loading />;
}