Some checks failed
Deploy Production Server / if_merged (pull_request) Failing after 2m31s
672 lines
29 KiB
JavaScript
672 lines
29 KiB
JavaScript
import React, {useEffect, useRef, useState} from "react";
|
|
import {useRequestWS, useWS} from "../../../hooks/useWS.jsx";
|
|
import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
|
import {toast} from "react-toastify";
|
|
import {build_tree, resize_tree} from "../../../utils/TreeUtils.js"
|
|
import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx";
|
|
import {CategoryContent} from "./CategoryAdminContent.jsx";
|
|
import {exportOBSConfiguration} from "../../../hooks/useOBS.jsx";
|
|
import {createPortal} from "react-dom";
|
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
|
import {SimpleIconsOBS} from "../../../assets/SimpleIconsOBS.ts";
|
|
import JSZip from "jszip";
|
|
import {detectOptimalBackground} from "../../../components/SmartLogoBackground.jsx";
|
|
import {faGlobe} from "@fortawesome/free-solid-svg-icons";
|
|
|
|
const vite_url = import.meta.env.VITE_URL;
|
|
|
|
export function CMAdmin({compUuid}) {
|
|
const [catId, setCatId] = useState(null);
|
|
const [cat, setCat] = useState(null);
|
|
const menuActions = useRef({});
|
|
const {dispatch} = useWS();
|
|
|
|
useEffect(() => {
|
|
const categoryListener = ({data}) => {
|
|
if (!cat || data.id !== cat.id)
|
|
return
|
|
setCat(cat_ => ({
|
|
...cat_,
|
|
name: data.name,
|
|
liceName: data.liceName,
|
|
type: data.type
|
|
}))
|
|
}
|
|
dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}})
|
|
return () => dispatch({type: 'removeListener', payload: categoryListener})
|
|
}, [cat]);
|
|
|
|
return <>
|
|
<div className="card">
|
|
<div className='card-header'>
|
|
<CategoryHeader cat={cat} setCatId={setCatId}/>
|
|
</div>
|
|
|
|
<div className="card-body">
|
|
<LoadingProvider>
|
|
<div className="row">
|
|
<CategoryContent cat={cat} catId={catId} setCat={setCat} menuActions={menuActions}/>
|
|
</div>
|
|
</LoadingProvider>
|
|
</div>
|
|
<Menu menuActions={menuActions} compUuid={compUuid}/>
|
|
</div>
|
|
</>
|
|
}
|
|
|
|
let tto = [];
|
|
|
|
function resizeImageWithOptimalBackground(blob) {
|
|
return new Promise(async (resolve) => {
|
|
const background = await detectOptimalBackground(blob);
|
|
const imgUrl = URL.createObjectURL(blob);
|
|
const img = new Image();
|
|
img.crossOrigin = "anonymous";
|
|
img.onload = () => {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = 1080;
|
|
canvas.height = 1080;
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Dessiner l'image centrée
|
|
const scale = Math.min(1080 / img.width, 1080 / img.height);
|
|
const newWidth = img.width * scale;
|
|
const newHeight = img.height * scale;
|
|
const x = (1080 - newWidth) / 2;
|
|
const y = (1080 - newHeight) / 2;
|
|
|
|
ctx.filter = `drop-shadow(0 0 2rem ${background})`;
|
|
ctx.drawImage(img, x, y, newWidth, newHeight);
|
|
|
|
// Exporter en PNG
|
|
canvas.toBlob((newBlob) => {
|
|
resolve(newBlob);
|
|
URL.revokeObjectURL(imgUrl);
|
|
}, 'image/png', 1.0);
|
|
};
|
|
img.onerror = () => resolve(blob); // Retourne l'original en cas d'erreur
|
|
img.src = imgUrl;
|
|
});
|
|
}
|
|
|
|
async function downloadResourcesAsZip(resourceList) {
|
|
const zip = new JSZip();
|
|
const modal = new bootstrap.Modal(document.getElementById('progressModal'));
|
|
const progressBar = document.getElementById('progressBar');
|
|
const progressText = document.getElementById('progressText');
|
|
let completed = 0;
|
|
|
|
if (!resourceList.some(d => d.url === '/obs_template.json'))
|
|
resourceList.push({url: '/obs_template.json', name: 'saf_obs_template.json'});
|
|
|
|
// Afficher la modale
|
|
modal.show();
|
|
|
|
// Fonction pour télécharger une ressource et l'ajouter au ZIP
|
|
const addResourceToZip = async (data) => {
|
|
try {
|
|
const response = await fetch(data.url);
|
|
if (!response.ok) {
|
|
if (response.status === 404) {
|
|
return {success: false, filename: data.name || data.url.split('/').pop()};
|
|
}
|
|
// noinspection ExceptionCaughtLocallyJS
|
|
throw new Error(`Erreur HTTP: ${response.status}`);
|
|
}
|
|
const blob = await response.blob();
|
|
const filename = data.name || data.url.split('/').pop();
|
|
const format = filename.split('.').pop().toLowerCase();
|
|
|
|
if (['png', 'jpg', 'jpeg', 'svg'].includes(format)) {
|
|
const resizedBlob = await resizeImageWithOptimalBackground(blob);
|
|
const pngFilename = filename.replace(/\.[^/.]+$/, ".png");
|
|
zip.file(pngFilename, resizedBlob);
|
|
return {success: true, pngFilename};
|
|
} else {
|
|
zip.file(filename, blob);
|
|
return {success: true, filename};
|
|
}
|
|
} catch (error) {
|
|
console.error(`Impossible d'ajouter ${data.url} au ZIP:`, error);
|
|
return {success: false, filename: data.name || data.url.split('/').pop()};
|
|
}
|
|
};
|
|
|
|
// Télécharger toutes les ressources et mettre à jour la progression
|
|
await Promise.all(
|
|
resourceList.map(async (data) => {
|
|
const result = await addResourceToZip(data);
|
|
completed++;
|
|
const progress = Math.round((completed / resourceList.length) * 100);
|
|
progressBar.style.width = `${progress}%`;
|
|
progressText.textContent = `Téléchargement (${completed}/${resourceList.length}) : ${result.filename}`;
|
|
return result;
|
|
})
|
|
);
|
|
|
|
// Générer le ZIP et déclencher le téléchargement
|
|
const zipBlob = await zip.generateAsync({type: 'blob'});
|
|
const zipUrl = URL.createObjectURL(zipBlob);
|
|
const a = document.createElement('a');
|
|
a.href = zipUrl;
|
|
a.download = 'ressources.zip';
|
|
a.click();
|
|
URL.revokeObjectURL(zipUrl);
|
|
|
|
// Fermer la modale
|
|
modal.hide();
|
|
progressText.textContent = "Téléchargement terminé !";
|
|
}
|
|
|
|
function Menu({menuActions, compUuid}) {
|
|
const e = document.getElementById("actionMenu")
|
|
const longPress = useRef({time: null, timer: null, button: null});
|
|
const obsModal = useRef(null);
|
|
|
|
for (const x of tto)
|
|
x.dispose();
|
|
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip2"]')
|
|
tto = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
|
|
|
|
const longTimeAction = (button) => {
|
|
if (button === "obs") {
|
|
obsModal.current.click();
|
|
}
|
|
}
|
|
|
|
const longPressDown = (button) => {
|
|
longPress.current.button = button;
|
|
longPress.current.time = new Date();
|
|
longPress.current.timer = setTimeout(() => {
|
|
longTimeAction(button);
|
|
|
|
longPress.current.time = null;
|
|
longPress.current.button = null;
|
|
}, 1000);
|
|
}
|
|
|
|
const longPressUp = (button) => {
|
|
clearTimeout(longPress.current.timer);
|
|
|
|
if (longPress.current.time) {
|
|
const diff = new Date() - longPress.current.time;
|
|
if (longPress.current.button === button) {
|
|
if (diff >= 1000) {
|
|
longTimeAction(button);
|
|
} else {
|
|
if (button === "obs") {
|
|
downloadResourcesAsZip(menuActions.current.resourceList || [])
|
|
.then(__ => console.log("Ressources téléchargées"));
|
|
}
|
|
}
|
|
}
|
|
|
|
longPress.current.time = null;
|
|
longPress.current.button = null;
|
|
}
|
|
}
|
|
|
|
const handleOBSSubmit = (e) => {
|
|
e.preventDefault();
|
|
const form = e.target;
|
|
const adresse = form[0].value;
|
|
const password = form[1].value;
|
|
const assets_dir = form[2].value;
|
|
|
|
exportOBSConfiguration(adresse, password, assets_dir)
|
|
}
|
|
|
|
const copyScriptToClipboard = () => {
|
|
navigator.clipboard.writeText(`<div id='safca_api_data'></div>
|
|
<script id="safca_api_script" type="text/javascript" src="${vite_url}/competition.js?id=${compUuid}"></script>`
|
|
).then(() => {
|
|
toast.success("Texte copié dans le presse-papier ! Collez-le dans une balise HTML sur votre WordPress.");
|
|
}).catch(err => {
|
|
toast.error("Erreur lors de la copie dans le presse-papier : " + err);
|
|
});
|
|
}
|
|
|
|
if (!e)
|
|
return <></>;
|
|
return <>
|
|
{createPortal(
|
|
<>
|
|
<div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div>
|
|
<FontAwesomeIcon icon={SimpleIconsOBS} size="xl"
|
|
style={{color: "#6c757d", cursor: "pointer", marginRight: "0.25em"}}
|
|
onMouseDown={() => longPressDown("obs")}
|
|
onMouseUp={() => longPressUp("obs")}
|
|
data-bs-toggle="tooltip2" data-bs-placement="top"
|
|
data-bs-title="Clique court : Télécharger les ressources. Clique long : Créer la configuration obs"/>
|
|
<FontAwesomeIcon icon={faGlobe} size="xl"
|
|
style={{color: "#6c757d", cursor: "pointer"}}
|
|
onClick={() => copyScriptToClipboard()}
|
|
data-bs-toggle="tooltip2" data-bs-placement="top"
|
|
data-bs-title="Copier le scripte d'intégration"/>
|
|
</>, document.getElementById("actionMenu"))}
|
|
|
|
<button ref={obsModal} type="button" className="btn btn-link" data-bs-toggle="modal" data-bs-target="#OBSModal" style={{display: 'none'}}>
|
|
Launch OBS Modal
|
|
</button>
|
|
<div className="modal fade" id="OBSModal" tabIndex="-1" aria-labelledby="OBSModalLabel" aria-hidden="true">
|
|
<div className="modal-dialog">
|
|
<div className="modal-content">
|
|
<div className="modal-header">
|
|
<h5 className="modal-title">Configuration OBS</h5>
|
|
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<form onSubmit={handleOBSSubmit}>
|
|
<div className="modal-body">
|
|
<strong>/!\ Le mot de passe va être stoker en claire, il est recommandé de ne l'utiliser que sur obs websocket et d'en
|
|
changer entre chaque compétition</strong>
|
|
<div className="input-group mb-3">
|
|
<span className="input-group-text">Adresse du serveur</span>
|
|
<span className="input-group-text">ws://</span>
|
|
<input type="text" className="form-control" placeholder="127.0.0.1:4455" aria-label=""
|
|
defaultValue={"127.0.0.1:4455"}/>
|
|
<span className="input-group-text">/</span>
|
|
</div>
|
|
<div className="input-group mb-3">
|
|
<span className="input-group-text">Mot de passe du serveur</span>
|
|
<input type="password" className="form-control" placeholder="12345" aria-label=""
|
|
defaultValue={""}/>
|
|
</div>
|
|
<div className="input-group mb-3">
|
|
<span className="input-group-text">Dossier des resources</span>
|
|
<input type="text" className="form-control" placeholder="" aria-label="" required/>
|
|
</div>
|
|
</div>
|
|
<div className="modal-footer">
|
|
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
|
|
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Exporter</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="modal fade" id="progressModal" tabIndex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
|
<div className="modal-dialog">
|
|
<div className="modal-content">
|
|
<div className="modal-header">
|
|
<h5 className="modal-title">Téléchargement en cours...</h5>
|
|
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div className="modal-body">
|
|
<div className="progress">
|
|
<div id="progressBar" className="progress-bar" role="progressbar" style={{width: "0%"}}></div>
|
|
</div>
|
|
<div id="progressText" className="mt-2">Préparation...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
}
|
|
|
|
function CategoryHeader({cat, setCatId}) {
|
|
const setLoading = useLoadingSwitcher()
|
|
const bthRef = useRef();
|
|
const confirmRef = useRef();
|
|
const [modal, setModal] = useState({})
|
|
const [confirm, setConfirm] = useState({})
|
|
|
|
const {data: cats, setData: setCats} = useRequestWS('getAllCategory', {}, setLoading);
|
|
const {dispatch} = useWS();
|
|
|
|
useEffect(() => {
|
|
const categoryListener = ({data}) => {
|
|
setCats([
|
|
...cats.filter(c => c.id !== data.id),
|
|
data
|
|
])
|
|
}
|
|
const sendAddCategory = ({data}) => {
|
|
setCats([...cats, data])
|
|
}
|
|
const sendDelCategory = ({data}) => {
|
|
setCatId(catId => {
|
|
if (catId === data) return null;
|
|
return catId;
|
|
})
|
|
setCats([...cats.filter(c => c.id !== data)])
|
|
}
|
|
dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}})
|
|
dispatch({type: 'addListener', payload: {callback: sendAddCategory, code: 'sendAddCategory'}})
|
|
dispatch({type: 'addListener', payload: {callback: sendDelCategory, code: 'sendDelCategory'}})
|
|
return () => {
|
|
dispatch({type: 'removeListener', payload: categoryListener})
|
|
dispatch({type: 'removeListener', payload: sendAddCategory})
|
|
dispatch({type: 'removeListener', payload: sendDelCategory})
|
|
}
|
|
}, [cats]);
|
|
|
|
useEffect(() => {
|
|
if (cats && cats.length > 0 && !cat || (cats && !cats.find(c => c.id === cat.id))) {
|
|
setCatId(cats.sort((a, b) => a.name.localeCompare(b.name))[0].id);
|
|
} else if (cats && cats.length === 0) {
|
|
setModal({});
|
|
bthRef.current.click();
|
|
}
|
|
}, [cats, cat]);
|
|
|
|
const handleCatChange = (e) => {
|
|
const selectedCatId = e.target.value;
|
|
if (selectedCatId !== "-1") {
|
|
setCatId(selectedCatId);
|
|
} else { // New category
|
|
setModal({});
|
|
bthRef.current.click();
|
|
e.target.value = cat?.id;
|
|
}
|
|
}
|
|
|
|
return <div className="row">
|
|
<div className="col-auto">
|
|
<div className="input-group">
|
|
<h5 style={{margin: "auto 0.5em auto 0"}}>Edition de la catégorie</h5>
|
|
<select className="form-select" onChange={handleCatChange} value={cat?.id || ""}>
|
|
{cats && cats.sort((a, b) => a.name.localeCompare(b.name)).map(c => (
|
|
<option key={c.id} value={c.id}>{c.name}</option>))}
|
|
{cats && <option value={-1}>Nouvelle...</option>}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div className="col" style={{margin: "auto 0", textAlign: "center"}}>
|
|
{cat &&
|
|
<div>Type: {(cat.type & 1) !== 0 ? "Poule" : ""}{cat.type === 3 ? " & " : ""}{(cat.type & 2) !== 0 ? "Tournois" : ""} |
|
|
Zone: {cat.liceName}</div>}
|
|
</div>
|
|
<div className="col-auto">
|
|
<button className="btn btn-primary float-end" onClick={() => {
|
|
setModal(cat);
|
|
bthRef.current.click();
|
|
}} disabled={cat === null}>Modifier
|
|
</button>
|
|
</div>
|
|
|
|
|
|
<button ref={bthRef} data-bs-toggle="modal" data-bs-target="#CategorieModal" style={{display: "none"}}>open</button>
|
|
<div className="modal fade" id="CategorieModal" tabIndex="-1" aria-labelledby="CategorieModalLabel"
|
|
aria-hidden="true">
|
|
<div className="modal-dialog">
|
|
<div className="modal-content">
|
|
<ModalContent state={modal} setCatId={setCatId} setConfirm={setConfirm} confirmRef={confirmRef}/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button ref={confirmRef} data-bs-toggle="modal" data-bs-target="#confirm-dialog" style={{display: "none"}}>open</button>
|
|
<ConfirmDialog id="confirm-dialog" onConfirm={confirm.confirm ? confirm.confirm : () => {
|
|
}} onCancel={confirm.cancel ? confirm.cancel : () => {
|
|
}} title={confirm ? confirm.title : ""} message={confirm ? confirm.message : ""}/>
|
|
</div>
|
|
}
|
|
|
|
function ModalContent({state, setCatId, setConfirm, confirmRef}) {
|
|
const [name, setName] = useState("")
|
|
const [lice, setLice] = useState("A")
|
|
const [poule, setPoule] = useState(true)
|
|
const [tournoi, setTournoi] = useState(false)
|
|
const [size, setSize] = useState(4)
|
|
const [loserMatch, setLoserMatch] = useState(1)
|
|
|
|
const {sendRequest} = useWS();
|
|
|
|
useEffect(() => {
|
|
setName(state.name || "");
|
|
setLice(state.liceName || "A");
|
|
setPoule(((state.type || 1) & 1) !== 0);
|
|
setTournoi((state.type & 2) !== 0);
|
|
|
|
if (state?.trees && state.trees.length >= 1) {
|
|
const tree = state.trees[0];
|
|
setSize(tree.getMaxChildrenAtDepth(tree.death() - 1) * 2);
|
|
|
|
if (state.trees.length === 1) {
|
|
setLoserMatch(0);
|
|
} else if (state.trees.length === 2) {
|
|
setLoserMatch(1);
|
|
} else {
|
|
setLoserMatch(-1);
|
|
}
|
|
} else {
|
|
setSize(4);
|
|
setLoserMatch(1);
|
|
}
|
|
}, [state])
|
|
|
|
const handleSubmit = (e) => {
|
|
e.preventDefault();
|
|
|
|
const regex = /^([^;]+;)*[^;]+$/;
|
|
if (regex.test(lice.trim()) === false) {
|
|
toast.error("Le format du nom des zones de combat est invalide. Veuillez séparer les noms par des ';'.");
|
|
return;
|
|
}
|
|
|
|
const nType = (poule ? 1 : 0) + (tournoi ? 2 : 0);
|
|
if (nType === 0) {
|
|
toast.error("Au moins un type (poule ou tournoi) doit être sélectionné.");
|
|
return;
|
|
}
|
|
|
|
if (state?.id) {
|
|
const applyChanges = () => {
|
|
const newData = {
|
|
id: state.id,
|
|
name: name.trim(),
|
|
liceName: lice.trim(),
|
|
type: nType
|
|
}
|
|
|
|
let nbMatch = -1;
|
|
let oldSubTree = -1;
|
|
const oldTrees = state?.trees || [];
|
|
if (oldTrees.length >= 1) {
|
|
const tree = state.trees[0];
|
|
nbMatch = tree.getMaxChildrenAtDepth(tree.death() - 1);
|
|
if (state.trees.length === 1)
|
|
oldSubTree = 0
|
|
else if (state.trees.length === 2)
|
|
oldSubTree = 1
|
|
}
|
|
|
|
console.log(tournoi, size, nbMatch, loserMatch, oldSubTree);
|
|
if (tournoi && (size !== nbMatch * 2 || loserMatch !== oldSubTree)) {
|
|
setConfirm({
|
|
title: "Changement de l'arbre du tournoi",
|
|
message: `Voulez-vous vraiment changer la taille de l'arbre du tournoi ou les matchs pour les perdants ? Cela va modifier les matchs existants (incluant des possibles suppressions)!`,
|
|
confirm: () => {
|
|
const trees2 = build_tree(size, loserMatch)
|
|
const newTrees = []
|
|
|
|
let i = 0;
|
|
for (; i < oldTrees.length && i < trees2.length; i++) {
|
|
newTrees.push(oldTrees[i]);
|
|
resize_tree(newTrees.at(i), trees2.at(i));
|
|
}
|
|
|
|
for (; i < trees2.length; i++) {
|
|
newTrees.push(trees2.at(i));
|
|
}
|
|
|
|
toast.promise(sendRequest('updateTrees', {categoryId: state.id, trees: newTrees}),
|
|
{
|
|
pending: 'Mise à jour des arbres du tournoi...',
|
|
success: 'Arbres mis à jour !',
|
|
error: 'Erreur lors de la mise à jour des arbres'
|
|
}
|
|
).then(__ => {
|
|
toast.promise(sendRequest('updateCategory', newData),
|
|
{
|
|
pending: 'Mise à jour de la catégorie...',
|
|
success: 'Catégorie mise à jour !',
|
|
error: 'Erreur lors de la mise à jour de la catégorie'
|
|
}
|
|
)
|
|
})
|
|
}
|
|
})
|
|
confirmRef.current.click();
|
|
} else {
|
|
toast.promise(sendRequest('updateCategory', newData),
|
|
{
|
|
pending: 'Mise à jour de la catégorie...',
|
|
success: 'Catégorie mise à jour !',
|
|
error: 'Erreur lors de la mise à jour de la catégorie'
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
if (nType !== state.type) {
|
|
let typeStr = "";
|
|
if ((state.type & 1) !== 0 && (nType & 1) === 0)
|
|
typeStr += "poule ";
|
|
if ((state.type & 2) !== 0 && (nType & 2) === 0)
|
|
typeStr += "tournoi ";
|
|
|
|
setConfirm({
|
|
title: "Changement de type de catégorie",
|
|
message: `Voulez-vous vraiment enlever la partie ${typeStr} de la catégorie. Cela va supprimer les matchs contenus dans cette partie !`,
|
|
confirm: () => {
|
|
setTimeout(() =>
|
|
applyChanges(), 500);
|
|
}
|
|
})
|
|
confirmRef.current.click();
|
|
} else {
|
|
applyChanges();
|
|
}
|
|
} else {
|
|
toast.promise(sendRequest('createCategory', {name: name.trim(), liceName: lice.trim(), type: nType}),
|
|
{
|
|
pending: 'Création de la catégorie...',
|
|
success: 'Catégorie créée !',
|
|
error: 'Erreur lors de la création de la catégorie'
|
|
}
|
|
).then(id => {
|
|
if (tournoi) {
|
|
const trees = build_tree(size, loserMatch)
|
|
console.log("Creating trees for new category:", trees);
|
|
|
|
toast.promise(sendRequest('updateTrees', {categoryId: id, trees: trees}),
|
|
{
|
|
pending: 'Création des arbres du tournoi...',
|
|
success: 'Arbres créés !',
|
|
error: 'Erreur lors de la création des arbres'
|
|
}
|
|
).finally(() => setCatId(id))
|
|
} else {
|
|
setCatId(id);
|
|
}
|
|
})
|
|
}
|
|
|
|
const data = {
|
|
name: name.trim(),
|
|
liceName: lice.trim(),
|
|
type: poule + (tournoi << 1),
|
|
size: size,
|
|
loserMatch: loserMatch
|
|
}
|
|
console.log("Submitting category data:", data);
|
|
}
|
|
|
|
return <form onSubmit={handleSubmit}>
|
|
<div className="modal-header">
|
|
<h1 className="modal-title fs-5" id="CategorieModalLabel">{state.id === undefined ? "Ajouter" : "Modifier"} une catégorie</h1>
|
|
<button type="button" className="btn-close" data-bs-dismiss="modal"
|
|
aria-label="Close"></button>
|
|
</div>
|
|
<div className="modal-body">
|
|
<div className="mb-3">
|
|
<label htmlFor="nameInput1" className="form-label">Nom</label>
|
|
<input type="text" className="form-control" id="nameInput1" placeholder="Epée bouclier" name="name" value={name}
|
|
onChange={e => setName(e.target.value)}/>
|
|
</div>
|
|
|
|
<div className="mb-3">
|
|
<label htmlFor="liceInput1" className="form-label">Nom des zones de combat <small>(séparée par des ';')</small></label>
|
|
<input type="text" className="form-control" id="liceInput1" placeholder="A;B" name="zone de combat" value={lice}
|
|
onChange={e => setLice(e.target.value)}/>
|
|
</div>
|
|
|
|
|
|
<div className="mb-3">
|
|
<label className="form-label">Type</label>
|
|
<div className="form-check form-switch">
|
|
<input className="form-check-input" type="checkbox" role="switch" id="switchCheckDefault" name="poule" checked={poule}
|
|
onChange={e => setPoule(e.target.checked)}/>
|
|
<label className="form-check-label" htmlFor="switchCheckDefault">Poule</label>
|
|
</div>
|
|
|
|
<div className="form-check form-switch">
|
|
<input className="form-check-input" type="checkbox" role="switch" id="switchCheckDefault2" name="trournoi" checked={tournoi}
|
|
onChange={e => setTournoi(e.target.checked)}/>
|
|
<label className="form-check-label" htmlFor="switchCheckDefault2">Tournoi</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mb-3">
|
|
<label htmlFor="sizeInput1" className="form-label">Nombre de combattants</label>
|
|
<input type="number" className="form-control" id="sizeInput1" placeholder="4" name="size" disabled={!tournoi} value={size}
|
|
onChange={e => setSize(Number.parseInt(e.target.value))}/>
|
|
</div>
|
|
|
|
<div className="mb-3">
|
|
<span>Match pour les perdants du tournoi:</span>
|
|
<div className="form-check">
|
|
<input className="form-check-input" type="radio" name="radioDefault" id="radioDefault1" disabled={!tournoi}
|
|
checked={loserMatch === -1} onChange={e => {
|
|
if (e.target.checked) setLoserMatch(-1)
|
|
}}/>
|
|
<label className="form-check-label" htmlFor="radioDefault1">
|
|
Tous les matchs
|
|
</label>
|
|
</div>
|
|
<div className="form-check">
|
|
<input className="form-check-input" type="radio" name="radioDefault" id="radioDefault2" disabled={!tournoi}
|
|
checked={loserMatch === 1} onChange={e => {
|
|
if (e.target.checked) setLoserMatch(1)
|
|
}}/>
|
|
<label className="form-check-label" htmlFor="radioDefault2">
|
|
Demi-finales et finales
|
|
</label>
|
|
</div>
|
|
<div className="form-check">
|
|
<input className="form-check-input" type="radio" name="radioDefault" id="radioDefault3" disabled={!tournoi}
|
|
checked={loserMatch === 0} onChange={e => {
|
|
if (e.target.checked) setLoserMatch(0)
|
|
}}/>
|
|
<label className="form-check-label" htmlFor="radioDefault3">
|
|
Finales uniquement
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
<div className="modal-footer">
|
|
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
|
|
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Enregistrer</button>
|
|
{state.id !== undefined && <button type="button" className="btn btn-danger" data-bs-dismiss="modal" onClick={() => {
|
|
setConfirm({
|
|
title: "Suppression de la catégorie",
|
|
message: `Voulez-vous vraiment supprimer la catégorie ${state.name}. Cela va supprimer tous les matchs associés !`,
|
|
confirm: () => {
|
|
toast.promise(sendRequest('deleteCategory', state.id),
|
|
{
|
|
pending: 'Suppression de la catégorie...',
|
|
success: 'Catégorie supprimée !',
|
|
error: 'Erreur lors de la suppression de la catégorie'
|
|
}
|
|
).then(() => setCatId(null));
|
|
}
|
|
})
|
|
confirmRef.current.click();
|
|
}}>Supprimer</button>}
|
|
</div>
|
|
</form>
|
|
}
|