feat: add club member's gestion page
This commit is contained in:
@@ -10,6 +10,7 @@ import {ToastContainer} from "react-toastify";
|
||||
|
||||
import './App.css'
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import {ClubRoot, getClubChildren} from "./pages/club/ClubRoot.jsx";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@@ -25,6 +26,11 @@ const router = createBrowserRouter([
|
||||
path: 'admin',
|
||||
element: <AdminRoot/>,
|
||||
children: getAdminChildren()
|
||||
},
|
||||
{
|
||||
path: 'club',
|
||||
element: <ClubRoot/>,
|
||||
children: getClubChildren()
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -13,4 +13,16 @@ export const ColoredCircle = ({color, boolean}) => {
|
||||
return <Fragment>
|
||||
<span className="colored-circle" style={styles}/>
|
||||
</Fragment>
|
||||
};
|
||||
|
||||
export const ColoredText = ({boolean, text={true: "Oui", false: "Non"}}) => {
|
||||
const styles = {color: '#F00'};
|
||||
|
||||
if (boolean !== undefined) {
|
||||
styles.color = (boolean) ? '#00c700' : '#e50000';
|
||||
}
|
||||
|
||||
return <Fragment>
|
||||
<span className="font-weight-bold" style={styles}>{text[boolean]}</span>
|
||||
</Fragment>
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {getCategoryFormBirthDate} from "../../../utils/Tools.js";
|
||||
import {getCategoryFormBirthDate} from "../utils/Tools.js";
|
||||
|
||||
export function BirthDayField({inti_date, inti_category}) {
|
||||
const [date, setDate] = useState(inti_date)
|
||||
@@ -36,11 +36,11 @@ export function BirthDayField({inti_date, inti_category}) {
|
||||
</>
|
||||
}
|
||||
|
||||
export function OptionField({name, text, values, value}) {
|
||||
export function OptionField({name, text, values, value, disabled=false}) {
|
||||
return <div className="row">
|
||||
<div className="input-group mb-3">
|
||||
<label className="input-group-text" id={name}>{text}</label>
|
||||
<select className="form-select" id={name} name={name} defaultValue={value} required>
|
||||
<select className="form-select" id={name} name={name} defaultValue={value} required disabled={disabled}>
|
||||
{Object.keys(values).map((key, _) => {
|
||||
return (<option key={key} value={key}>{values[key]}</option>)
|
||||
})}
|
||||
@@ -22,6 +22,7 @@ export function Nav() {
|
||||
<div className="collapse-item">
|
||||
<ul className="navbar-nav">
|
||||
<li className="nav-item"><NavLink className="nav-link" to="/">Accueil</NavLink></li>
|
||||
<ClubMenu/>
|
||||
<AdminMenu/>
|
||||
<LoginMenu/>
|
||||
</ul>
|
||||
@@ -31,6 +32,24 @@ export function Nav() {
|
||||
</nav>
|
||||
}
|
||||
|
||||
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 <li className="nav-item dropdown">
|
||||
<div className="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Club
|
||||
</div>
|
||||
<ul className="dropdown-menu">
|
||||
<li className="nav-item"><NavLink className="nav-link" to="/club/member">Member</NavLink></li>
|
||||
<li className="nav-item"><NavLink className="nav-link" to="/club/b">B</NavLink></li>
|
||||
</ul>
|
||||
</li>
|
||||
}
|
||||
|
||||
function AdminMenu() {
|
||||
const {is_authenticated, userinfo} = useAuth()
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ 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 "./MemberCustomFiels.jsx";
|
||||
import {BirthDayField, OptionField, TextField} from "../../../components/MemberCustomFiels.jsx";
|
||||
import {ClubSelect} from "../../../components/ClubSelect.jsx";
|
||||
|
||||
export function InformationForm({data}) {
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 "./MemberCustomFiels.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";
|
||||
@@ -115,10 +115,9 @@ function removeLicence(id, dispatch) {
|
||||
success: "Licence supprimée avec succès 🎉",
|
||||
error: "Échec de la suppression de la licence 😕"
|
||||
}
|
||||
).then(data => {
|
||||
).then(_ => {
|
||||
dispatch({type: 'REMOVE', payload: id})
|
||||
})
|
||||
console.log(id)
|
||||
}
|
||||
|
||||
function ModalContent({licence, dispatch}) {
|
||||
|
||||
@@ -2,7 +2,7 @@ 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 "./MemberCustomFiels.jsx";
|
||||
import {CheckField} from "../../../components/MemberCustomFiels.jsx";
|
||||
import {AxiosError} from "../../../components/AxiosError.jsx";
|
||||
|
||||
export function PremForm({userData}) {
|
||||
@@ -66,7 +66,7 @@ function PremFormContent({userData}) {
|
||||
<h5>FFSAF intra</h5>
|
||||
{data
|
||||
? <>
|
||||
<CheckField name="federation_admin" text="Accès à l'intra"
|
||||
<CheckField name="federation_admin" text="Administrateur de la fédération"
|
||||
value={data.includes("federation_admin")}/>
|
||||
</>
|
||||
: error && <AxiosError error={error}/>}
|
||||
|
||||
43
src/main/webapp/src/pages/club/ClubRoot.jsx
Normal file
43
src/main/webapp/src/pages/club/ClubRoot.jsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import {Outlet} from "react-router-dom";
|
||||
import {LoadingProvider} from "../../hooks/useLoading.jsx";
|
||||
import {MemberList} from "./MemberList.jsx";
|
||||
import {MemberPage} from "./member/MemberPage.jsx";
|
||||
import {useAuth} from "../../hooks/useAuth.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 <>
|
||||
<div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap'}}>
|
||||
<h1>Espace club</h1><h3 style={{marginLeft: '0.75em'}}>{club}</h3></div>
|
||||
<LoadingProvider>
|
||||
<Outlet/>
|
||||
</LoadingProvider>
|
||||
</>
|
||||
}
|
||||
|
||||
export function getClubChildren() {
|
||||
return [
|
||||
{
|
||||
path: 'member',
|
||||
element: <MemberList/>
|
||||
},
|
||||
{
|
||||
path: 'member/:id',
|
||||
element: <MemberPage/>
|
||||
},
|
||||
{
|
||||
path: 'b',
|
||||
element: <div>Club B</div>
|
||||
}
|
||||
]
|
||||
}
|
||||
60
src/main/webapp/src/pages/club/MemberList.jsx
Normal file
60
src/main/webapp/src/pages/club/MemberList.jsx
Normal file
@@ -0,0 +1,60 @@
|
||||
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/club`, 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 <>
|
||||
<SearchBar searchInput={searchInput} onSearchInputChange={setSearchInput}/>
|
||||
{data
|
||||
? <div className="list-group">
|
||||
{visibleMember.map(member => (
|
||||
<span key={member.id}
|
||||
onClick={() => navigate("/club/member/" + member.id)}
|
||||
className="list-group-item list-group-item-action">{member.fname} {member.lname}</span>))}
|
||||
</div>
|
||||
: error
|
||||
? <AxiosError error={error}/>
|
||||
: <Def/>
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
function SearchBar({searchInput, onSearchInputChange}) {
|
||||
return <div>
|
||||
<div className="mb-3">
|
||||
<Input value={searchInput} onChange={onSearchInputChange} placeholder="Rechercher..."/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
function Def() {
|
||||
return <div className="list-group">
|
||||
<li className="list-group-item"><ThreeDots/></li>
|
||||
<li className="list-group-item"><ThreeDots/></li>
|
||||
<li className="list-group-item"><ThreeDots/></li>
|
||||
<li className="list-group-item"><ThreeDots/></li>
|
||||
<li className="list-group-item"><ThreeDots/></li>
|
||||
</div>
|
||||
}
|
||||
55
src/main/webapp/src/pages/club/member/CompteInfo.jsx
Normal file
55
src/main/webapp/src/pages/club/member/CompteInfo.jsx
Normal file
@@ -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 <div className="card mb-4">
|
||||
<div className="card-header">Compte</div>
|
||||
<div className="card-body text-center">
|
||||
{userData.userId
|
||||
? <CompteInfoContent userData={userData}/>
|
||||
:
|
||||
<>
|
||||
<div className="row">
|
||||
<div className="input-group mb-3">
|
||||
<div>Ce membre ne dispose pas de compte... <br/>
|
||||
Un compte sera créé par la fédération lors de la validation de sa première licence
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
function CompteInfoContent({userData}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/compte/${userData.userId}`, setLoading, 1)
|
||||
|
||||
return <>
|
||||
{data
|
||||
? <>
|
||||
<div className="row">
|
||||
<div className="input-group mb-3">
|
||||
<div>Identifiant: {data.login}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="input-group mb-3">
|
||||
<div>Activer: <ColoredCircle boolean={data.enabled}/></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="input-group mb-3">
|
||||
<div>Email vérifié: <ColoredCircle boolean={data.emailVerified}/></div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
: error && <AxiosError error={error}/>
|
||||
} </>
|
||||
}
|
||||
101
src/main/webapp/src/pages/club/member/InformationForm.jsx
Normal file
101
src/main/webapp/src/pages/club/member/InformationForm.jsx
Normal file
@@ -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 <form onSubmit={handleSubmit}>
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">Information</div>
|
||||
<div className="card-body">
|
||||
<TextField name="lname" text="Nom" value={data.lname}/>
|
||||
<TextField name="fname" text="Prénom" value={data.fname}/>
|
||||
<TextField name="email" text="Email" value={data.email} placeholder="name@example.com"
|
||||
type="email"/>
|
||||
<OptionField name="genre" text="Genre" value={data.genre}
|
||||
values={{NA: 'N/A', H: 'H', F: 'F'}}/>
|
||||
<OptionField name="country" text="Pays" value={data.country}
|
||||
values={{NA: 'Sélectionner...', fr: 'FR', es: 'ES', be: 'BE'}}/>
|
||||
<BirthDayField inti_date={data.birth_date ? data.birth_date.split('T')[0] : ''}
|
||||
inti_category={data.categorie}/>
|
||||
<OptionField name="role" text="Rôle" value={data.role}
|
||||
values={{
|
||||
MEMBRE: 'Membre',
|
||||
PRESIDENT: 'Président',
|
||||
TRESORIER: 'Trésorier',
|
||||
SECRETAIRE: 'Secrétaire'
|
||||
}}/>
|
||||
<OptionField name="grade_arbitrage" text="Grade d'arbitrage" value={data.grade_arbitrage}
|
||||
values={{NA: 'N/A', ASSESSEUR: 'Assesseur', ARBITRE: 'Arbitre'}} disabled={true}/>
|
||||
<div className="row">
|
||||
<div className="input-group mb-3">
|
||||
<label className="input-group-text" htmlFor="url_photo">Photos
|
||||
(optionnelle)</label>
|
||||
<input type="file" className="form-control" id="url_photo" name="url_photo"
|
||||
accept=".jpg,.jpeg,.gif,.png,.svg"/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<button type="submit" className="btn btn-primary">Enregistrer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>;
|
||||
}
|
||||
176
src/main/webapp/src/pages/club/member/LicenceCard.jsx
Normal file
176
src/main/webapp/src/pages/club/member/LicenceCard.jsx
Normal file
@@ -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 <div className="card mb-4 mb-md-0">
|
||||
<div className="card-header container-fluid">
|
||||
<div className="row">
|
||||
<div className="col">Licence</div>
|
||||
<div className="col" style={{textAlign: 'right'}}>
|
||||
<button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#LicenceModal"
|
||||
onClick={() => setModal(defaultLicence)}
|
||||
disabled={licences.some(licence => licence.saison === getSaison())}>Demander
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<ul className="list-group">
|
||||
{licences.map((licence, index) => {
|
||||
return <div key={index}
|
||||
className={"list-group-item d-flex justify-content-between align-items-start list-group-item-" +
|
||||
(licence.validate ? "success" : (licence.certificate ? "warning" : "danger"))}>
|
||||
<div className="me-auto">{licence?.saison}-{licence?.saison + 1}</div>
|
||||
<button className="badge btn btn-primary rounded-pill" data-bs-toggle="modal"
|
||||
data-bs-target="#LicenceModal" onClick={_ => setModal(licence)}>
|
||||
{licence.saison === getSaison() ? <FontAwesomeIcon icon={faPen}/> :
|
||||
<FontAwesomeIcon icon={faInfo}/>}</button>
|
||||
</div>
|
||||
})}
|
||||
{error && <AxiosError error={error}/>}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="modal fade" id="LicenceModal" tabIndex="-1" aria-labelledby="LicenceModalLabel"
|
||||
aria-hidden="true">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<ModalContent licence={modalLicence} dispatch={dispatch}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
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 <form onSubmit={e => sendLicence(e, dispatch)}>
|
||||
<input name="id" value={licence.id} readOnly hidden/>
|
||||
<input name="membre" value={licence.membre} readOnly hidden/>
|
||||
<div className="modal-header">
|
||||
<h1 className="modal-title fs-5" id="LicenceModalLabel">
|
||||
{isNew ? "Demande de licence " : "Edition de la demande "}
|
||||
(saison {licence.saison}-{licence.saison + 1})</h1>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="btn-group input-group mb-3 justify-content-md-center" role="group"
|
||||
aria-label="Basic radio toggle button group">
|
||||
<span className="input-group-text">Certificat médical</span>
|
||||
<input type="radio" className="btn-check" id="btnradio1" autoComplete="off" value="false"
|
||||
checked={certificate === false} onChange={handleCertificateChange} disabled={!currentSaison}/>
|
||||
<label className="btn btn-outline-primary" htmlFor="btnradio1">Non</label>
|
||||
<input type="radio" className="btn-check" name="certificate" id="btnradio2" autoComplete="off"
|
||||
value="true" checked={certificate === true} onChange={handleCertificateChange}
|
||||
disabled={!currentSaison}/>
|
||||
<label className="btn btn-outline-primary" htmlFor="btnradio2">Oui</label>
|
||||
</div>
|
||||
|
||||
<div className="input-group mb-3 justify-content-md-center">
|
||||
<div>Validation de la licence: <ColoredText boolean={licence.validate}/></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
{currentSaison &&
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Enregistrer</button>}
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
|
||||
{currentSaison && licence.validate === false &&
|
||||
<button type="button" className="btn btn-danger" data-bs-dismiss="modal"
|
||||
onClick={() => removeLicence(licence.id, dispatch)}>Annuler</button>}
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
70
src/main/webapp/src/pages/club/member/MemberPage.jsx
Normal file
70
src/main/webapp/src/pages/club/member/MemberPage.jsx
Normal file
@@ -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 <>
|
||||
<h2>Page membre</h2>
|
||||
<button type="button" className="btn btn-link" onClick={() => navigate("/club/member")}>
|
||||
<< retour
|
||||
</button>
|
||||
{data
|
||||
? <div>
|
||||
<div className="row">
|
||||
<div className="col-lg-4">
|
||||
<PhotoCard data={data}/>
|
||||
<LoadingProvider><CompteInfo userData={data}/></LoadingProvider>
|
||||
</div>
|
||||
<div className="col-lg-8">
|
||||
<InformationForm data={data}/>
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
<LoadingProvider><LicenceCard userData={data}/></LoadingProvider>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<LoadingProvider><SelectCard/></LoadingProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
: error && <AxiosError error={error}/>
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
function PhotoCard({data}) {
|
||||
return <div className="card mb-4">
|
||||
<div className="card-header">Licence n°{data.licence}</div>
|
||||
<div className="card-body text-center">
|
||||
<div className="input-group mb-3">
|
||||
<img
|
||||
src={`${vite_url}/api/member/${data.id}/photo`}
|
||||
alt="avatar"
|
||||
className="rounded-circle img-fluid" style={{object_fit: 'contain'}}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function SelectCard() {
|
||||
return <div className="card mb-4 mb-md-0">
|
||||
<div className="card-header">Sélection en équipe de France</div>
|
||||
<div className="card-body">
|
||||
<p className="mb-1">Soon</p>
|
||||
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
Reference in New Issue
Block a user