Merge remote-tracking branch 'refs/remotes/origin/main'

This commit is contained in:
Alexis Leboeuf
2026-01-07 12:19:16 +01:00
13 changed files with 333 additions and 40 deletions

View File

@@ -3,4 +3,5 @@
background-color: var(--tint4); background-color: var(--tint4);
color: var(--text); color: var(--text);
min-height: 100vh; min-height: 100vh;
padding: 16px;
} }

View File

@@ -1,4 +1,4 @@
import React from 'react'; import React, { useEffect } from 'react';
import './App.css'; import './App.css';
import { ReactKeycloakProvider } from '@react-keycloak/web' import { ReactKeycloakProvider } from '@react-keycloak/web'
import keycloak from './keycloak' import keycloak from './keycloak'
@@ -7,6 +7,7 @@ import { LocalDataProvider } from './provider/LocalDataProvider';
import EDT from './components/edt'; import EDT from './components/edt';
import SwitchThemeColor from './components/SwitchThemeColor'; import SwitchThemeColor from './components/SwitchThemeColor';
import CreateSession from './components/createSession' import CreateSession from './components/createSession'
import RessourcePanel from './components/ressourcePanel';
const keycloakInitOptions = { const keycloakInitOptions = {
@@ -14,7 +15,10 @@ const keycloakInitOptions = {
checkLoginIframe: false checkLoginIframe: false
} }
function App() { function App() {
return ( return (
<ReactKeycloakProvider authClient={keycloak} /*initOptions={keycloakInitOptions}*/> <ReactKeycloakProvider authClient={keycloak} /*initOptions={keycloakInitOptions}*/>
<LocalDataProvider> <LocalDataProvider>
@@ -22,6 +26,7 @@ function App() {
<SwitchThemeColor/> <SwitchThemeColor/>
<h1>Frisbyee</h1> <h1>Frisbyee</h1>
<Login/> <Login/>
<RessourcePanel/>
<EDT/> <EDT/>
<CreateSession/> <CreateSession/>
</div> </div>

View File

@@ -11,6 +11,7 @@ export class Admin extends User{
} }
export class Athlete extends User{ export class Athlete extends User{
nom!: String;
groupe!: Groupe; groupe!: Groupe;
} }
@@ -23,7 +24,7 @@ export class Session{
id!: number; id!: number;
name!: String; name!: String;
activites: Activite[] = []; activites: Activite[] = [];
isRecurent! : Boolean; isRecurrent! : Boolean;
creneau!: Date; creneau!: Date;
coach!: Coach; coach!: Coach;
athletes!: Athlete[] athletes!: Athlete[]
@@ -60,10 +61,83 @@ export function getUserTest():User{
s3.id = 3; s3.id = 3;
s3.name = "entraintement3" s3.name = "entraintement3"
const a1:Activite = new Activite(); const athlete1 = new Athlete();
const a2:Activite = new Activite(); athlete1.id = 1;
s1.activites.push(a1); athlete1.nom = "Alice Dupont";
s1.activites.push(a2); athlete1.groupe = "Entrainement";
const athlete2 = new Athlete();
athlete2.id = 2;
athlete2.nom = "Bob Martin";
athlete2.groupe = "Competition";
const athlete3 = new Athlete();
athlete3.id = 3;
athlete3.nom = "Clara Lopez";
athlete3.groupe = "Loisir";
s1.athletes = [athlete1, athlete2];
s2.athletes = [athlete2, athlete3];
s3.athletes = [athlete1, athlete3];
const act1 = new Activite();
act1.id = 1;
act1.nom = "Échauffement";
act1.theme = "Cardio";
act1.duree = 15;
act1.session = s1;
act1.data = new Map([["objectif", "Préparer le corps"], ["matériel", "Ballon"]]);
const act2 = new Activite();
act2.id = 2;
act2.nom = "Dribbles et passes";
act2.theme = "Technique";
act2.duree = 30;
act2.session = s1;
act2.data = new Map([["objectif", "Améliorer les passes"], ["niveau", "Intermédiaire"]]);
const act3 = new Activite();
act3.id = 3;
act3.nom = "Renforcement musculaire";
act3.theme = "Force";
act3.duree = 25;
act3.session = s2;
act3.data = new Map([["objectif", "Renforcer les jambes"], ["matériel", "Haltères"]]);
const act4 = new Activite();
act4.id = 4;
act4.nom = "Sprint et agilité";
act4.theme = "Vitesse";
act4.duree = 20;
act4.session = s2;
act4.data = new Map([["objectif", "Améliorer les sprints"], ["matériel", "Plots"]]);
const act5 = new Activite();
act5.id = 5;
act5.nom = "Match 5v5";
act5.theme = "Jeu";
act5.duree = 60;
act5.session = s3;
act5.data = new Map([["objectif", "Appliquer les techniques"], ["niveau", "Avancé"]]);
const act6 = new Activite();
act6.id = 6;
act6.nom = "Étirements";
act6.theme = "Récupération";
act6.duree = 10;
act6.session = s3;
act6.data = new Map([["objectif", "Éviter les blessures"], ["matériel", "Tapis"]]);
// attach the concrete activities to their sessions
s1.activites.push(act1);
s1.activites.push(act2);
s2.activites.push(act3);
s2.activites.push(act4);
s3.activites.push(act5);
s3.activites.push(act6);
user.sessions.push(s1); user.sessions.push(s1);
user.sessions.push(s2); user.sessions.push(s2);
user.sessions.push(s3); user.sessions.push(s3);

View File

@@ -37,7 +37,7 @@ export const CreateSession = () => {
newSession.groupe = groupe; newSession.groupe = groupe;
newSession.creneau = new Date(creneau); newSession.creneau = new Date(creneau);
newSession.duree= duree; newSession.duree= duree;
newSession.isRecurent= isRecurent; newSession.isRecurrent= isRecurent;
newSession.coach= user as Coach; newSession.coach= user as Coach;
newSession.athletes= []; newSession.athletes= [];
newSession.activites= activities; newSession.activites= activities;

View File

@@ -0,0 +1,27 @@
import Dropdown from 'react-bootstrap/Dropdown';
import { Athlete } from '../classes';
type Props = {
onAthletesClick: () => void;
onActivitiesClick: () => void;
}
function ListButton({ onAthletesClick, onActivitiesClick }: Props) {
return (
<Dropdown>
<Dropdown.Toggle>
Sélectionner la ressource
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item as="button" onClick={onAthletesClick}>
Athlètes
</Dropdown.Item>
<Dropdown.Item as="button" onClick={onActivitiesClick}>
Activités
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
);
}
export default ListButton;

View File

@@ -4,7 +4,8 @@ import { useLocalData } from "../context/useLocalData"
import './style/edt.css'; import './style/edt.css';
import {updateSessionsOfUserAPI } from "../requetes"; import {updateSessionsOfUserAPI } from "../requetes";
import EdtSession from "./edt_session"; import EdtSession from "./edt_session";
import {delay} from "../requetes";
import Loading from "./loading";
export function dateToString(date:Date){ export function dateToString(date:Date){
const dd_prefix = date.getDate()<10 ? "0" : ""; const dd_prefix = date.getDate()<10 ? "0" : "";
@@ -28,11 +29,14 @@ export const EDT =() =>{
const {user,setUser} = useLocalData() const {user,setUser} = useLocalData()
const [sessions, setSessions] = useState<Session[]>([]) const [sessions, setSessions] = useState<Session[]>([])
const [week,setWeek] = useState<Date>(getFirstDay(new Date())); const [week,setWeek] = useState<Date>(getFirstDay(new Date()));
const [loadedWeek,setLoadedWeek] = useState<Date|null>(null);
const [loading,setLoading] = useState<boolean>(false);
const week_days:String[] = ["Lundi","Mardi","Mercredi","Jeudi","Vendredi","Samedi","Dimanche"]; const week_days:String[] = ["Lundi","Mardi","Mercredi","Jeudi","Vendredi","Samedi","Dimanche"];
const week_days_nums:number[] = [1,2,3,4,5,6,0]; const week_days_nums:number[] = [1,2,3,4,5,6,0];
function loadSessions(date:Date){ function loadSessions(date:Date){
var maxDate = getNextDay(date,6) var maxDate = getNextDay(date,6)
@@ -47,18 +51,39 @@ export const EDT =() =>{
function changeWeek(date:Date){ function changeWeek(date:Date){
setWeek(date); setWeek(date);
loadSessions(date) }
function isSameDay(date1:Date,date2:Date){
return (
date1.getDay()===date2.getDay() &&
date1.getMonth()===date2.getMonth() &&
date1.getFullYear()===date2.getFullYear());
} }
useEffect(() => { useEffect(() => {
updateWeek(); setLoadedWeek(null);
},[week]) updateWeek(week);
loadSessions(week)
setLoading(true);
},[week,user])
async function updateWeek(){ useEffect(() => {
if(loadedWeek!==null){
if(isSameDay(week,loadedWeek)){
loadSessions(week)
setLoading(false);
}
else{
setLoadedWeek(null);
}
}
},[loadedWeek])
async function updateWeek(week:Date){
//TODO updateSession //TODO updateSession
await delay(2000);
//await updateSessionsOfUser(user,null,null); //await updateSessionsOfUser(user,null,null);
loadSessions(week); setLoadedWeek(week);
setUser(getUserTest())
} }
@@ -96,14 +121,14 @@ export const EDT =() =>{
<button className="edt_button_week_select" onClick={() => handleNext()}>Next</button> <button className="edt_button_week_select" onClick={() => handleNext()}>Next</button>
</div> </div>
<div className="edt_colonnes"> <div className="edt_colonnes">
<div className="edt_loading">{loading && <Loading/>}</div>
{week_days_nums.map((num,index)=>( {week_days_nums.map((num,index)=>(
<div className="edt_colonne"> <div className="edt_colonne">
<div className="edt_day_header"> <div className="edt_day_header">
<div> {week_days[index]} </div> <div> {week_days[index]} </div>
<div className="edt_date"> {dateToString(getNextDay(week,index))} </div> <div className="edt_date"> {dateToString(getNextDay(week,index))} </div>
</div> </div>
<div className="edt_day_contedt"> <div className="edt_day_content">
{sessions.map((session,index2)=>( {sessions.map((session,index2)=>(
session.creneau.getDay()===num && session.creneau.getDay()===num &&
<EdtSession session={session}/> <EdtSession session={session}/>

View File

@@ -4,6 +4,7 @@ import { dateToString, hoursToString } from './edt';
import './style/edt.css'; import './style/edt.css';
import { Modal } from './Modal'; import { Modal } from './Modal';
import Loading from './loading'; import Loading from './loading';
import {delay} from "../requetes";
type Props = { type Props = {
@@ -21,6 +22,7 @@ function EdtSession({session}:Props){
async function updateActivites(){ async function updateActivites(){
//TODO //TODO
await delay(2000);
//await updateActivitiesOfSessionAPI(session); //await updateActivitiesOfSessionAPI(session);
setLoading(false); setLoading(false);
} }
@@ -37,19 +39,24 @@ function EdtSession({session}:Props){
return( return(
<div> <div>
<div className="edt_session" onClick={() => handleOpen()}> <div className="edt_session" onClick={() => handleOpen()}>
<div className="edt_date">{hoursToString(sDate)}</div> <div className="edt_session_header">
<div className="edt_date">{hoursToString(sDate)}</div>
{!session.isRecurrent && <div className="edt_date"> recurrent</div>}
</div>
<div>{session.name}</div> <div>{session.name}</div>
</div> </div>
{open && {open &&
<Modal isOpen={open} onClose={() => setOpen(false)}> <Modal isOpen={open} onClose={() => setOpen(false)}>
<div>{session.name}</div> <div className="edt_session_modal">
<div>{hoursToString(sDate)}</div> <div>{session.name}</div>
<div>{dateToString(sDate)}</div> <div>{hoursToString(sDate)}</div>
{session.activites.map((activite,index)=>( <div>{dateToString(sDate)}</div>
<div>activite</div> {session.activites.map((activite,index)=>(
))} <div>activite</div>
))}
{loading && <div className='edt_loading'><Loading/></div>}
</div>
{loading && <Loading/>}
</Modal> </Modal>
} }
</div> </div>

View File

@@ -1,19 +1,40 @@
import { useKeycloak } from '@react-keycloak/web' import { useKeycloak } from '@react-keycloak/web'
import { useEffect } from 'react';
import { getUserTest, User } from '../classes';
import { useLocalData } from '../context/useLocalData';
export const Login =() =>{ export const Login =() =>{
const {user,setUser} = useLocalData()
useEffect(() => { //TODO à supprimer
setUser(getUserTest())
},[]);
function handleLogin(): void {
keycloak.login()
//TODO setUser
}
function handleLogout(): void {
keycloak.logout()
setUser(new User());
}
const { keycloak } = useKeycloak() const { keycloak } = useKeycloak()
return( return(
<div> <div>
<div> <div>
Authenticated : {keycloak.authenticated ? 'oui' : 'non'} Authenticated : {keycloak.authenticated ? 'oui' : 'non'}
</div> </div>
<button onClick={() => keycloak.login()}> <button onClick={() => handleLogin()}>
Se connecter Se connecter
</button> </button>
<button onClick={() => keycloak.logout()}> <button onClick={() => handleLogout()}>
Se déconnecter Se déconnecter
</button> </button>
</div> </div>

View File

@@ -0,0 +1,47 @@
import ListGroup from "react-bootstrap/ListGroup";
import { Athlete, Activite } from "../classes";
type Props = {
athletes: Athlete[];
activites: Activite[];
};
function AthleteList({ athletes }: Props) {
return (
<ListGroup>
{athletes.map((athlete) => (
<ListGroup.Item key={athlete.id}>
<div>
<strong>Nom:</strong> {athlete.nom}
</div>
<div>
<strong>Groupe:</strong> {athlete.groupe}
</div>
</ListGroup.Item>
))}
</ListGroup>
);
}
function ActiviteList({ activites }: Props) {
return (
<ListGroup>
{activites.map((activite) => (
<ListGroup.Item key={activite.id}>
<div>
<strong>Nom:</strong> {activite.nom}
</div>
<div>
<strong>Thème:</strong> {activite.theme}
</div>
<div>
<strong>Durée:</strong> {activite.duree} minutes
</div>
</ListGroup.Item>
))}
</ListGroup>
);
}
export { AthleteList, ActiviteList };

View File

@@ -0,0 +1,52 @@
import { useState } from "react";
import { useLocalData } from "../context/useLocalData";
import ListButton from "./dropdownButton";
import { AthleteList, ActiviteList } from "./ressourceList";
import { Activite, Athlete } from "../classes";
export default function RessourcePanel() {
const { user } = useLocalData();
const [showAthletes, setShowAthletes] = useState(false);
const [showActivites, setShowActivites] = useState(false);
const athleteMap: Map<number, Athlete> = new Map();
user.sessions.forEach(session => {
session.athletes?.forEach(a => athleteMap.set(a.id, a));
});
const allAthletes: Athlete[] = Array.from(athleteMap.values());
const activiteMap: Map<number, Activite> = new Map();
user.sessions.forEach(session => {
session.activites?.forEach(act => activiteMap.set(act.id, act));
});
const allActivites: Activite[] = Array.from(activiteMap.values());
return (
<div className="ressource_panel">
<ListButton
onAthletesClick={() => {
setShowAthletes(prev => !prev);
setShowActivites(false);
}}
onActivitiesClick={() => {
setShowActivites(prev => !prev);
setShowAthletes(false);
}}
/>
{showAthletes && (
<div className="edt_athletes_panel">
<h3>Liste des athlètes</h3>
<AthleteList athletes={allAthletes} activites={[]}/>
</div>
)}
{showActivites && (
<div className="edt_activites_panel">
<h3>Liste des activités</h3>
<ActiviteList athletes={[]} activites={allActivites} />
</div>
)}
</div>
);
}

View File

@@ -1,56 +1,74 @@
.edt{ .edt{
justify-contedt: cedter; justify-content: center;
background-color: var(--tint1); background-color: var(--tint1);
border-radius: 30px; border-radius: 30px;
padding: 10px; padding: 10px;
} }
.edt_header{ .edt_header{
justify-contedt: cedter; justify-content: center;
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 0.5fr);
/* background-color: #0000FF; */ /* background-color: #0000FF; */
padding-bottom: 10px; padding-bottom: 10px;
gap: 30%; gap: 30%;
} }
.edt_colonnes { .edt_colonnes {
position: relative;
display: grid; display: grid;
align-items: flex-start; align-items: flex-start;
grid-template-columns: repeat(7, 1fr); grid-template-columns: repeat(7, minmax(0, 1fr));
gap: 16px; gap: 16px;
color: var(--text); color: var(--text);
/* background-color: #00FF00; */ /* background-color: #00FF00; */
width: 100%; width: 100%;
} }
.edt_loading{
position: absolute;
inset: 0;
display: grid;
/* place-items: center; */
pointer-events: none;
}
.edt_colonne { .edt_colonne {
display: grid; display: grid;
background-color: var(--tint3); background-color: var(--tint3);
border-radius: 20px; border-radius: 20px;
container-type: inline-size;
} }
.edt_day_header{ .edt_day_header{
font-size: clamp(5px, 1vw, 18px);
padding: 8px; padding: 8px;
/* background-color: var(--tint2); */ /* background-color: var(--tint2); */
border-radius: 20px; border-radius: 20px;
height: 30px; height: 30px;
text-align: cedter; text-align: center;
font-size: 1em; font-size: 1em;
} }
.edt_day_contedt{ .edt_day_content{
display: grid; display: flex;
flex-direction: column;
gap: 8px; gap: 8px;
padding: 8px; padding: 8px;
border-radius: 20px; border-radius: 20px;
min-width: 0;
/* background-color: #FF0000; */
} }
.edt_session { .edt_session {
font-size: clamp(1px, 8cqi, 18px);
gap: 8px; gap: 8px;
background-color: var(--tint4); background-color: var(--tint4);
border-radius: 12px; border-radius: 12px;
padding: 8px; padding: 5%;
min-width: 0;
} }
.edt_session:hover { .edt_session:hover {
@@ -61,6 +79,11 @@
background-color: var(--tint5); background-color: var(--tint5);
} }
.edt_session_header{
display: flex;
gap: 5px;
}
.edt_date{ .edt_date{
font-size: 0.75em; font-size: 0.75em;
} }
@@ -72,3 +95,11 @@
border-radius: 20px; border-radius: 20px;
} }
.edt_session_modal{
background-color: var(--tint2);
padding: 20px;
border-radius: 20px;
position: relative;
}

View File

@@ -49,16 +49,14 @@ code {
} }
.modal{ .modal{
background-color: var(--tint2);
padding: 10px; padding: 10px;
border-radius: 20px;
min-width: 200px; min-width: 200px;
min-height: 100px; min-height: 100px;
} }
.loading{ .loading{
width: 24px; width: 40px;
height: 24px; height: 40px;
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
} }

View File

@@ -8,6 +8,11 @@ const useAuthHeader = () => {
return keycloak?.token return keycloak?.token
? { Authorization: `Bearer ${keycloak.token}` } ? { Authorization: `Bearer ${keycloak.token}` }
: {} : {}
}*/
//debug:
export function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
} }
*/ */
//UPDATE ///////////////////////////////////////////////////////// //UPDATE /////////////////////////////////////////////////////////