result = new ArrayList<>();
+}
diff --git a/src/main/java/fr/titionfire/ffsaf/utils/RoleAsso.java b/src/main/java/fr/titionfire/ffsaf/utils/RoleAsso.java
index 1051a16..84991b1 100644
--- a/src/main/java/fr/titionfire/ffsaf/utils/RoleAsso.java
+++ b/src/main/java/fr/titionfire/ffsaf/utils/RoleAsso.java
@@ -4,19 +4,17 @@ import io.quarkus.runtime.annotations.RegisterForReflection;
@RegisterForReflection
public enum RoleAsso {
- MEMBRE("Membre"),
- PRESIDENT("Président"),
- TRESORIER("Trésorier"),
- SECRETAIRE("Secrétaire");
+ MEMBRE("Membre", 0),
+ PRESIDENT("Président", 3),
+ TRESORIER("Trésorier", 1),
+ SECRETAIRE("Secrétaire", 2);
- public String name;
+ public final String name;
+ public final int level;
- RoleAsso(String name) {
- this.name = name;
- }
-
- public void setName(String name) {
+ RoleAsso(String name, int level) {
this.name = name;
+ this.level = level;
}
@Override
diff --git a/src/main/java/fr/titionfire/ffsaf/utils/Utils.java b/src/main/java/fr/titionfire/ffsaf/utils/Utils.java
new file mode 100644
index 0000000..1a33bb2
--- /dev/null
+++ b/src/main/java/fr/titionfire/ffsaf/utils/Utils.java
@@ -0,0 +1,22 @@
+package fr.titionfire.ffsaf.utils;
+
+import java.util.Calendar;
+import java.util.Date;
+
+public class Utils {
+
+ public static int getSaison() {
+ return getSaison(new Date());
+ }
+
+ public static int getSaison(Date date) {
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTime(date);
+
+ if (calendar.get(Calendar.MONTH) >= Calendar.SEPTEMBER) {
+ return calendar.get(Calendar.YEAR);
+ } else {
+ return calendar.get(Calendar.YEAR) - 1;
+ }
+ }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 053cf03..92aa416 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -34,13 +34,16 @@ database.port=3306
database.user=root
database.pass=
+siren-api.key=siren-ap
+quarkus.rest-client."fr.titionfire.ffsaf.rest.client.SirenService".url=https://data.siren-api.fr/
+
#Login
quarkus.oidc.token-state-manager.split-tokens=true
quarkus.oidc.token.refresh-expired=true
quarkus.oidc.authentication.redirect-path=/api/auth/login
quarkus.oidc.logout.path=/api/logout
-quarkus.oidc.logout.post-logout-path=/index.html
+quarkus.oidc.logout.post-logout-path=/
# Only the authenticated users can initiate a logout:
quarkus.http.auth.permission.authenticated.paths=api/logout,api/auth/login
diff --git a/src/main/webapp/src/App.jsx b/src/main/webapp/src/App.jsx
index 04b7a97..e5e6d24 100644
--- a/src/main/webapp/src/App.jsx
+++ b/src/main/webapp/src/App.jsx
@@ -10,6 +10,8 @@ import {ToastContainer} from "react-toastify";
import './App.css'
import 'react-toastify/dist/ReactToastify.css';
+import {ClubRoot, getClubChildren} from "./pages/club/ClubRoot.jsx";
+import {DemandeAff, DemandeAffOk} from "./pages/DemandeAff.jsx";
const router = createBrowserRouter([
{
@@ -25,6 +27,24 @@ const router = createBrowserRouter([
path: 'admin',
element: ,
children: getAdminChildren()
+ },
+ {
+ path: 'club',
+ element: ,
+ children: getClubChildren()
+ },
+ {
+ path: 'affiliation',
+ children: [
+ {
+ path: '',
+ element:
+ },
+ {
+ path: 'ok',
+ element:
+ }
+ ]
}
]
},
diff --git a/src/main/webapp/src/components/ColoredCircle.jsx b/src/main/webapp/src/components/ColoredCircle.jsx
index 2f9d574..55fab61 100644
--- a/src/main/webapp/src/components/ColoredCircle.jsx
+++ b/src/main/webapp/src/components/ColoredCircle.jsx
@@ -13,4 +13,16 @@ export const ColoredCircle = ({color, boolean}) => {
return
+};
+
+export const ColoredText = ({boolean, text={true: "Oui", false: "Non"}}) => {
+ const styles = {color: '#F00'};
+
+ if (boolean !== undefined) {
+ styles.color = (boolean) ? '#00c700' : '#e50000';
+ }
+
+ return
+ {text[boolean]}
+
};
\ No newline at end of file
diff --git a/src/main/webapp/src/components/MemberCustomFiels.jsx b/src/main/webapp/src/components/MemberCustomFiels.jsx
new file mode 100644
index 0000000..6a00bce
--- /dev/null
+++ b/src/main/webapp/src/components/MemberCustomFiels.jsx
@@ -0,0 +1,91 @@
+import {useEffect, useState} from "react";
+import {getCategoryFormBirthDate} from "../utils/Tools.js";
+
+export function BirthDayField({inti_date, inti_category}) {
+ const [date, setDate] = useState(inti_date)
+ const [category, setCategory] = useState(inti_category)
+ const [canUpdate, setCanUpdate] = useState(false)
+ useEffect(() => {
+ const b = category !== getCategoryFormBirthDate(new Date(date), new Date('2023-09-01'))
+ if (b !== canUpdate)
+ setCanUpdate(b)
+ }, [date, category])
+
+ const updateCat = _ => {
+ setCategory(getCategoryFormBirthDate(new Date(date), new Date('2023-09-01')))
+ }
+
+
+ return <>
+
+ Date de naissance
+ setDate(e.target.value)}/>
+
+
+
+ Catégorie
+
+ {canUpdate && Mettre à jours }
+
+
+ >
+}
+
+export function OptionField({name, text, values, value, disabled=false}) {
+ return
+
+ {text}
+
+ {Object.keys(values).map((key, _) => {
+ return ({values[key]} )
+ })}
+
+
+
+}
+
+export function TextField({name, text, value, placeholder, type = "text"}) {
+ return
+}
+
+export function CheckField({name, text, value, row = false}) {
+ return <>{
+ row ?
+
+ :
+
+ {text}
+
+ }
+ >
+}
+
+export const Checkbox = ({ label, value, onChange }) => {
+ const handleChange = () => {
+ onChange(!value);
+ };
+
+ return
+
+ {label}
+
+};
\ No newline at end of file
diff --git a/src/main/webapp/src/components/Nav.jsx b/src/main/webapp/src/components/Nav.jsx
index 3b95604..3e5c6fb 100644
--- a/src/main/webapp/src/components/Nav.jsx
+++ b/src/main/webapp/src/components/Nav.jsx
@@ -22,7 +22,9 @@ export function Nav() {
@@ -31,6 +33,33 @@ export function Nav() {
}
+
+function AffiliationMenu() {
+ const {is_authenticated} = useAuth()
+
+ if (is_authenticated)
+ return <>>
+ return Demande d'affiliation
+}
+
+function ClubMenu() {
+ const {is_authenticated, userinfo} = useAuth()
+
+ if (!is_authenticated || !(userinfo?.roles?.includes("club_president")
+ || userinfo?.roles?.includes("club_secretaire") || userinfo?.roles?.includes("club_tresorier")))
+ return <>>
+
+ return
+
+ Club
+
+
+
+}
+
function AdminMenu() {
const {is_authenticated, userinfo} = useAuth()
diff --git a/src/main/webapp/src/hooks/useFetch.js b/src/main/webapp/src/hooks/useFetch.js
index f85bddf..99379fc 100644
--- a/src/main/webapp/src/hooks/useFetch.js
+++ b/src/main/webapp/src/hooks/useFetch.js
@@ -19,12 +19,16 @@ export function useFetch(url, setLoading = null, loadingLevel = 1, config = {})
const [data, setData] = useState(null)
const [error, setErrors] = useState(null)
- useEffect(() => {
+ const refresh = (url) => {
stdAction(apiAxios.get(url, config), setData, setErrors, setLoading, loadingLevel)
+ }
+
+ useEffect(() => {
+ refresh(url)
}, []);
return {
- data, error
+ data, error, refresh
}
}
diff --git a/src/main/webapp/src/pages/DemandeAff.jsx b/src/main/webapp/src/pages/DemandeAff.jsx
new file mode 100644
index 0000000..585bae7
--- /dev/null
+++ b/src/main/webapp/src/pages/DemandeAff.jsx
@@ -0,0 +1,187 @@
+import {useState} from "react";
+import {apiAxios} from "../utils/Tools.js";
+import {toast} from "react-toastify";
+import {useNavigate} from "react-router-dom";
+
+function reconstruireAdresse(infos) {
+ let adresseReconstruite = "";
+ adresseReconstruite += infos.numero_voie + ' ' + infos.type_voie + ' ';
+ adresseReconstruite += infos.libelle_voie + ', ';
+ adresseReconstruite += infos.code_postal + ' ' + infos.libelle_commune + ', ';
+
+ if (infos.complement_adresse) {
+ adresseReconstruite += infos.complement_adresse + ', ';
+ }
+ if (infos.code_cedex && infos.libelle_cedex) {
+ adresseReconstruite += 'Cedex ' + infos.code_cedex + ' - ' + infos.libelle_cedex;
+ }
+
+ if (adresseReconstruite.endsWith(', ')) {
+ adresseReconstruite = adresseReconstruite.slice(0, -2);
+ }
+
+ return adresseReconstruite;
+}
+
+
+export function DemandeAff() {
+ const navigate = useNavigate();
+
+ const submit = (event) => {
+ event.preventDefault()
+ const formData = new FormData(event.target)
+ toast.promise(
+ apiAxios.post(`asso/affiliation`, formData),
+ {
+ pending: "Enregistrement de la demande d'affiliation en cours",
+ success: "Demande d'affiliation enregistrée avec succès 🎉",
+ error: "Échec de la demande d'affiliation 😕"
+ }
+ ).then(_ => {
+ navigate("/affiliation/ok")
+ })
+ }
+
+ return
+
Demande d'affiliation
+
L'affiliation est annuelle et valable pour une saison sportive : du 1er septembre au 31 août de l’année suivante.
+ Pour s’affilier, une association sportive doit réunir les conditions suivantes :
+
+ Avoir son siège social en France ou Principauté de Monaco
+ Être constituée conformément au chapitre 1er du titre II du livre 1er du Code du Sport
+ Poursuivre un objet social entrant dans la définition de l’article 1 des statuts de la Fédération
+ Disposer de statuts compatibles avec les principes d’organisation et de fonctionnement de la Fédération
+ Assurer en son sein la liberté d’opinion et le respect des droits de la défense, et s’interdire toute discrimination
+ Respecter les règles d’encadrement, d’hygiène et de sécurité établies par les règlements de la Fédération
+
+
+
+
+}
+
+function AssoInfo() {
+ const [denomination, setDenomination] = useState("")
+ const [siren, setSiren] = useState("")
+ const [rna, setRna] = useState("")
+ const [rnaEnable, setRnaEnable] = useState(false)
+ const [adresse, setAdresse] = useState("")
+
+ const fetchSiren = () => {
+ toast.promise(
+ apiAxios.get(`asso/siren/${siren}`),
+ {
+ pending: "Recherche de l'association en cours",
+ success: "Association trouvée avec succès 🎉",
+ error: "Échec de la recherche de l'association 😕"
+ }
+ ).then(data => {
+ const data2 = data.data.unite_legale
+ setDenomination(data2.denomination)
+ setRnaEnable(data2.identifiant_association === null)
+ setRna(data2.identifiant_association ? data2.identifiant_association : "")
+ setAdresse(reconstruireAdresse(data2.etablissement_siege))
+ })
+ }
+
+ return <>
+
+ Nom de l'association*
+
+
+
+
+ N° SIREN*
+ setSiren(e.target.value)}/>
+ Rechercher
+
+
+
+ Dénomination
+
+
+
+
+ RNA
+ setRna(e.target.value)}/>
+
+
+
+ Adresse*
+ setAdresse(e.target.value)}/>
+
+
+
+ Status*
+
+
+
+
+ Logo*
+
+
+ >;
+}
+
+function MembreInfo({role}) {
+ return
+}
+
+export function DemandeAffOk() {
+ return (
+
+
Demande d'affiliation envoyée avec succès
+
Une fois votre demande validée, vous recevrez un login et mot de passe provisoire pour accéder à votre espace FFSAF
+
+ );
+}
\ No newline at end of file
diff --git a/src/main/webapp/src/pages/MemberList.jsx b/src/main/webapp/src/pages/MemberList.jsx
new file mode 100644
index 0000000..8fbd771
--- /dev/null
+++ b/src/main/webapp/src/pages/MemberList.jsx
@@ -0,0 +1,234 @@
+import {useLoadingSwitcher} from "../hooks/useLoading.jsx";
+import {useFetch} from "../hooks/useFetch.js";
+import {AxiosError} from "../components/AxiosError.jsx";
+import {ThreeDots} from "react-loader-spinner";
+import {useEffect, useState} from "react";
+import {Input} from "../components/Input.jsx";
+import {useLocation, useNavigate} from "react-router-dom";
+import {Checkbox} from "../components/MemberCustomFiels.jsx";
+import axios from "axios";
+import {apiAxios} from "../utils/Tools.js";
+import {toast} from "react-toastify";
+
+const removeDiacritics = str => {
+ return str
+ .normalize('NFD')
+ .replace(/[\u0300-\u036f]/g, '')
+}
+
+export function MemberList({source}) {
+ const {hash} = useLocation();
+ const navigate = useNavigate();
+ let page = Number(hash.substring(1));
+ page = (page > 0) ? page : 1;
+
+ const [memberData, setMemberData] = useState([]);
+ const [licenceData, setLicenceData] = useState([]);
+ const [showLicenceState, setShowLicenceState] = useState(false);
+ const [clubFilter, setClubFilter] = useState("");
+ const [lastSearch, setLastSearch] = useState("");
+
+ const setLoading = useLoadingSwitcher()
+ const {data, error, refresh} = useFetch(`/member/find/${source}?page=${page}`, setLoading, 1)
+
+
+ useEffect(() => {
+ refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}`);
+ }, [hash, clubFilter]);
+
+ useEffect(() => {
+ if (!data)
+ return;
+ const data2 = [];
+ for (const e of data.result) {
+ data2.push({
+ id: e.id,
+ fname: e.fname,
+ lname: e.lname,
+ club: e.club,
+ licence_number: e.licence,
+ licence: showLicenceState ? licenceData.find(licence => licence.membre === e.id) : null
+ })
+ }
+ setMemberData(data2);
+ }, [data, licenceData]);
+
+ useEffect(() => {
+ if (!showLicenceState)
+ return;
+
+ toast.promise(
+ apiAxios.get(`/licence/current/${source}`),
+ {
+ pending: "Chargement des licences...",
+ success: "Licences chargées",
+ error: "Impossible de charger les licences"
+ })
+ .then(data => {
+ setLicenceData(data.data);
+ });
+ }, [showLicenceState]);
+
+ const search = (search) => {
+ if (search === lastSearch)
+ return;
+ setLastSearch(search);
+ refresh(`/member/find/${source}?page=${page}&search=${search}&club=${clubFilter}`);
+ }
+
+ return <>
+
+
+
+
+ {data
+ ?
+ : error
+ ?
+ :
+ }
+
+
+
+ navigate("new")}>Ajouter un membre
+
+
+
+
+
+ >
+}
+
+function SearchBar({search}) {
+ const [searchInput, setSearchInput] = useState("");
+
+ const handelChange = (e) => {
+ setSearchInput(e.target.value);
+ }
+
+ const handleKeyDown = (event) => {
+ if (event.key === 'Enter') {
+ searchMember();
+ }
+ }
+
+ const searchMember = () => {
+ search(removeDiacritics(searchInput));
+ }
+
+ useEffect(() => {
+ const delayDebounceFn = setTimeout(() => {
+ searchMember();
+ }, 750)
+ return () => clearTimeout(delayDebounceFn)
+ }, [searchInput])
+
+ return
+}
+
+function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page}) {
+ const pages = []
+ for (let i = 1; i <= data.page_count; i++) {
+ pages.push(
+ navigate("#" + i)}>{i}
+ );
+ }
+
+ return <>
+
+
Ligne {((page - 1) * data.page_size) + 1} à {
+ (page * data.page_size > data.result_count) ? data.result_count : (page * data.page_size)} (page {page} sur {data.page_count})
+
+ {visibleMember.map(member => ())}
+
+
+
+
+
+
+ navigate("#" + (page - 1))}>«
+ {pages}
+ = data.page_count) ? " disabled" : "")}>
+ navigate("#" + (page + 1))}>»
+
+
+
+ >
+}
+
+function MakeRow({member, showLicenceState, navigate}) {
+ const rowContent = <>
+
+
{String(member.licence_number).padStart(5, '0')}
+
+
{member.fname} {member.lname}
+
+
+ {member.club?.name || "Sans club"}
+ >
+
+ if (showLicenceState && member.licence != null) {
+ return navigate("" + member.id)}>{rowContent}
+ } else {
+ return navigate("" + member.id)}>
+ {rowContent}
+
+ }
+}
+
+let allClub = []
+
+function FiltreBar({showLicenceState, setShowLicenceState, data, clubFilter, setClubFilter, source}) {
+ useEffect(() => {
+ if (!data)
+ return;
+ allClub.push(...data.result.map((e) => e.club?.name))
+ allClub = allClub.filter((value, index, self) => self.indexOf(value) === index).filter(value => value != null).sort()
+ }, [data]);
+
+ return
+
+
+
+ {source !== "club" &&
+
+ setClubFilter(event.target.value)}>
+ --- tout les clubs ---
+ {allClub && allClub.map((value, index) => {
+ return {value}
+ })
+ }
+
+
+ }
+
+}
+
+function Def() {
+ return
+
+
+
+
+
+
+}
\ No newline at end of file
diff --git a/src/main/webapp/src/pages/admin/AdminRoot.jsx b/src/main/webapp/src/pages/admin/AdminRoot.jsx
index 804f6fa..1ea1031 100644
--- a/src/main/webapp/src/pages/admin/AdminRoot.jsx
+++ b/src/main/webapp/src/pages/admin/AdminRoot.jsx
@@ -1,8 +1,8 @@
import {Outlet} from "react-router-dom";
import './AdminRoot.css'
import {LoadingProvider} from "../../hooks/useLoading.jsx";
-import {MemberList} from "./MemberList.jsx";
-import {MemberPage} from "./MemberPage.jsx";
+import {MemberList} from "../MemberList.jsx";
+import {MemberPage} from "./member/MemberPage.jsx";
export function AdminRoot() {
return <>
@@ -17,7 +17,7 @@ export function getAdminChildren () {
return [
{
path: 'member',
- element:
+ element:
},
{
path: 'member/:id',
diff --git a/src/main/webapp/src/pages/admin/MemberList.jsx b/src/main/webapp/src/pages/admin/MemberList.jsx
deleted file mode 100644
index 5f4ed16..0000000
--- a/src/main/webapp/src/pages/admin/MemberList.jsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import {useLoadingSwitcher} from "../../hooks/useLoading.jsx";
-import {useFetch} from "../../hooks/useFetch.js";
-import {AxiosError} from "../../components/AxiosError.jsx";
-import {ThreeDots} from "react-loader-spinner";
-import {useState} from "react";
-import {Input} from "../../components/Input.jsx";
-import {useNavigate} from "react-router-dom";
-
-const removeDiacritics = str => {
- return str
- .normalize('NFD')
- .replace(/[\u0300-\u036f]/g, '')
-}
-
-export function MemberList() {
- const setLoading = useLoadingSwitcher()
- const {data, error} = useFetch(`/member/all`, setLoading, 1)
- const [searchInput, setSearchInput] = useState("");
- const navigate = useNavigate();
-
- const visibleMember = data ? data.filter(member => {
- const lo = removeDiacritics(searchInput).toLowerCase()
- return !searchInput
- || (removeDiacritics(member.fname).toLowerCase().startsWith(lo)
- || removeDiacritics(member.lname).toLowerCase().startsWith(lo));
- }) : [];
-
- return <>
-
- {data
- ?
- {visibleMember.map(member => (
- navigate("/admin/member/" + member.id)}
- className="list-group-item list-group-item-action">{member.fname} {member.lname} ))}
-
- : error
- ?
- :
- }
- >
-}
-
-function SearchBar({searchInput, onSearchInputChange}) {
- return
-}
-
-function Def() {
- return
-
-
-
-
-
-
-}
\ No newline at end of file
diff --git a/src/main/webapp/src/pages/admin/MemberPage.jsx b/src/main/webapp/src/pages/admin/MemberPage.jsx
deleted file mode 100644
index c06da70..0000000
--- a/src/main/webapp/src/pages/admin/MemberPage.jsx
+++ /dev/null
@@ -1,411 +0,0 @@
-import {useNavigate, useParams} from "react-router-dom";
-import {LoadingProvider, useLoadingSwitcher} from "../../hooks/useLoading.jsx";
-import {useFetch} from "../../hooks/useFetch.js";
-import {AxiosError} from "../../components/AxiosError.jsx";
-import {ClubSelect} from "../../components/ClubSelect.jsx";
-import {useEffect, useState} from "react";
-import {apiAxios, getCategoryFormBirthDate} from "../../utils/Tools.js";
-import imageCompression from "browser-image-compression";
-import {ColoredCircle} from "../../components/ColoredCircle.jsx";
-import {toast} from "react-toastify";
-
-const vite_url = import.meta.env.VITE_URL;
-
-export function MemberPage() {
- const {id} = useParams()
- const navigate = useNavigate();
-
- const setLoading = useLoadingSwitcher()
- const {data, error} = useFetch(`/member/${id}`, setLoading, 1)
-
- return <>
- Page membre
- navigate("/admin/member")}>
- << retour
-
- {data
- ?
- : error &&
- }
- >
-}
-
-function PhotoCard({data}) {
- return
-
Licence n°{data.licence}
-
-
-
-
-
-
;
-}
-
-function InformationForm({data}) {
- const setLoading = useLoadingSwitcher()
- const handleSubmit = (event) => {
- event.preventDefault();
- setLoading(1)
-
- const formData = new FormData();
- formData.append("id", data.id);
- formData.append("lname", event.target.lname?.value);
- formData.append("fname", event.target.fname?.value);
- formData.append("categorie", event.target.category?.value);
- formData.append("club", event.target.club?.value);
- formData.append("genre", event.target.genre?.value);
- formData.append("country", event.target.country?.value);
- formData.append("birth_date", new Date(event.target.birth_date?.value).toUTCString());
- formData.append("email", event.target.email?.value);
- formData.append("role", event.target.role?.value);
- formData.append("grade_arbitrage", event.target.grade_arbitrage?.value);
-
- const send = (formData_) => {
- apiAxios.post(`/member/${data.id}`, formData_, {
- headers: {
- 'Accept': '*/*',
- 'Content-Type': 'multipart/form-data',
- }
- }).then(_ => {
- toast.success('Profile mis à jours avec succès 🎉');
- }).catch(e => {
- console.log(e.response)
- toast.error('Échec de la mise à jours du profile 😕 (code: ' + e.response.status + ')');
- }).finally(() => {
- if (setLoading)
- setLoading(0)
- })
- }
-
- const imageFile = event.target.url_photo.files[0];
- if (imageFile) {
- console.log(`originalFile size ${imageFile.size / 1024 / 1024} MB`);
-
- const options = {
- maxSizeMB: 1,
- maxWidthOrHeight: 1920,
- useWebWorker: true,
- }
-
- imageCompression(imageFile, options).then(compressedFile => {
- console.log(`compressedFile size ${compressedFile.size / 1024 / 1024} MB`); // smaller than maxSizeMB
- formData.append("photo_data", compressedFile)
- send(formData)
- });
- } else {
- send(formData)
- }
- }
-
- return ;
-}
-
-function PremForm({userData}) {
- const setLoading = useLoadingSwitcher()
- const handleSubmitPerm = (event) => {
- event.preventDefault();
- setLoading(1)
-
- const formData = new FormData();
- formData.append("federation_admin", event.target.federation_admin?.checked);
- formData.append("safca_user", event.target.safca_user?.checked);
- formData.append("safca_create_compet", event.target.safca_create_compet?.checked);
- formData.append("safca_super_admin", event.target.safca_super_admin?.checked);
-
- apiAxios.put(`/compte/${userData.userId}/roles`, formData, {
- headers: {
- 'Accept': '*/*',
- 'Content-Type': 'form-data',
- }
- }).then(_ => {
- toast.success('Permission mise à jours avec succès 🎉');
- }).catch(e => {
- console.log(e.response)
- toast.error('Échec de la mise à jours des permissions 😕 (code: ' + e.response.status + ')');
- }).finally(() => {
- if (setLoading)
- setLoading(0)
- })
- }
-
- return
-}
-
-function PremFormContent({userData}) {
- const setLoading = useLoadingSwitcher()
- const {data, error} = useFetch(`/compte/${userData.userId}/roles`, setLoading, 1)
-
- return <>
-
-
FFSAF intra
- {data
- ? <>
-
- >
- : error && }
-
-
-
SAFCA
- {data
- ? <>
-
-
-
- >
- : error && }
-
- >
-}
-
-function LicenceCard() {
- return ;
-}
-
-function SelectCard() {
- return
-
-
Sélection en équipe de France
-
-
-
;
-}
-
-function CompteInfo({userData}) {
-
- const creatAccount = () => {
- let err = {};
- toast.promise(
- apiAxios.put(`/compte/${userData.id}/init`).catch(e => {
- err = e
- }),
- {
- pending: 'Création du compte en cours',
- success: 'Compte créé avec succès 🎉',
- error: 'Échec de la création du compte 😕 (code: ' + err.response.status + ')'
- }
- )
- }
-
- return
-
Compte
-
- {userData.userId
- ?
- :
- <>
-
-
-
Ce membre ne dispose pas de compte...
-
-
-
-
- Initialiser le compte
-
-
- >
- }
-
-
-
-}
-
-function CompteInfoContent({
- userData
- }) {
- const setLoading = useLoadingSwitcher()
- const {data, error} = useFetch(`/compte/${userData.userId}`, setLoading, 1)
-
- return <>
- {data
- ? <>
-
-
-
Identifiant: {data.login}
-
-
-
-
- >
- : error &&
- } >
-}
-
-function BirthDayField({inti_date, inti_category}) {
- const [date, setDate] = useState(inti_date)
- const [category, setCategory] = useState(inti_category)
- const [canUpdate, setCanUpdate] = useState(false)
- useEffect(() => {
- const b = category !== getCategoryFormBirthDate(new Date(date), new Date('2023-09-01'))
- if (b !== canUpdate)
- setCanUpdate(b)
- }, [date, category])
-
- const updateCat = _ => {
- setCategory(getCategoryFormBirthDate(new Date(date), new Date('2023-09-01')))
- }
-
-
- return <>
-
- Date de naissance
- setDate(e.target.value)}/>
-
-
-
- Catégorie
-
- {canUpdate && Mettre à jours }
-
-
- >
-}
-
-function OptionField({name, text, values, value}) {
- return
-
- {text}
-
- {Object.keys(values).map((key, _) => {
- return ({values[key]} )
- })}
-
-
-
-}
-
-function TextField({name, text, value, placeholder, type = "text"}) {
- return
-}
-
-function CheckField({name, text, value, row = false}) {
- return <>{
- row ?
-
- :
-
- {text}
-
- }
- >
-}
\ No newline at end of file
diff --git a/src/main/webapp/src/pages/admin/member/CompteInfo.jsx b/src/main/webapp/src/pages/admin/member/CompteInfo.jsx
new file mode 100644
index 0000000..7d825ce
--- /dev/null
+++ b/src/main/webapp/src/pages/admin/member/CompteInfo.jsx
@@ -0,0 +1,120 @@
+import {toast} from "react-toastify";
+import {apiAxios} from "../../../utils/Tools.js";
+import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
+import {useFetch} from "../../../hooks/useFetch.js";
+import {ColoredCircle} from "../../../components/ColoredCircle.jsx";
+import {AxiosError} from "../../../components/AxiosError.jsx";
+
+export function CompteInfo({userData}) {
+
+ const creatAccount = () => {
+ let err = {};
+ toast.promise(
+ apiAxios.put(`/compte/${userData.id}/init`).catch(e => {
+ err = e
+ }),
+ {
+ pending: 'Création du compte en cours',
+ success: 'Compte créé avec succès 🎉',
+ error: 'Échec de la création du compte 😕 (code: ' + err.response.status + ')'
+ }
+ )
+ }
+ const sendId = (event) => {
+ event.preventDefault();
+
+ toast.promise(
+ apiAxios.put(`/compte/${userData.id}/setUUID/${event.target.uuid?.value}`),
+ {
+ pending: "Définition de l'identifient en cours",
+ success: "Identifient défini avec succès 🎉",
+ error: "Échec de la définition de l'identifient 😕 "
+ }
+ )
+ }
+
+ return
+
+
+
+ Compte
+
+
+
+ Définir l'id du compte
+
+
+
+
+
+
+ {userData.userId
+ ?
+ :
+ <>
+
+
+
Ce membre ne dispose pas de compte...
+
+
+
+
+ Initialiser le compte
+
+
+ >
+ }
+
+
+
+}
+
+function CompteInfoContent({userData}) {
+ const setLoading = useLoadingSwitcher()
+ const {data, error} = useFetch(`/compte/${userData.userId}`, setLoading, 1)
+
+ return <>
+ {data
+ ? <>
+
+
+
Identifiant: {data.login}
+
+
+
+
+ >
+ : error &&
+ } >
+}
diff --git a/src/main/webapp/src/pages/admin/member/InformationForm.jsx b/src/main/webapp/src/pages/admin/member/InformationForm.jsx
new file mode 100644
index 0000000..265decc
--- /dev/null
+++ b/src/main/webapp/src/pages/admin/member/InformationForm.jsx
@@ -0,0 +1,106 @@
+import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
+import {apiAxios} from "../../../utils/Tools.js";
+import {toast} from "react-toastify";
+import imageCompression from "browser-image-compression";
+import {BirthDayField, OptionField, TextField} from "../../../components/MemberCustomFiels.jsx";
+import {ClubSelect} from "../../../components/ClubSelect.jsx";
+
+export function InformationForm({data}) {
+ const setLoading = useLoadingSwitcher()
+ const handleSubmit = (event) => {
+ event.preventDefault();
+ setLoading(1)
+
+ const formData = new FormData();
+ formData.append("id", data.id);
+ formData.append("lname", event.target.lname?.value);
+ formData.append("fname", event.target.fname?.value);
+ formData.append("categorie", event.target.category?.value);
+ formData.append("club", event.target.club?.value);
+ formData.append("genre", event.target.genre?.value);
+ formData.append("country", event.target.country?.value);
+ formData.append("birth_date", new Date(event.target.birth_date?.value).toUTCString());
+ formData.append("email", event.target.email?.value);
+ formData.append("role", event.target.role?.value);
+ formData.append("grade_arbitrage", event.target.grade_arbitrage?.value);
+
+ const send = (formData_) => {
+ apiAxios.post(`/member/${data.id}`, formData_, {
+ headers: {
+ 'Accept': '*/*',
+ 'Content-Type': 'multipart/form-data',
+ }
+ }).then(_ => {
+ toast.success('Profile mis à jours avec succès 🎉');
+ }).catch(e => {
+ console.log(e.response)
+ toast.error('Échec de la mise à jours du profile 😕 (code: ' + e.response.status + ')');
+ }).finally(() => {
+ if (setLoading)
+ setLoading(0)
+ })
+ }
+
+ const imageFile = event.target.url_photo.files[0];
+ if (imageFile) {
+ console.log(`originalFile size ${imageFile.size / 1024 / 1024} MB`);
+
+ const options = {
+ maxSizeMB: 1,
+ maxWidthOrHeight: 1920,
+ useWebWorker: true,
+ }
+
+ imageCompression(imageFile, options).then(compressedFile => {
+ console.log(`compressedFile size ${compressedFile.size / 1024 / 1024} MB`); // smaller than maxSizeMB
+ formData.append("photo_data", compressedFile)
+ send(formData)
+ });
+ } else {
+ send(formData)
+ }
+ }
+
+ return ;
+}
\ No newline at end of file
diff --git a/src/main/webapp/src/pages/admin/member/LicenceCard.jsx b/src/main/webapp/src/pages/admin/member/LicenceCard.jsx
new file mode 100644
index 0000000..fd11ec3
--- /dev/null
+++ b/src/main/webapp/src/pages/admin/member/LicenceCard.jsx
@@ -0,0 +1,195 @@
+import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
+import {useFetch} from "../../../hooks/useFetch.js";
+import {useEffect, useReducer, useState} from "react";
+import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
+import {faPen} from "@fortawesome/free-solid-svg-icons";
+import {AxiosError} from "../../../components/AxiosError.jsx";
+import {CheckField, TextField} from "../../../components/MemberCustomFiels.jsx";
+import {apiAxios, getSaison} from "../../../utils/Tools.js";
+import {Input} from "../../../components/Input.jsx";
+import {toast} from "react-toastify";
+
+function licenceReducer(licences, action) {
+ switch (action.type) {
+ case 'ADD':
+ return [
+ ...licences,
+ action.payload
+ ]
+ case 'REMOVE':
+ return licences.filter(licence => licence.id !== action.payload)
+ case 'UPDATE_OR_ADD':
+ const index = licences.findIndex(licence => licence.id === action.payload.id)
+ if (index === -1) {
+ return [
+ ...licences,
+ action.payload
+ ]
+ } else {
+ licences[index] = action.payload
+ return [...licences]
+ }
+ case 'SORT':
+ return licences.sort((a, b) => b.saison - a.saison)
+ default:
+ throw new Error()
+ }
+}
+
+export function LicenceCard({userData}) {
+ const setLoading = useLoadingSwitcher()
+ const {data, error} = useFetch(`/licence/${userData.id}`, setLoading, 1)
+
+ const [modalLicence, setModal] = useState({id: -1, membre: userData.id})
+ const [licences, dispatch] = useReducer(licenceReducer, [])
+
+ useEffect(() => {
+ if (!data) return
+ for (const dataKey of data) {
+ dispatch({type: 'UPDATE_OR_ADD', payload: dataKey})
+ }
+ dispatch({type: 'SORT'})
+ }, [data]);
+
+ return
+
+
+
Licence
+
+ setModal({id: -1, membre: userData.id})}>Ajouter
+
+
+
+
+
+
+
+
;
+}
+
+function sendLicence(event, dispatch) {
+ event.preventDefault();
+
+ const formData = new FormData(event.target);
+ toast.promise(
+ apiAxios.post(`/licence/${formData.get('membre')}`, formData),
+ {
+ pending: "Enregistrement de la licence en cours",
+ success: "Licence enregistrée avec succès 🎉",
+ error: "Échec de l'enregistrement de la licence 😕"
+ }
+ ).then(data => {
+ dispatch({type: 'UPDATE_OR_ADD', payload: data.data})
+ dispatch({type: 'SORT'})
+ })
+
+}
+
+function removeLicence(id, dispatch) {
+ toast.promise(
+ apiAxios.delete(`/licence/${id}`),
+ {
+ pending: "Suppression de la licence en cours",
+ success: "Licence supprimée avec succès 🎉",
+ error: "Échec de la suppression de la licence 😕"
+ }
+ ).then(_ => {
+ dispatch({type: 'REMOVE', payload: id})
+ })
+}
+
+function ModalContent({licence, dispatch}) {
+ const [saison, setSaison] = useState(0)
+ const [certificate, setCertificate] = useState(false)
+ const [validate, setValidate] = useState(false)
+ const [isNew, setNew] = useState(true)
+ const setSeason = (event) => {
+ setSaison(Number(event.target.value))
+ }
+ const handleCertificateChange = (event) => {
+ setCertificate(event.target.value === 'true');
+ }
+ const handleValidateChange = (event) => {
+ setValidate(event.target.value === 'true');
+ }
+
+ useEffect(() => {
+ if (licence.id !== -1) {
+ setNew(false)
+ setSaison(licence.saison)
+ setCertificate(licence.certificate)
+ setValidate(licence.validate)
+ } else {
+ setNew(true)
+ setSaison(getSaison())
+ setCertificate(false)
+ setValidate(false)
+ }
+ }, [licence]);
+
+ return
+}
+
+function RadioGroupeOnOff({value, onChange, name, text}) {
+ return
+ {text}
+
+ Non
+
+ Oui
+
;
+}
\ No newline at end of file
diff --git a/src/main/webapp/src/pages/admin/member/MemberPage.jsx b/src/main/webapp/src/pages/admin/member/MemberPage.jsx
new file mode 100644
index 0000000..badac6f
--- /dev/null
+++ b/src/main/webapp/src/pages/admin/member/MemberPage.jsx
@@ -0,0 +1,72 @@
+import {useNavigate, useParams} from "react-router-dom";
+import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
+import {useFetch} from "../../../hooks/useFetch.js";
+import {AxiosError} from "../../../components/AxiosError.jsx";
+import {CompteInfo} from "./CompteInfo.jsx";
+import {PremForm} from "./PremForm.jsx";
+import {InformationForm} from "./InformationForm.jsx";
+import {LicenceCard} from "./LicenceCard.jsx";
+
+const vite_url = import.meta.env.VITE_URL;
+
+export function MemberPage() {
+ const {id} = useParams()
+ const navigate = useNavigate();
+
+ const setLoading = useLoadingSwitcher()
+ const {data, error} = useFetch(`/member/${id}`, setLoading, 1)
+
+ return <>
+ Page membre
+ navigate("/admin/member")}>
+ « retour
+
+ {data
+ ?
+ : error &&
+ }
+ >
+}
+
+function PhotoCard({data}) {
+ return
+
Licence n°{data.licence}
+
+
+
+
+
+
;
+}
+
+function SelectCard() {
+ return
+
Sélection en équipe de France
+
+
;
+}
diff --git a/src/main/webapp/src/pages/admin/member/PremForm.jsx b/src/main/webapp/src/pages/admin/member/PremForm.jsx
new file mode 100644
index 0000000..59525e9
--- /dev/null
+++ b/src/main/webapp/src/pages/admin/member/PremForm.jsx
@@ -0,0 +1,87 @@
+import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
+import {apiAxios} from "../../../utils/Tools.js";
+import {toast} from "react-toastify";
+import {useFetch} from "../../../hooks/useFetch.js";
+import {CheckField} from "../../../components/MemberCustomFiels.jsx";
+import {AxiosError} from "../../../components/AxiosError.jsx";
+
+export function PremForm({userData}) {
+ const setLoading = useLoadingSwitcher()
+ const handleSubmitPerm = (event) => {
+ event.preventDefault();
+ setLoading(1)
+
+ const formData = new FormData();
+ formData.append("federation_admin", event.target.federation_admin?.checked);
+ formData.append("safca_user", event.target.safca_user?.checked);
+ formData.append("safca_create_compet", event.target.safca_create_compet?.checked);
+ formData.append("safca_super_admin", event.target.safca_super_admin?.checked);
+
+ apiAxios.put(`/compte/${userData.userId}/roles`, formData, {
+ headers: {
+ 'Accept': '*/*',
+ 'Content-Type': 'form-data',
+ }
+ }).then(_ => {
+ toast.success('Permission mise à jours avec succès 🎉');
+ }).catch(e => {
+ console.log(e.response)
+ toast.error('Échec de la mise à jours des permissions 😕 (code: ' + e.response.status + ')');
+ }).finally(() => {
+ if (setLoading)
+ setLoading(0)
+ })
+ }
+
+ return
+}
+
+function PremFormContent({userData}) {
+ const setLoading = useLoadingSwitcher()
+ const {data, error} = useFetch(`/compte/${userData.userId}/roles`, setLoading, 1)
+
+ return <>
+
+
FFSAF intra
+ {data
+ ? <>
+
+ >
+ : error && }
+
+
+
SAFCA
+ {data
+ ? <>
+
+
+
+ >
+ : error && }
+
+ >
+}
\ No newline at end of file
diff --git a/src/main/webapp/src/pages/club/ClubRoot.jsx b/src/main/webapp/src/pages/club/ClubRoot.jsx
new file mode 100644
index 0000000..5ddc95a
--- /dev/null
+++ b/src/main/webapp/src/pages/club/ClubRoot.jsx
@@ -0,0 +1,43 @@
+import {Outlet} from "react-router-dom";
+import {LoadingProvider} from "../../hooks/useLoading.jsx";
+import {MemberPage} from "./member/MemberPage.jsx";
+import {useAuth} from "../../hooks/useAuth.jsx";
+import {MemberList} from "../MemberList.jsx";
+
+export function ClubRoot() {
+ const {userinfo} = useAuth()
+ let club = ""
+ if (userinfo?.groups) {
+ for (let group of userinfo.groups) {
+ if (group.startsWith("/club/")) {
+ club = group.slice(group.indexOf("-") + 1)
+ break
+ }
+ }
+ }
+
+ return <>
+
+
Espace club {club}
+
+
+
+ >
+}
+
+export function getClubChildren() {
+ return [
+ {
+ path: 'member',
+ element:
+ },
+ {
+ path: 'member/:id',
+ element:
+ },
+ {
+ path: 'b',
+ element: Club B
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/main/webapp/src/pages/club/member/CompteInfo.jsx b/src/main/webapp/src/pages/club/member/CompteInfo.jsx
new file mode 100644
index 0000000..e9128c9
--- /dev/null
+++ b/src/main/webapp/src/pages/club/member/CompteInfo.jsx
@@ -0,0 +1,55 @@
+import {toast} from "react-toastify";
+import {apiAxios} from "../../../utils/Tools.js";
+import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
+import {useFetch} from "../../../hooks/useFetch.js";
+import {ColoredCircle} from "../../../components/ColoredCircle.jsx";
+import {AxiosError} from "../../../components/AxiosError.jsx";
+
+export function CompteInfo({userData}) {
+
+ return
+
Compte
+
+ {userData.userId
+ ?
+ :
+ <>
+
+
+
Ce membre ne dispose pas de compte...
+ Un compte sera créé par la fédération lors de la validation de sa première licence
+
+
+
+ >
+ }
+
+
+}
+
+function CompteInfoContent({userData}) {
+ const setLoading = useLoadingSwitcher()
+ const {data, error} = useFetch(`/compte/${userData.userId}`, setLoading, 1)
+
+ return <>
+ {data
+ ? <>
+
+
+
Identifiant: {data.login}
+
+
+
+
+ >
+ : error &&
+ } >
+}
diff --git a/src/main/webapp/src/pages/club/member/InformationForm.jsx b/src/main/webapp/src/pages/club/member/InformationForm.jsx
new file mode 100644
index 0000000..4fd541c
--- /dev/null
+++ b/src/main/webapp/src/pages/club/member/InformationForm.jsx
@@ -0,0 +1,101 @@
+// noinspection DuplicatedCode
+
+import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
+import {apiAxios} from "../../../utils/Tools.js";
+import {toast} from "react-toastify";
+import imageCompression from "browser-image-compression";
+import {BirthDayField, OptionField, TextField} from "../../../components/MemberCustomFiels.jsx";
+import {ClubSelect} from "../../../components/ClubSelect.jsx";
+
+export function InformationForm({data}) {
+ const setLoading = useLoadingSwitcher()
+ const handleSubmit = (event) => {
+ event.preventDefault();
+ setLoading(1)
+
+ const formData = new FormData();
+ formData.append("id", data.id);
+ formData.append("lname", event.target.lname?.value);
+ formData.append("fname", event.target.fname?.value);
+ formData.append("categorie", event.target.category?.value);
+ formData.append("genre", event.target.genre?.value);
+ formData.append("country", event.target.country?.value);
+ formData.append("birth_date", new Date(event.target.birth_date?.value).toUTCString());
+ formData.append("email", event.target.email?.value);
+ formData.append("role", event.target.role?.value);
+
+ const send = (formData_) => {
+ apiAxios.post(`/member/club/${data.id}`, formData_, {
+ headers: {
+ 'Accept': '*/*',
+ 'Content-Type': 'multipart/form-data',
+ }
+ }).then(_ => {
+ toast.success('Profile mis à jours avec succès 🎉');
+ }).catch(e => {
+ console.log(e.response)
+ toast.error('Échec de la mise à jours du profile 😕 (code: ' + e.response.status + ')');
+ }).finally(() => {
+ if (setLoading)
+ setLoading(0)
+ })
+ }
+
+ const imageFile = event.target.url_photo.files[0];
+ if (imageFile) {
+ console.log(`originalFile size ${imageFile.size / 1024 / 1024} MB`);
+ const options = {
+ maxSizeMB: 1,
+ maxWidthOrHeight: 1920,
+ useWebWorker: true,
+ }
+ imageCompression(imageFile, options).then(compressedFile => {
+ console.log(`compressedFile size ${compressedFile.size / 1024 / 1024} MB`); // smaller than maxSizeMB
+ formData.append("photo_data", compressedFile)
+ send(formData)
+ });
+ } else {
+ send(formData)
+ }
+ }
+
+ return ;
+}
\ No newline at end of file
diff --git a/src/main/webapp/src/pages/club/member/LicenceCard.jsx b/src/main/webapp/src/pages/club/member/LicenceCard.jsx
new file mode 100644
index 0000000..48023ea
--- /dev/null
+++ b/src/main/webapp/src/pages/club/member/LicenceCard.jsx
@@ -0,0 +1,176 @@
+import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
+import {useFetch} from "../../../hooks/useFetch.js";
+import {useEffect, useReducer, useState} from "react";
+import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
+import {faInfo, faPen} from "@fortawesome/free-solid-svg-icons";
+import {AxiosError} from "../../../components/AxiosError.jsx";
+import {apiAxios, getSaison} from "../../../utils/Tools.js";
+import {toast} from "react-toastify";
+import {ColoredText} from "../../../components/ColoredCircle.jsx";
+
+function licenceReducer(licences, action) {
+ switch (action.type) {
+ case 'REMOVE':
+ return licences.filter(licence => licence.id !== action.payload)
+ case 'UPDATE_OR_ADD':
+ const index = licences.findIndex(licence => licence.id === action.payload.id)
+ if (index === -1) {
+ return [
+ ...licences,
+ action.payload
+ ]
+ } else {
+ licences[index] = action.payload
+ return [...licences]
+ }
+ case 'SORT':
+ return licences.sort((a, b) => b.saison - a.saison)
+ default:
+ throw new Error()
+ }
+}
+
+export function LicenceCard({userData}) {
+ const defaultLicence = {id: -1, membre: userData.id, validate: false, saison: getSaison(), certificate: false}
+
+ const setLoading = useLoadingSwitcher()
+ const {data, error} = useFetch(`/licence/${userData.id}`, setLoading, 1)
+ const [modalLicence, setModal] = useState(defaultLicence)
+ const [licences, dispatch] = useReducer(licenceReducer, [])
+
+ useEffect(() => {
+ if (!data) return
+ for (const dataKey of data) {
+ dispatch({type: 'UPDATE_OR_ADD', payload: dataKey})
+ }
+ dispatch({type: 'SORT'})
+ }, [data]);
+
+ return
+
+
+
Licence
+
+ setModal(defaultLicence)}
+ disabled={licences.some(licence => licence.saison === getSaison())}>Demander
+
+
+
+
+
+
+
+
;
+}
+
+function sendLicence(event, dispatch) {
+ event.preventDefault();
+
+ const formData = new FormData(event.target);
+ toast.promise(
+ apiAxios.post(`/licence/club/${formData.get('membre')}`, formData),
+ {
+ pending: "Enregistrement de la demande de licence en cours",
+ success: "Demande de licence enregistrée avec succès 🎉",
+ error: "Échec de la demande de licence 😕"
+ }
+ ).then(data => {
+ dispatch({type: 'UPDATE_OR_ADD', payload: data.data})
+ dispatch({type: 'SORT'})
+ })
+
+}
+
+function removeLicence(id, dispatch) {
+ toast.promise(
+ apiAxios.delete(`/licence/club/${id}`),
+ {
+ pending: "Suppression de la demande en cours",
+ success: "Demande supprimée avec succès 🎉",
+ error: "Échec de la suppression de la demande de licence 😕"
+ }
+ ).then(_ => {
+ dispatch({type: 'REMOVE', payload: id})
+ })
+}
+
+function ModalContent({licence, dispatch}) {
+ const [certificate, setCertificate] = useState(false)
+ const [isNew, setNew] = useState(true)
+
+ const handleCertificateChange = (event) => {
+ setCertificate(event.target.value === 'true');
+ }
+
+ useEffect(() => {
+ if (licence.id !== -1) {
+ setNew(false)
+ setCertificate(licence.certificate)
+ } else {
+ setNew(true)
+ setCertificate(false)
+ }
+ }, [licence]);
+
+ const currentSaison = licence.saison === getSaison();
+
+ return
+}
diff --git a/src/main/webapp/src/pages/club/member/MemberPage.jsx b/src/main/webapp/src/pages/club/member/MemberPage.jsx
new file mode 100644
index 0000000..622fd5d
--- /dev/null
+++ b/src/main/webapp/src/pages/club/member/MemberPage.jsx
@@ -0,0 +1,70 @@
+import {useNavigate, useParams} from "react-router-dom";
+import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
+import {useFetch} from "../../../hooks/useFetch.js";
+import {AxiosError} from "../../../components/AxiosError.jsx";
+import {CompteInfo} from "./CompteInfo.jsx";
+import {InformationForm} from "./InformationForm.jsx";
+import {LicenceCard} from "./LicenceCard.jsx";
+
+const vite_url = import.meta.env.VITE_URL;
+
+export function MemberPage() {
+ const {id} = useParams()
+ const navigate = useNavigate();
+
+ const setLoading = useLoadingSwitcher()
+ const {data, error} = useFetch(`/member/${id}`, setLoading, 1)
+
+ return <>
+ Page membre
+ navigate("/club/member")}>
+ « retour
+
+ {data
+ ?
+ : error &&
+ }
+ >
+}
+
+function PhotoCard({data}) {
+ return
+
Licence n°{data.licence}
+
+
+
+
+
+
;
+}
+
+function SelectCard() {
+ return
+
Sélection en équipe de France
+
+
;
+}