i18n #99

Merged
Thibaut merged 5 commits from i18n into master 2026-01-16 12:18:54 +00:00
11 changed files with 488 additions and 214 deletions
Showing only changes of commit c7a2215103 - Show all commits

View File

@ -0,0 +1,143 @@
{
"--SélectionnerUnCombattant--": "-- Select a fighter --",
"--Tous--": "-- All --",
"actuel": "Current",
"administration": "Administration",
"adresseDuServeur": "Server address",
"ajouter": "Add",
"ajouterDesCombattants": "Add fighters",
"attention": "Warning",
"aucuneConfigurationObs": "No OBS configuration found, please import one",
"bleu": "Blue",
"blue": "Blue",
"catégorie": "Category",
"chrono.+/-...S": "+/- ... s",
"chrono.+10S": "+10 s",
"chrono.+1S": "+1 s",
"chrono.-10S": "-10 s",
"chrono.-1S": "-1 s",
"chrono.arrêter": "Stop",
"chrono.définirLeTemps": "Set time",
"chrono.démarrer": "Start",
"chrono.editionTemps": "Time editing",
"chrono.entrezLeTempsEnS": "Enter time in seconds",
"chrono.recapTemps": "Time: {{temps}}, pause: {{pause}}",
"chronomètre": "Stopwatch",
"club": "Club",
"compétition": "Competition",
"compétitionManager": "Competition manager",
"config.obs.dossierDesResources": "Resources folder",
"config.obs.motDePasseDuServeur": "Server password",
"config.obs.warn1": "/! The password will be stored in plain text; it is recommended to use it only on OBS WebSocket and to change it between each competition",
"config.obs.ws": "ws://",
"configurationObs": "OBS Configuration",
"confirm1": "This match already has results; are you sure you want to delete it?",
"confirm2.msg": "Do you really want to change the tournament tree size or the loser matches? This will modify existing matches (including possible deletions)!",
"confirm2.title": "Tournament tree change",
"confirm3.msg": "Do you really want to remove the {{typeStr}} part from the category? This will delete the matches contained in this part!",
"confirm3.title": "Change category type",
"confirm4.msg": "Do you really want to delete the category {{name}}. This will delete all associated matches!",
"confirm4.title": "Delete category",
"conserverUniquementLesMatchsTerminés": "Keep only finished matches",
"contre": "vs",
"créerLesMatchs": "Create matches",
"demi-finalesEtFinales": "Semi-finals and finals",
"duréePause": "Pause duration",
"duréeRound": "Round duration",
"editionDeLaCatégorie": "Edit category",
"enregister": "Save",
"enregistrer": "Save",
"epéeBouclier": "Sword and shield",
"err1": "A fighter cannot fight themselves!",
"err2": "The combat zone name format is invalid. Please separate names with ';'.",
"err3": "At least one type (pool or tournament) must be selected.",
"erreurLorsDeLaCopieDansLePresse": "Error while copying to clipboard: ",
"erreurLorsDeLaCréationDesMatchs": "Error while creating matches: ",
"exporter": "Export",
"fermer": "Close",
"finalesUniquement": "Finals only",
"genre": "Gender",
"genre.f": "F",
"genre.h": "M",
"genre.na": "NA",
"inscrit": "Registered",
"manche": "Round",
"matchPourLesPerdantsDuTournoi": "Match for tournament losers:",
"matches": "Matches",
"modifier": "Edit",
"msg1": "There are already matches in this pool; what do you want to do with them?",
"neRienConserver": "Keep nothing",
"no": "No.",
"nom": "Name",
"nomDesZonesDeCombat": "Combat zone names <1>(separated by ';')</1>",
"nouvelle...": "New...",
"obs.préfixDesSources": "Source prefix",
"pays": "Country",
"poids": "Weight",
"poule": "Pool",
"poulePour": "Pool for: ",
"préparation...": "Preparing...",
"rouge": "Red",
"réinitialiser": "Reset",
"résultat": "Result",
"sansPoule": "No pool",
"sauvegarde": "Backup",
"sauvegarder": "Save",
"score": "Score",
"score.err1": "Cannot end a tournament match in a draw.",
"score.spe": "Special scores: <br/>-997: disqualified <br/>-998: absent <br/>-999: forfeit",
"scores": "Scores",
"secrétariatsDeLice": "Ring secretariats",
"select.aucunCombattantDisponible": "No fighters available",
"select.aucunCombattantSélectionné": "No fighters selected",
"select.msg1": "(0 = disabled)",
"select.recherche": "Search",
"select.sélectionnerDesCombatants": "Select fighters",
"select.à": "to",
"serveur": "Server",
"suivant": "Next",
"supprimer": "Delete",
"sélectionneLesModesDaffichage": "Select display modes",
"sélectionner": "Select",
"terminé": "Finished",
"texteCopiéDansLePresse": "Text copied to clipboard! Paste it into an HTML tag on your WordPress.",
"toast.createCategory.error": "Error while creating the category",
"toast.createCategory.pending": "Creating category...",
"toast.createCategory.success": "Category created!",
"toast.deleteCategory.error": "Error while deleting the category",
"toast.deleteCategory.pending": "Deleting category...",
"toast.deleteCategory.success": "Category deleted!",
"toast.matchs.create.error": "Error while creating matches.",
"toast.matchs.create.pending": "Creating matches in progress...",
"toast.matchs.create.success": "Matches created successfully.",
"toast.updateCategory.error": "Error while updating the category",
"toast.updateCategory.pending": "Updating category...",
"toast.updateCategory.success": "Category updated!",
"toast.updateMatchScore.error": "Error while updating match score",
"toast.updateMatchScore.pending": "Updating match score...",
"toast.updateMatchScore.success": "Match score updated!",
"toast.updateTrees.error": "Error while updating trees",
"toast.updateTrees.init.error": "Error while creating trees",
"toast.updateTrees.init.pending": "Creating tournament trees...",
"toast.updateTrees.init.success": "Trees created!",
"toast.updateTrees.pending": "Updating tournament trees...",
"toast.updateTrees.success": "Trees updated!",
"tournoi": "Tournament",
"tournois": "Tournaments",
"tousLesMatchs": "All matches",
"toutConserver": "Keep all",
"ttm.admin.obs": "Short click: Download resources. Long click: Create OBS configuration",
"ttm.admin.scripte": "Copy integration script",
"ttm.table.inverserLaPosition": "Reverse fighter positions on this screen",
"ttm.table.obs": "Short click: Load configuration and connect. Long click: Ring configuration",
"ttm.table.pub_aff": "Open public display",
"ttm.table.pub_score": "Show scores on public display",
"type": "Type",
"téléchargement": "Download",
"téléchargementEnCours": "Downloading...",
"téléchargementTerminé!": "Download completed!",
"uneCatégorie": "a category",
"valider": "Validate",
"zone": "Zone",
"zoneDeCombat": "Combat zone"
}

View File

@ -0,0 +1,143 @@
{
"--SélectionnerUnCombattant--": "-- Sélectionner un combattant --",
"--Tous--": "-- Tous --",
"actuel": "Actuel",
"administration": "Administration",
"adresseDuServeur": "Adresse du serveur",
"ajouter": "Ajouter",
"ajouterDesCombattants": "Ajouter des combattants",
"attention": "Attention",
"aucuneConfigurationObs": "Aucune configuration OBS trouvée, veuillez en importer une",
"bleu": "Bleu",
"blue": "Blue",
"catégorie": "Catégorie",
"chrono.+/-...S": "+/- ... s",
"chrono.+10S": "+10 s",
"chrono.+1S": "+1 s",
"chrono.-10S": "-10 s",
"chrono.-1S": "-1 s",
"chrono.arrêter": "Arrêter",
"chrono.définirLeTemps": "Définir le temps",
"chrono.démarrer": "Démarrer",
"chrono.editionTemps": "Edition temps",
"chrono.entrezLeTempsEnS": "Entrez le temps en s",
"chrono.recapTemps": "Temps: {{temps}}, pause: {{pause}}",
"chronomètre": "Chronomètre",
"club": "Club",
"compétition": "Compétition",
"compétitionManager": "Compétition manager",
"config.obs.dossierDesResources": "Dossier des resources",
"config.obs.motDePasseDuServeur": "Mot de passe du serveur",
"config.obs.warn1": "/! 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",
"config.obs.ws": "ws://",
"configurationObs": "Configuration OBS",
"confirm1": "Ce match a déjà des résultats, êtes-vous sûr de vouloir le supprimer ?",
"confirm2.msg": "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)!",
"confirm2.title": "Changement de l'arbre du tournoi",
"confirm3.msg": "Voulez-vous vraiment enlever la partie {{typeStr}} de la catégorie. Cela va supprimer les matchs contenus dans cette partie !",
"confirm3.title": "Changement de type de catégorie",
"confirm4.msg": "Voulez-vous vraiment supprimer la catégorie {{name}}. Cela va supprimer tous les matchs associés !",
"confirm4.title": "Suppression de la catégorie",
"conserverUniquementLesMatchsTerminés": "Conserver uniquement les matchs terminés",
"contre": "contre",
"créerLesMatchs": "Créer les matchs",
"demi-finalesEtFinales": "Demi-finales et finales",
"duréePause": "Durée pause",
"duréeRound": "Durée round",
"editionDeLaCatégorie": "Edition de la catégorie",
"enregister": "Enregister",
"enregistrer": "Enregistrer",
"epéeBouclier": "Epée bouclier",
"err1": "Un combattant ne peut pas s'affronter lui-même !",
"err2": "Le format du nom des zones de combat est invalide. Veuillez séparer les noms par des ';'.",
"err3": "Au moins un type (poule ou tournoi) doit être sélectionné.",
"erreurLorsDeLaCopieDansLePresse": "Erreur lors de la copie dans le presse-papier : ",
"erreurLorsDeLaCréationDesMatchs": "Erreur lors de la création des matchs: ",
"exporter": "Exporter",
"fermer": "Fermer",
"finalesUniquement": "Finales uniquement",
"genre": "Genre",
"genre.f": "F",
"genre.h": "H",
"genre.na": "NA",
"inscrit": "Inscrit",
"manche": "Manche",
"matchPourLesPerdantsDuTournoi": "Match pour les perdants du tournoi:",
"matches": "Matches",
"modifier": "Modifier",
"msg1": "Il y a déjà des matchs dans cette poule, que voulez-vous faire avec ?",
"neRienConserver": "Ne rien conserver",
"no": "N°",
"nom": "Nom",
"nomDesZonesDeCombat": "Nom des zones de combat <1>(séparée par des ';')</1>",
"nouvelle...": "Nouvelle...",
"obs.préfixDesSources": "Préfix des sources",
"pays": "Pays",
"poids": "Poids",
"poule": "Poule",
"poulePour": "Poule pour: ",
"préparation...": "Préparation...",
"rouge": "Rouge",
"réinitialiser": "Réinitialiser",
"résultat": "Résultat",
"sansPoule": "Sans poule",
"sauvegarde": "Sauvegarde",
"sauvegarder": "Sauvegarder",
"score": "Score",
"score.err1": "Impossible de terminer un match nul en tournois.",
"score.spe": "Score speciaux : <br/>-997 : disqualifié <br/>-998 : absent <br/>-999 : forfait",
"scores": "Scores",
"secrétariatsDeLice": "Secrétariats de lice",
"select.aucunCombattantDisponible": "Aucun combattant disponible",
"select.aucunCombattantSélectionné": "Aucun combattant sélectionné",
"select.msg1": "(0 = désactivé)",
"select.recherche": "Recherche",
"select.sélectionnerDesCombatants": "Sélectionner des combatants",
"select.à": "à",
"serveur": "Serveur",
"suivant": "Suivant",
"supprimer": "Supprimer",
"sélectionneLesModesDaffichage": "Sélectionne les modes d'affichage",
"sélectionner": "Sélectionner",
"terminé": "Terminé",
"texteCopiéDansLePresse": "Texte copié dans le presse-papier ! Collez-le dans une balise HTML sur votre WordPress.",
"toast.createCategory.error": "Erreur lors de la création de la catégorie",
"toast.createCategory.pending": "Création de la catégorie...",
"toast.createCategory.success": "Catégorie créée !",
"toast.deleteCategory.error": "Erreur lors de la suppression de la catégorie",
"toast.deleteCategory.pending": "Suppression de la catégorie...",
"toast.deleteCategory.success": "Catégorie supprimée !",
"toast.matchs.create.error": "Erreur lors de la création des matchs.",
"toast.matchs.create.pending": "Création des matchs en cours...",
"toast.matchs.create.success": "Matchs créés avec succès.",
"toast.updateCategory.error": "Erreur lors de la mise à jour de la catégorie",
"toast.updateCategory.pending": "Mise à jour de la catégorie...",
"toast.updateCategory.success": "Catégorie mise à jour !",
"toast.updateMatchScore.error": "Erreur lors de la mise à jour du score du match",
"toast.updateMatchScore.pending": "Mise à jour du score du match...",
"toast.updateMatchScore.success": "Score du match mis à jour !",
"toast.updateTrees.error": "Erreur lors de la mise à jour des arbres",
"toast.updateTrees.init.error": "Erreur lors de la création des arbres",
"toast.updateTrees.init.pending": "Création des arbres du tournoi...",
"toast.updateTrees.init.success": "Arbres créés !",
"toast.updateTrees.pending": "Mise à jour des arbres du tournoi...",
"toast.updateTrees.success": "Arbres mis à jour !",
"tournoi": "Tournoi",
"tournois": "Tournois",
"tousLesMatchs": "Tous les matchs",
"toutConserver": "Tout conserver",
"ttm.admin.obs": "Clique court : Télécharger les ressources. Clique long : Créer la configuration obs",
"ttm.admin.scripte": "Copier le scripte d'intégration",
"ttm.table.inverserLaPosition": "Inverser la position des combattants sur cette écran",
"ttm.table.obs": "Clique court : Charger la configuration et se connecter. Clique long : Configuration de la lice",
"ttm.table.pub_aff": "Ouvrir l'affichage public",
"ttm.table.pub_score": "Afficher les scores sur l'affichage public",
"type": "Type",
"téléchargement": "Téléchargement",
"téléchargementEnCours": "Téléchargement en cours...",
"téléchargementTerminé!": "Téléchargement terminé !",
"uneCatégorie": "une catégorie",
"valider": "Valider",
"zone": "Zone",
"zoneDeCombat": "Zone de combat"
}

View File

@ -12,6 +12,9 @@ import {SimpleIconsOBS} from "../../../assets/SimpleIconsOBS.ts";
import JSZip from "jszip"; import JSZip from "jszip";
import {detectOptimalBackground} from "../../../components/SmartLogoBackground.jsx"; import {detectOptimalBackground} from "../../../components/SmartLogoBackground.jsx";
import {faGlobe} from "@fortawesome/free-solid-svg-icons"; import {faGlobe} from "@fortawesome/free-solid-svg-icons";
import {Trans, useTranslation} from "react-i18next";
import i18n from "i18next";
import {getToastMessage} from "../../../utils/Tools.js";
const vite_url = import.meta.env.VITE_URL; const vite_url = import.meta.env.VITE_URL;
@ -139,7 +142,7 @@ async function downloadResourcesAsZip(resourceList) {
completed++; completed++;
const progress = Math.round((completed / resourceList.length) * 100); const progress = Math.round((completed / resourceList.length) * 100);
progressBar.style.width = `${progress}%`; progressBar.style.width = `${progress}%`;
progressText.textContent = `Téléchargement (${completed}/${resourceList.length}) : ${result.filename}`; progressText.textContent = `${i18n.t('téléchargement')} (${completed}/${resourceList.length}) : ${result.filename}`;
return result; return result;
}) })
); );
@ -155,13 +158,14 @@ async function downloadResourcesAsZip(resourceList) {
// Fermer la modale // Fermer la modale
modal.hide(); modal.hide();
progressText.textContent = "Téléchargement terminé !"; progressText.textContent = i18n.t('téléchargementTerminé!');
} }
function Menu({menuActions, compUuid}) { function Menu({menuActions, compUuid}) {
const e = document.getElementById("actionMenu") const e = document.getElementById("actionMenu")
const longPress = useRef({time: null, timer: null, button: null}); const longPress = useRef({time: null, timer: null, button: null});
const obsModal = useRef(null); const obsModal = useRef(null);
const {t} = useTranslation("cm");
for (const x of tto) for (const x of tto)
x.dispose(); x.dispose();
@ -224,9 +228,9 @@ function Menu({menuActions, compUuid}) {
initCompetitionApi("${vite_url}/api/public/result/${compUuid}") initCompetitionApi("${vite_url}/api/public/result/${compUuid}")
</script>` </script>`
).then(() => { ).then(() => {
toast.success("Texte copié dans le presse-papier ! Collez-le dans une balise HTML sur votre WordPress."); toast.success(t('texteCopiéDansLePresse'));
}).catch(err => { }).catch(err => {
toast.error("Erreur lors de la copie dans le presse-papier : " + err); toast.error(t('erreurLorsDeLaCopieDansLePresse') + err);
}); });
} }
@ -241,12 +245,12 @@ function Menu({menuActions, compUuid}) {
onMouseDown={() => longPressDown("obs")} onMouseDown={() => longPressDown("obs")}
onMouseUp={() => longPressUp("obs")} onMouseUp={() => longPressUp("obs")}
data-bs-toggle="tooltip2" data-bs-placement="top" 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"/> data-bs-title={t('ttm.admin.obs')}/>
<FontAwesomeIcon icon={faGlobe} size="xl" <FontAwesomeIcon icon={faGlobe} size="xl"
style={{color: "#6c757d", cursor: "pointer"}} style={{color: "#6c757d", cursor: "pointer"}}
onClick={() => copyScriptToClipboard()} onClick={() => copyScriptToClipboard()}
data-bs-toggle="tooltip2" data-bs-placement="top" data-bs-toggle="tooltip2" data-bs-placement="top"
data-bs-title="Copier le scripte d'intégration"/> data-bs-title={t('ttm.admin.scripte')}/>
</>, document.getElementById("actionMenu"))} </>, document.getElementById("actionMenu"))}
<button ref={obsModal} type="button" className="btn btn-link" data-bs-toggle="modal" data-bs-target="#OBSModal" style={{display: 'none'}}> <button ref={obsModal} type="button" className="btn btn-link" data-bs-toggle="modal" data-bs-target="#OBSModal" style={{display: 'none'}}>
@ -256,33 +260,32 @@ function Menu({menuActions, compUuid}) {
<div className="modal-dialog"> <div className="modal-dialog">
<div className="modal-content"> <div className="modal-content">
<div className="modal-header"> <div className="modal-header">
<h5 className="modal-title">Configuration OBS</h5> <h5 className="modal-title">{t('configurationObs')}</h5>
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<form onSubmit={handleOBSSubmit}> <form onSubmit={handleOBSSubmit}>
<div className="modal-body"> <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 <strong>{t('config.obs.warn1')}</strong>
changer entre chaque compétition</strong>
<div className="input-group mb-3"> <div className="input-group mb-3">
<span className="input-group-text">Adresse du serveur</span> <span className="input-group-text">{t('adresseDuServeur')}</span>
<span className="input-group-text">ws://</span> <span className="input-group-text">{t('config.obs.ws')}</span>
<input type="text" className="form-control" placeholder="127.0.0.1:4455" aria-label="" <input type="text" className="form-control" placeholder="127.0.0.1:4455" aria-label=""
defaultValue={"127.0.0.1:4455"}/> defaultValue={"127.0.0.1:4455"}/>
<span className="input-group-text">/</span> <span className="input-group-text">/</span>
</div> </div>
<div className="input-group mb-3"> <div className="input-group mb-3">
<span className="input-group-text">Mot de passe du serveur</span> <span className="input-group-text">{t('config.obs.motDePasseDuServeur')}</span>
<input type="password" className="form-control" placeholder="12345" aria-label="" <input type="password" className="form-control" placeholder="12345" aria-label=""
defaultValue={""}/> defaultValue={""}/>
</div> </div>
<div className="input-group mb-3"> <div className="input-group mb-3">
<span className="input-group-text">Dossier des resources</span> <span className="input-group-text">{t('config.obs.dossierDesResources')}</span>
<input type="text" className="form-control" placeholder="" aria-label="" required/> <input type="text" className="form-control" placeholder="" aria-label="" required/>
</div> </div>
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button> <button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Exporter</button> <button type="submit" className="btn btn-primary" data-bs-dismiss="modal">{t('exporter')}</button>
</div> </div>
</form> </form>
</div> </div>
@ -293,14 +296,14 @@ function Menu({menuActions, compUuid}) {
<div className="modal-dialog"> <div className="modal-dialog">
<div className="modal-content"> <div className="modal-content">
<div className="modal-header"> <div className="modal-header">
<h5 className="modal-title">Téléchargement en cours...</h5> <h5 className="modal-title">{t('téléchargementEnCours')}</h5>
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div className="modal-body"> <div className="modal-body">
<div className="progress"> <div className="progress">
<div id="progressBar" className="progress-bar" role="progressbar" style={{width: "0%"}}></div> <div id="progressBar" className="progress-bar" role="progressbar" style={{width: "0%"}}></div>
</div> </div>
<div id="progressText" className="mt-2">Préparation...</div> <div id="progressText" className="mt-2">{t('préparation...')}</div>
</div> </div>
</div> </div>
</div> </div>
@ -314,6 +317,7 @@ function CategoryHeader({cat, setCatId}) {
const confirmRef = useRef(); const confirmRef = useRef();
const [modal, setModal] = useState({}) const [modal, setModal] = useState({})
const [confirm, setConfirm] = useState({}) const [confirm, setConfirm] = useState({})
const {t} = useTranslation("cm");
const {data: cats, setData: setCats} = useRequestWS('getAllCategory', {}, setLoading); const {data: cats, setData: setCats} = useRequestWS('getAllCategory', {}, setLoading);
const {dispatch} = useWS(); const {dispatch} = useWS();
@ -368,17 +372,17 @@ function CategoryHeader({cat, setCatId}) {
return <div className="row"> return <div className="row">
<div className="col-auto"> <div className="col-auto">
<div className="input-group"> <div className="input-group">
<h5 style={{margin: "auto 0.5em auto 0"}}>Edition de la catégorie</h5> <h5 style={{margin: "auto 0.5em auto 0"}}>{t('editionDeLaCatégorie')}</h5>
<select className="form-select" onChange={handleCatChange} value={cat?.id || ""}> <select className="form-select" onChange={handleCatChange} value={cat?.id || ""}>
{cats && cats.sort((a, b) => a.name.localeCompare(b.name)).map(c => ( {cats && cats.sort((a, b) => a.name.localeCompare(b.name)).map(c => (
<option key={c.id} value={c.id}>{c.name}</option>))} <option key={c.id} value={c.id}>{c.name}</option>))}
{cats && <option value={-1}>Nouvelle...</option>} {cats && <option value={-1}>{t('nouvelle...')}</option>}
</select> </select>
</div> </div>
</div> </div>
<div className="col" style={{margin: "auto 0", textAlign: "center"}}> <div className="col" style={{margin: "auto 0", textAlign: "center"}}>
{cat && {cat &&
<div>Type: {(cat.type & 1) !== 0 ? "Poule" : ""}{cat.type === 3 ? " & " : ""}{(cat.type & 2) !== 0 ? "Tournois" : ""} | <div>Type: {(cat.type & 1) !== 0 ? t('poule') : ""}{cat.type === 3 ? " & " : ""}{(cat.type & 2) !== 0 ? t('tournois') : ""} |
Zone: {cat.liceName}</div>} Zone: {cat.liceName}</div>}
</div> </div>
<div className="col-auto"> <div className="col-auto">
@ -409,17 +413,18 @@ function CategoryHeader({cat, setCatId}) {
function ModalContent({state, setCatId, setConfirm, confirmRef}) { function ModalContent({state, setCatId, setConfirm, confirmRef}) {
const [name, setName] = useState("") const [name, setName] = useState("")
const [lice, setLice] = useState("A") const [lice, setLice] = useState("1")
const [poule, setPoule] = useState(true) const [poule, setPoule] = useState(true)
const [tournoi, setTournoi] = useState(false) const [tournoi, setTournoi] = useState(false)
const [size, setSize] = useState(4) const [size, setSize] = useState(4)
const [loserMatch, setLoserMatch] = useState(1) const [loserMatch, setLoserMatch] = useState(1)
const {t} = useTranslation("cm");
const {sendRequest} = useWS(); const {sendRequest} = useWS();
useEffect(() => { useEffect(() => {
setName(state.name || ""); setName(state.name || "");
setLice(state.liceName || "A"); setLice(state.liceName || "1");
setPoule(((state.type || 1) & 1) !== 0); setPoule(((state.type || 1) & 1) !== 0);
setTournoi((state.type & 2) !== 0); setTournoi((state.type & 2) !== 0);
@ -445,13 +450,13 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
const regex = /^([^;]+;)*[^;]+$/; const regex = /^([^;]+;)*[^;]+$/;
if (regex.test(lice.trim()) === false) { 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 ';'."); toast.error(t('err2'));
return; return;
} }
const nType = (poule ? 1 : 0) + (tournoi ? 2 : 0); const nType = (poule ? 1 : 0) + (tournoi ? 2 : 0);
if (nType === 0) { if (nType === 0) {
toast.error("Au moins un type (poule ou tournoi) doit être sélectionné."); toast.error(t('err3'));
return; return;
} }
@ -479,8 +484,8 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
console.log(tournoi, size, nbMatch, loserMatch, oldSubTree); console.log(tournoi, size, nbMatch, loserMatch, oldSubTree);
if (tournoi && (size !== nbMatch * 2 || loserMatch !== oldSubTree)) { if (tournoi && (size !== nbMatch * 2 || loserMatch !== oldSubTree)) {
setConfirm({ setConfirm({
title: "Changement de l'arbre du tournoi", title: t('confirm2.title'),
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)!`, message: t('confirm2.msg'),
confirm: () => { confirm: () => {
const trees2 = build_tree(size, loserMatch) const trees2 = build_tree(size, loserMatch)
const newTrees = [] const newTrees = []
@ -495,48 +500,30 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
newTrees.push(trees2.at(i)); newTrees.push(trees2.at(i));
} }
toast.promise(sendRequest('updateTrees', {categoryId: state.id, trees: newTrees}), toast.promise(sendRequest('updateTrees', {categoryId: state.id, trees: newTrees}), getToastMessage("toast.updateTrees")
{
pending: 'Mise à jour des arbres du tournoi...',
success: 'Arbres mis à jour !',
error: 'Erreur lors de la mise à jour des arbres'
}
).then(__ => { ).then(__ => {
toast.promise(sendRequest('updateCategory', newData), toast.promise(sendRequest('updateCategory', newData), getToastMessage("toast.updateCategory"))
{
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(); confirmRef.current.click();
} else { } else {
toast.promise(sendRequest('updateCategory', newData), toast.promise(sendRequest('updateCategory', newData), getToastMessage("toast.updateCategory"))
{
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) { if (nType !== state.type) {
let typeStr = ""; let typeStr = "";
if ((state.type & 1) !== 0 && (nType & 1) === 0) if ((state.type & 1) !== 0 && (nType & 1) === 0)
typeStr += "poule "; typeStr += `${t('poule').toLowerCase()} `;
if ((state.type & 2) !== 0 && (nType & 2) === 0) if ((state.type & 2) !== 0 && (nType & 2) === 0)
typeStr += "tournoi "; typeStr += `${t('tournoi').toLowerCase()} `;
setConfirm({ setConfirm({
title: "Changement de type de catégorie", title: t('confirm3.title'),
message: `Voulez-vous vraiment enlever la partie ${typeStr} de la catégorie. Cela va supprimer les matchs contenus dans cette partie !`, message: t('confirm3.msg', {typeStr: typeStr.trim()}),
confirm: () => { confirm: () => {
setTimeout(() => setTimeout(() => applyChanges(), 500);
applyChanges(), 500);
} }
}) })
confirmRef.current.click(); confirmRef.current.click();
@ -544,23 +531,17 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
applyChanges(); applyChanges();
} }
} else { } else {
toast.promise(sendRequest('createCategory', {name: name.trim(), liceName: lice.trim(), type: nType}), toast.promise(sendRequest('createCategory', {
{ name: name.trim(),
pending: 'Création de la catégorie...', liceName: lice.trim(),
success: 'Catégorie créée !', type: nType
error: 'Erreur lors de la création de la catégorie' }), getToastMessage("toast.createCategory")
}
).then(id => { ).then(id => {
if (tournoi) { if (tournoi) {
const trees = build_tree(size, loserMatch) const trees = build_tree(size, loserMatch)
console.log("Creating trees for new category:", trees); console.log("Creating trees for new category:", trees);
toast.promise(sendRequest('updateTrees', {categoryId: id, trees: trees}), toast.promise(sendRequest('updateTrees', {categoryId: id, trees: trees}), getToastMessage("toast.updateTrees.init")
{
pending: 'Création des arbres du tournoi...',
success: 'Arbres créés !',
error: 'Erreur lors de la création des arbres'
}
).finally(() => setCatId(id)) ).finally(() => setCatId(id))
} else { } else {
setCatId(id); setCatId(id);
@ -580,36 +561,36 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
return <form onSubmit={handleSubmit}> return <form onSubmit={handleSubmit}>
<div className="modal-header"> <div className="modal-header">
<h1 className="modal-title fs-5" id="CategorieModalLabel">{state.id === undefined ? "Ajouter" : "Modifier"} une catégorie</h1> <h1 className="modal-title fs-5" id="CategorieModalLabel">{state.id === undefined ? t('ajouter') : t('modifier')} {t('uneCatégorie')}</h1>
<button type="button" className="btn-close" data-bs-dismiss="modal" <button type="button" className="btn-close" data-bs-dismiss="modal"
aria-label="Close"></button> aria-label="Close"></button>
</div> </div>
<div className="modal-body"> <div className="modal-body">
<div className="mb-3"> <div className="mb-3">
<label htmlFor="nameInput1" className="form-label">Nom</label> <label htmlFor="nameInput1" className="form-label">{t('nom')}</label>
<input type="text" className="form-control" id="nameInput1" placeholder="Epée bouclier" name="name" value={name} <input type="text" className="form-control" id="nameInput1" placeholder={t('epéeBouclier')} name="name" value={name}
onChange={e => setName(e.target.value)}/> onChange={e => setName(e.target.value)}/>
</div> </div>
<div className="mb-3"> <div className="mb-3">
<label htmlFor="liceInput1" className="form-label">Nom des zones de combat <small>(séparée par des ';')</small></label> <label htmlFor="liceInput1" className="form-label"><Trans i18nKey="nomDesZonesDeCombat" ns="cm">t <small>(séparée par des ';')</small></Trans></label>
<input type="text" className="form-control" id="liceInput1" placeholder="A;B" name="zone de combat" value={lice} <input type="text" className="form-control" id="liceInput1" placeholder="1;2" name="zone de combat" value={lice}
onChange={e => setLice(e.target.value)}/> onChange={e => setLice(e.target.value)}/>
</div> </div>
<div className="mb-3"> <div className="mb-3">
<label className="form-label">Type</label> <label className="form-label">{t('type')}</label>
<div className="form-check form-switch"> <div className="form-check form-switch">
<input className="form-check-input" type="checkbox" role="switch" id="switchCheckDefault" name="poule" checked={poule} <input className="form-check-input" type="checkbox" role="switch" id="switchCheckDefault" name="poule" checked={poule}
onChange={e => setPoule(e.target.checked)}/> onChange={e => setPoule(e.target.checked)}/>
<label className="form-check-label" htmlFor="switchCheckDefault">Poule</label> <label className="form-check-label" htmlFor="switchCheckDefault">{t('poule')}</label>
</div> </div>
<div className="form-check form-switch"> <div className="form-check form-switch">
<input className="form-check-input" type="checkbox" role="switch" id="switchCheckDefault2" name="trournoi" checked={tournoi} <input className="form-check-input" type="checkbox" role="switch" id="switchCheckDefault2" name="trournoi" checked={tournoi}
onChange={e => setTournoi(e.target.checked)}/> onChange={e => setTournoi(e.target.checked)}/>
<label className="form-check-label" htmlFor="switchCheckDefault2">Tournoi</label> <label className="form-check-label" htmlFor="switchCheckDefault2">{t('tournoi')}</label>
</div> </div>
</div> </div>
@ -620,14 +601,14 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
</div> </div>
<div className="mb-3"> <div className="mb-3">
<span>Match pour les perdants du tournoi:</span> <span>{t('matchPourLesPerdantsDuTournoi')}</span>
<div className="form-check"> <div className="form-check">
<input className="form-check-input" type="radio" name="radioDefault" id="radioDefault1" disabled={!tournoi} <input className="form-check-input" type="radio" name="radioDefault" id="radioDefault1" disabled={!tournoi}
checked={loserMatch === -1} onChange={e => { checked={loserMatch === -1} onChange={e => {
if (e.target.checked) setLoserMatch(-1) if (e.target.checked) setLoserMatch(-1)
}}/> }}/>
<label className="form-check-label" htmlFor="radioDefault1"> <label className="form-check-label" htmlFor="radioDefault1">
Tous les matchs {t('tousLesMatchs')}
</label> </label>
</div> </div>
<div className="form-check"> <div className="form-check">
@ -636,7 +617,7 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
if (e.target.checked) setLoserMatch(1) if (e.target.checked) setLoserMatch(1)
}}/> }}/>
<label className="form-check-label" htmlFor="radioDefault2"> <label className="form-check-label" htmlFor="radioDefault2">
Demi-finales et finales {t('demi-finalesEtFinales')}
</label> </label>
</div> </div>
<div className="form-check"> <div className="form-check">
@ -645,31 +626,26 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
if (e.target.checked) setLoserMatch(0) if (e.target.checked) setLoserMatch(0)
}}/> }}/>
<label className="form-check-label" htmlFor="radioDefault3"> <label className="form-check-label" htmlFor="radioDefault3">
Finales uniquement {t('finalesUniquement')}
</label> </label>
</div> </div>
</div> </div>
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button> <button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Enregistrer</button> <button type="submit" className="btn btn-primary" data-bs-dismiss="modal">{t('enregistrer')}</button>
{state.id !== undefined && <button type="button" className="btn btn-danger" data-bs-dismiss="modal" onClick={() => { {state.id !== undefined && <button type="button" className="btn btn-danger" data-bs-dismiss="modal" onClick={() => {
setConfirm({ setConfirm({
title: "Suppression de la catégorie", title: t('confirm4.title'),
message: `Voulez-vous vraiment supprimer la catégorie ${state.name}. Cela va supprimer tous les matchs associés !`, message: t('confirm4.msg', {name: state.name}),
confirm: () => { confirm: () => {
toast.promise(sendRequest('deleteCategory', state.id), toast.promise(sendRequest('deleteCategory', state.id), getToastMessage("toast.deleteCategory")
{
pending: 'Suppression de la catégorie...',
success: 'Catégorie supprimée !',
error: 'Erreur lors de la suppression de la catégorie'
}
).then(() => setCatId(null)); ).then(() => setCatId(null));
} }
}) })
confirmRef.current.click(); confirmRef.current.click();
}}>Supprimer</button>} }}>{t('supprimer')}</button>}
</div> </div>
</form> </form>
} }

View File

@ -1,6 +1,7 @@
import React, {useEffect, useRef, useState} from "react"; import React, {useEffect, useRef, useState} from "react";
import {usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx"; import {usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx";
import {timePrint} from "../../../utils/Tools.js"; import {timePrint} from "../../../utils/Tools.js";
import {useTranslation} from "react-i18next";
export function ChronoPanel() { export function ChronoPanel() {
const [config, setConfig] = useState({ const [config, setConfig] = useState({
@ -11,6 +12,7 @@ export function ChronoPanel() {
const chronoText = useRef(null) const chronoText = useRef(null)
const state = useRef({chronoState: 0, countBlink: 20, lastColor: "#000000", lastTimeStr: "00:00"}) const state = useRef({chronoState: 0, countBlink: 20, lastColor: "#000000", lastTimeStr: "00:00"})
const publicAffDispatch = usePubAffDispatch(); const publicAffDispatch = usePubAffDispatch();
const {t} = useTranslation("cm");
const addTime = (time) => setChrono(prev => ({...prev, time: prev.time - time})) const addTime = (time) => setChrono(prev => ({...prev, time: prev.time - time}))
const isRunning = () => chrono.startTime !== 0 const isRunning = () => chrono.startTime !== 0
@ -111,11 +113,11 @@ export function ChronoPanel() {
onClick={__ => isRunning() ? onClick={__ => isRunning() ?
setChrono(prev => ({...prev, time: prev.time + Date.now() - prev.startTime, startTime: 0})) : setChrono(prev => ({...prev, time: prev.time + Date.now() - prev.startTime, startTime: 0})) :
setChrono(prev => ({...prev, startTime: Date.now()}))}> setChrono(prev => ({...prev, startTime: Date.now()}))}>
{isRunning() ? "Arrêter" : "Démarrer"}</button> {isRunning() ? t('chrono.arrêter') : t('chrono.démarrer')}</button>
<button className="btn btn-danger col" onClick={__ => { <button className="btn btn-danger col" onClick={__ => {
setChrono(prev => ({...prev, time: 0, startTime: 0})) setChrono(prev => ({...prev, time: 0, startTime: 0}))
state.current.chronoState = 0 state.current.chronoState = 0
}}>Réinitialiser }}>{t('réinitialiser')}
</button> </button>
</div> </div>
<div className="row" style={{marginTop: "0.5em"}}> <div className="row" style={{marginTop: "0.5em"}}>
@ -125,29 +127,29 @@ export function ChronoPanel() {
</div> </div>
<div className="col" style={{margin: "auto 0"}}> <div className="col" style={{margin: "auto 0"}}>
<div className="row"> <div className="row">
<button className="btn btn-outline-secondary col-6" onClick={__ => addTime(-10000)}>-10 s</button> <button className="btn btn-outline-secondary col-6" onClick={__ => addTime(-10000)}>{t('chrono.-10S')}</button>
<button className="btn btn-outline-secondary col-6" onClick={__ => addTime(10000)}>+10 s</button> <button className="btn btn-outline-secondary col-6" onClick={__ => addTime(10000)}>{t('chrono.+10S')}</button>
</div> </div>
<div className="row" style={{marginTop: "0.5em"}}> <div className="row" style={{marginTop: "0.5em"}}>
<button className="btn btn-outline-secondary col-6" onClick={__ => addTime(-1000)}>-1 s</button> <button className="btn btn-outline-secondary col-6" onClick={__ => addTime(-1000)}>{t('chrono.-1S')}</button>
<button className="btn btn-outline-secondary col-6" onClick={__ => addTime(1000)}>+1 s</button> <button className="btn btn-outline-secondary col-6" onClick={__ => addTime(1000)}>{t('chrono.+1S')}</button>
</div> </div>
<div className="row" style={{marginTop: "0.5em"}}> <div className="row" style={{marginTop: "0.5em"}}>
<button className="btn btn-outline-secondary col-12" onClick={__ => { <button className="btn btn-outline-secondary col-12" onClick={__ => {
const timeStr = prompt("Entrez le temps en s", "0"); const timeStr = prompt(t('chrono.entrezLeTempsEnS'), "0");
if (timeStr === null) if (timeStr === null)
return; return;
addTime(parseInt(timeStr, 10) * 1000); addTime(parseInt(timeStr, 10) * 1000);
}}>+/- ... s }}>{t('chrono.+/-...S')}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<div className="row" style={{marginTop: "0.5em"}}> <div className="row" style={{marginTop: "0.5em"}}>
<div className="col-12 col-sm-8" style={{margin: 'auto 0'}}> <div className="col-12 col-sm-8" style={{margin: 'auto 0'}}>
<div>Temps: {timePrint(config.time)}, pause: {timePrint(config.pause)}</div> <div>{t('chrono.recapTemps', {temps: timePrint(config.time), pause: timePrint(config.pause)})}</div>
</div> </div>
<button className="btn btn-secondary col" data-bs-toggle="modal" data-bs-target="#timeModal">Définir le temps <button className="btn btn-secondary col" data-bs-toggle="modal" data-bs-target="#timeModal">{t('chrono.définirLeTemps')}
</button> </button>
</div> </div>
@ -155,25 +157,25 @@ export function ChronoPanel() {
<div className="modal-dialog"> <div className="modal-dialog">
<div className="modal-content"> <div className="modal-content">
<div className="modal-header"> <div className="modal-header">
<h5 className="modal-title">Edition temps</h5> <h5 className="modal-title">{t('chrono.editionTemps')}</h5>
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="modal-body"> <div className="modal-body">
<div className="input-group mb-3"> <div className="input-group mb-3">
<span className="input-group-text">Durée round</span> <span className="input-group-text">{t('duréeRound')}</span>
<input type="text" className="form-control" placeholder="0" aria-label="Min" defaultValue={timePrint(config.time)}/> <input type="text" className="form-control" placeholder="0" aria-label="Min" defaultValue={timePrint(config.time)}/>
<span className="input-group-text">(mm:ss)</span> <span className="input-group-text">(mm:ss)</span>
</div> </div>
<div className="input-group mb-3"> <div className="input-group mb-3">
<span className="input-group-text">Durée pause</span> <span className="input-group-text">{t('duréePause')}</span>
<input type="text" className="form-control" placeholder="0" aria-label="Min" defaultValue={timePrint(config.pause)}/> <input type="text" className="form-control" placeholder="0" aria-label="Min" defaultValue={timePrint(config.pause)}/>
<span className="input-group-text">(mm:ss)</span> <span className="input-group-text">(mm:ss)</span>
</div> </div>
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button> <button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Valider</button> <button type="submit" className="btn btn-primary" data-bs-dismiss="modal">{t('valider')}</button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -6,12 +6,13 @@ import {DrawGraph} from "../../result/DrawGraph.jsx";
import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {useRequestWS, useWS} from "../../../hooks/useWS.jsx"; import {useRequestWS, useWS} from "../../../hooks/useWS.jsx";
import {MarchReducer} from "../../../utils/MatchReducer.jsx"; import {MarchReducer} from "../../../utils/MatchReducer.jsx";
import {scorePrint, win} from "../../../utils/Tools.js"; import {getToastMessage, scorePrint, win} from "../../../utils/Tools.js";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faCircleQuestion} from "@fortawesome/free-regular-svg-icons"; import {faCircleQuestion} from "@fortawesome/free-regular-svg-icons";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import "./CMTMatchPanel.css" import "./CMTMatchPanel.css"
import {useOBS} from "../../../hooks/useOBS.jsx"; import {useOBS} from "../../../hooks/useOBS.jsx";
import {useTranslation} from "react-i18next";
function CupImg() { function CupImg() {
return <img decoding="async" loading="lazy" width={"16"} height={"16"} className="wp-image-1635" return <img decoding="async" loading="lazy" width={"16"} height={"16"} className="wp-image-1635"
@ -24,6 +25,7 @@ export function CategorieSelect({catId, setCatId, menuActions}) {
const {data: cats, setData: setCats} = useRequestWS('getAllCategory', {}, setLoading); const {data: cats, setData: setCats} = useRequestWS('getAllCategory', {}, setLoading);
const {dispatch} = useWS(); const {dispatch} = useWS();
const {connected, setText} = useOBS(); const {connected, setText} = useOBS();
const {t} = useTranslation("cm");
useEffect(() => { useEffect(() => {
const categoryListener = ({data}) => { const categoryListener = ({data}) => {
@ -55,7 +57,7 @@ export function CategorieSelect({catId, setCatId, menuActions}) {
return <> return <>
<div className="input-group"> <div className="input-group">
<h6 style={{margin: "auto 0.5em auto 0"}}>Catégorie</h6> <h6 style={{margin: "auto 0.5em auto 0"}}>{t('catégorie')}</h6>
<select className="form-select" onChange={e => setCatId(Number(e.target.value))} value={catId}> <select className="form-select" onChange={e => setCatId(Number(e.target.value))} value={catId}>
{cats && <option value={-1}></option>} {cats && <option value={-1}></option>}
{cats && cats.sort((a, b) => a.name.localeCompare(b.name)).map(c => ( {cats && cats.sort((a, b) => a.name.localeCompare(b.name)).map(c => (
@ -149,6 +151,7 @@ function CMTMatchPanel({catId, cat, menuActions}) {
function ListMatch({cat, matches, trees, menuActions}) { function ListMatch({cat, matches, trees, menuActions}) {
const [type, setType] = useState(1); const [type, setType] = useState(1);
const {t} = useTranslation("cm");
useEffect(() => { useEffect(() => {
if (!cat) if (!cat)
@ -165,12 +168,12 @@ function ListMatch({cat, matches, trees, menuActions}) {
<ul className="nav nav-tabs"> <ul className="nav nav-tabs">
<li className="nav-item"> <li className="nav-item">
<div className={"nav-link" + (type === 1 ? " active" : "")} aria-current={(type === 1 ? " page" : "false")} <div className={"nav-link" + (type === 1 ? " active" : "")} aria-current={(type === 1 ? " page" : "false")}
onClick={_ => setType(1)}>Poule onClick={_ => setType(1)}>{t('poule')}
</div> </div>
</li> </li>
<li className="nav-item"> <li className="nav-item">
<div className={"nav-link" + (type === 2 ? " active" : "")} aria-current={(type === 2 ? " page" : "false")} <div className={"nav-link" + (type === 2 ? " active" : "")} aria-current={(type === 2 ? " page" : "false")}
onClick={_ => setType(2)}>Tournois onClick={_ => setType(2)}>{t('tournois')}
</div> </div>
</li> </li>
</ul> </ul>
@ -189,8 +192,9 @@ function ListMatch({cat, matches, trees, menuActions}) {
function MatchList({matches, cat, menuActions}) { function MatchList({matches, cat, menuActions}) {
const [activeMatch, setActiveMatch] = useState(null) const [activeMatch, setActiveMatch] = useState(null)
const [lice, setLice] = useState(localStorage.getItem("cm_lice") || "A") const [lice, setLice] = useState(localStorage.getItem("cm_lice") || "1")
const publicAffDispatch = usePubAffDispatch(); const publicAffDispatch = usePubAffDispatch();
const {t} = useTranslation("cm");
const liceName = (cat.liceName || "N/A").split(";"); const liceName = (cat.liceName || "N/A").split(";");
const marches2 = matches.filter(m => m.categorie_ord !== -42) const marches2 = matches.filter(m => m.categorie_ord !== -42)
@ -241,7 +245,7 @@ function MatchList({matches, cat, menuActions}) {
return <> return <>
{liceName.length > 1 && {liceName.length > 1 &&
<div className="input-group" style={{maxWidth: "15em", marginTop: "0.5em"}}> <div className="input-group" style={{maxWidth: "15em", marginTop: "0.5em"}}>
<label className="input-group-text" htmlFor="selectLice">Zone de combat</label> <label className="input-group-text" htmlFor="selectLice">{t('zoneDeCombat')}</label>
<select className="form-select" id="selectLice" value={lice} onChange={e => { <select className="form-select" id="selectLice" value={lice} onChange={e => {
setLice(e.target.value); setLice(e.target.value);
localStorage.setItem("cm_lice", e.target.value); localStorage.setItem("cm_lice", e.target.value);
@ -259,10 +263,10 @@ function MatchList({matches, cat, menuActions}) {
<tr> <tr>
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">Z</th> <th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">Z</th>
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">P</th> <th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">P</th>
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">N°</th> <th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">{t('no')}</th>
<th style={{textAlign: "center"}} scope="col"></th> <th style={{textAlign: "center"}} scope="col"></th>
<th style={{textAlign: "center"}} scope="col">Rouge</th> <th style={{textAlign: "center"}} scope="col">{t('rouge')}</th>
<th style={{textAlign: "center"}} scope="col">Blue</th> <th style={{textAlign: "center"}} scope="col">{t('blue')}</th>
<th style={{textAlign: "center"}} scope="col"></th> <th style={{textAlign: "center"}} scope="col"></th>
</tr> </tr>
</thead> </thead>
@ -288,7 +292,8 @@ function MatchList({matches, cat, menuActions}) {
</table> </table>
</div> </div>
{activeMatch && <LoadingProvider><ScorePanel matchId={activeMatch} matchs={matches} match={match} menuActions={menuActions}/></LoadingProvider>} {activeMatch &&
<LoadingProvider><ScorePanel matchId={activeMatch} matchs={matches} match={match} menuActions={menuActions}/></LoadingProvider>}
</> </>
} }
@ -362,7 +367,8 @@ function BuildTree({treeData, matches, menuActions}) {
</div> </div>
{currentMatch?.matchSelect && {currentMatch?.matchSelect &&
<LoadingProvider><ScorePanel matchId={currentMatch?.matchSelect} matchs={matches} match={match} menuActions={menuActions}/></LoadingProvider>} <LoadingProvider><ScorePanel matchId={currentMatch?.matchSelect} matchs={matches} match={match}
menuActions={menuActions}/></LoadingProvider>}
</div> </div>
} }
@ -379,6 +385,7 @@ function ScorePanel({matchId, matchs, match, menuActions}) {
function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) { function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) {
const {sendRequest} = useWS() const {sendRequest} = useWS()
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {t} = useTranslation("cm");
const [end, setEnd] = useState(match?.end || false) const [end, setEnd] = useState(match?.end || false)
const [scoreIn, setScoreIn] = useState("") const [scoreIn, setScoreIn] = useState("")
@ -396,13 +403,7 @@ function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) {
menuActions.current.saveScore = (scoreRed, scoreBlue) => { menuActions.current.saveScore = (scoreRed, scoreBlue) => {
const maxRound = (Math.max(...match.scores.map(s => s.n_round), -1) + 1) || 0; const maxRound = (Math.max(...match.scores.map(s => s.n_round), -1) + 1) || 0;
const newScore = {n_round: maxRound, s1: scoreRed, s2: scoreBlue}; const newScore = {n_round: maxRound, s1: scoreRed, s2: scoreBlue};
toast.promise(sendRequest('updateMatchScore', {matchId: matchId, ...newScore}), toast.promise(sendRequest('updateMatchScore', {matchId: matchId, ...newScore}), getToastMessage("toast.updateMatchScore"));
{
pending: 'Sauvegarde du score...',
success: 'Score sauvegardé !',
error: 'Erreur lors de la sauvegarde du score'
}
);
} }
return () => menuActions.current.saveScore = undefined; return () => menuActions.current.saveScore = undefined;
}, [matchId]) }, [matchId])
@ -443,8 +444,6 @@ function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) {
const score = matchs.find(m => m.id === matchId).scores.find(s => s.n_round === round); const score = matchs.find(m => m.id === matchId).scores.find(s => s.n_round === round);
console.log("Updating score", matchId, round, comb, scoreIn_, score);
let newScore; let newScore;
if (score) { if (score) {
if (comb === 1) if (comb === 1)
@ -460,8 +459,6 @@ function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) {
return return
} }
console.log("Updating score", matchId, newScore);
setLoading(1) setLoading(1)
sendRequest('updateMatchScore', {matchId: matchId, ...newScore}) sendRequest('updateMatchScore', {matchId: matchId, ...newScore})
.finally(() => { .finally(() => {
@ -485,7 +482,7 @@ function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) {
if (end) { if (end) {
if (win(match?.scores) === 0 && match.categorie_ord === -42) { if (win(match?.scores) === 0 && match.categorie_ord === -42) {
toast.error("Impossible de terminer un match nul en tournois."); toast.error(t('score.err1'));
setEnd(false); setEnd(false);
return; return;
} }
@ -529,21 +526,18 @@ function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) {
const o = [...tooltipTriggerList] const o = [...tooltipTriggerList]
o.map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl)) o.map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
const tt = "Score speciaux : <br/>" + const tt = t('score.spe')
"-997 : disqualifié <br/>" +
"-998 : absent <br/>" +
"-999 : forfait"
const maxRound = (match?.scores) ? (Math.max(...match.scores.map(s => s.n_round), -1) + 1) : 0; const maxRound = (match?.scores) ? (Math.max(...match.scores.map(s => s.n_round), -1) + 1) : 0;
return <div ref={tableRef} className="col" style={{position: "relative"}}> return <div ref={tableRef} className="col" style={{position: "relative"}}>
<h6>Scores <FontAwesomeIcon icon={faCircleQuestion} role="button" data-bs-toggle="tooltip" data-bs-placement="right" data-bs-title={tt} <h6>{t('scores')} <FontAwesomeIcon icon={faCircleQuestion} role="button" data-bs-toggle="tooltip" data-bs-placement="right" data-bs-title={tt}
data-bs-html="true"/></h6> data-bs-html="true"/></h6>
<table className="table table-striped"> <table className="table table-striped">
<thead> <thead>
<tr> <tr>
<th style={{textAlign: "center"}} scope="col">Manche</th> <th style={{textAlign: "center"}} scope="col">{t('manche')}</th>
<th style={{textAlign: "center", minWidth: "4em"}} scope="col">Rouge</th> <th style={{textAlign: "center", minWidth: "4em"}} scope="col">{t('rouge')}</th>
<th style={{textAlign: "center", minWidth: "4em"}} scope="col">Bleu</th> <th style={{textAlign: "center", minWidth: "4em"}} scope="col">{t('bleu')}</th>
</tr> </tr>
</thead> </thead>
<tbody className="table-group-divider"> <tbody className="table-group-divider">
@ -568,7 +562,7 @@ function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) {
<div className="form-check" style={{display: "inline-block"}}> <div className="form-check" style={{display: "inline-block"}}>
<input className="form-check-input" type="checkbox" id="checkboxEnd" name="checkboxEnd" checked={end} <input className="form-check-input" type="checkbox" id="checkboxEnd" name="checkboxEnd" checked={end}
onChange={e => setEnd(e.target.checked)}/> onChange={e => setEnd(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkboxEnd">Terminé</label> <label className="form-check-label" htmlFor="checkboxEnd">{t('terminé')}</label>
</div> </div>
</div> </div>
<input ref={inputRef} type="number" className="form-control" style={{position: "absolute", top: 0, left: 0, display: "none"}} min="-999" <input ref={inputRef} type="number" className="form-control" style={{position: "absolute", top: 0, left: 0, display: "none"}} min="-999"

View File

@ -2,12 +2,14 @@ import {useEffect, useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faChevronDown, faChevronUp} from "@fortawesome/free-solid-svg-icons"; import {faChevronDown, faChevronUp} from "@fortawesome/free-solid-svg-icons";
import {usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx"; import {usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx";
import {useTranslation} from "react-i18next";
export function PointPanel({menuActions}) { export function PointPanel({menuActions}) {
const [revers, setRevers] = useState(false) const [revers, setRevers] = useState(false)
const [scoreRouge, setScoreRouge] = useState(0) const [scoreRouge, setScoreRouge] = useState(0)
const [scoreBleu, setScoreBleu] = useState(0) const [scoreBleu, setScoreBleu] = useState(0)
const publicAffDispatch = usePubAffDispatch() const publicAffDispatch = usePubAffDispatch()
const {t} = useTranslation("cm");
menuActions.current.switchSore = () => { menuActions.current.switchSore = () => {
setRevers(!revers) setRevers(!revers)
@ -44,8 +46,8 @@ export function PointPanel({menuActions}) {
{revers && red} {revers && red}
<div className="col row align-items-center"> <div className="col row align-items-center">
<button className="btn btn-danger" onClick={handleReset}>Réinitialiser</button> <button className="btn btn-danger" onClick={handleReset}>{t('réinitialiser')}</button>
<button className="btn btn-success" onClick={handleSave}>Sauvegarder</button> <button className="btn btn-success" onClick={handleSave}>{t('sauvegarder')}</button>
</div> </div>
</div> </div>
} }

View File

@ -14,12 +14,14 @@ import {PointPanel} from "./CMTPoint.jsx";
import {importOBSConfiguration, OBSProvider, useOBS} from "../../../hooks/useOBS.jsx"; import {importOBSConfiguration, OBSProvider, useOBS} from "../../../hooks/useOBS.jsx";
import {SimpleIconsOBS} from "../../../assets/SimpleIconsOBS.ts"; import {SimpleIconsOBS} from "../../../assets/SimpleIconsOBS.ts";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {useTranslation} from "react-i18next";
export function CMTable() { export function CMTable() {
const combDispatch = useCombsDispatch() const combDispatch = useCombsDispatch()
const [catId, setCatId] = useState(-1); const [catId, setCatId] = useState(-1);
const menuActions = useRef({}); const menuActions = useRef({});
const {data} = useRequestWS("getRegister", null) const {data} = useRequestWS("getRegister", null)
const {t} = useTranslation("cm");
useEffect(() => { useEffect(() => {
if (data === null) if (data === null)
@ -33,14 +35,14 @@ export function CMTable() {
<div className="row"> <div className="row">
<div className="col-md-12 col-lg"> <div className="col-md-12 col-lg">
<div className="card mb-3"> <div className="card mb-3">
<div className="card-header">Chronomètre</div> <div className="card-header">{t('chronomètre')}</div>
<div className="card-body"> <div className="card-body">
<ChronoPanel/> <ChronoPanel/>
</div> </div>
</div> </div>
<div className="card mb-3"> <div className="card mb-3">
<div className="card-header">Score</div> <div className="card-header">{t('score')}</div>
<div className="card-body"> <div className="card-body">
<PointPanel menuActions={menuActions}/> <PointPanel menuActions={menuActions}/>
</div> </div>
@ -48,7 +50,7 @@ export function CMTable() {
</div> </div>
<div className="col-md-12 col-xl-6 col-xxl-5"> <div className="col-md-12 col-xl-6 col-xxl-5">
<div className="card mb-3"> <div className="card mb-3">
<div className="card-header">Matches</div> <div className="card-header">{t('matches')}</div>
<div className="card-body"> <div className="card-body">
<CategorieSelect catId={catId} setCatId={setCatId} menuActions={menuActions}/> <CategorieSelect catId={catId} setCatId={setCatId} menuActions={menuActions}/>
</div> </div>
@ -74,6 +76,7 @@ function Menu({menuActions}) {
const {connected, connect, disconnect} = useOBS(); const {connected, connect, disconnect} = useOBS();
const longPress = useRef({time: null, timer: null, button: null}); const longPress = useRef({time: null, timer: null, button: null});
const obsModal = useRef(null); const obsModal = useRef(null);
const {t} = useTranslation("cm");
const externalWindow = useRef(null) const externalWindow = useRef(null)
const containerEl = useRef(document.createElement("div")) const containerEl = useRef(document.createElement("div"))
@ -154,7 +157,7 @@ function Menu({menuActions}) {
connect("ws://" + config.adresse + "/", config.password, config.assets_dir); connect("ws://" + config.adresse + "/", config.password, config.assets_dir);
}) })
.catch(() => { .catch(() => {
toast.error("Aucune configuration OBS trouvée, veuillez en importer une"); toast.error(t('aucuneConfigurationObs'));
}); });
} }
} }
@ -182,21 +185,21 @@ function Menu({menuActions}) {
<div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div> <div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div>
<FontAwesomeIcon icon={faArrowRightArrowLeft} size="xl" style={{color: "#6c757d", cursor: "pointer", marginRight: "0.25em"}} <FontAwesomeIcon icon={faArrowRightArrowLeft} size="xl" style={{color: "#6c757d", cursor: "pointer", marginRight: "0.25em"}}
onClick={handleSwitchScore} data-bs-toggle="tooltip2" data-bs-placement="top" onClick={handleSwitchScore} data-bs-toggle="tooltip2" data-bs-placement="top"
data-bs-title="Inverser la position des combattants sur cette écran"/> data-bs-title={t('ttm.table.inverserLaPosition')}/>
<FontAwesomeIcon icon={SimpleIconsOBS} size="xl" <FontAwesomeIcon icon={SimpleIconsOBS} size="xl"
style={{color: connected ? "#00c700" : "#6c757d", cursor: "pointer"}} style={{color: connected ? "#00c700" : "#6c757d", cursor: "pointer"}}
onMouseDown={() => longPressDown("obs")} onMouseDown={() => longPressDown("obs")}
onMouseUp={() => longPressUp("obs")} onMouseUp={() => longPressUp("obs")}
data-bs-toggle="tooltip2" data-bs-placement="top" data-bs-toggle="tooltip2" data-bs-placement="top"
data-bs-title="Clique court : Charger la configuration et se connecter. Clique long : Configuration de la lice"/> data-bs-title={t('ttm.table.obs')}/>
<div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div> <div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div>
<FontAwesomeIcon icon={faDisplay} size="xl" <FontAwesomeIcon icon={faDisplay} size="xl"
style={{color: showPubAff ? "#00c700" : "#6c757d", cursor: "pointer", marginRight: "0.25em"}} style={{color: showPubAff ? "#00c700" : "#6c757d", cursor: "pointer", marginRight: "0.25em"}}
onClick={handlePubAff} onClick={handlePubAff}
data-bs-toggle="tooltip2" data-bs-placement="top" data-bs-title="Ouvrir l'affichage public"/> data-bs-toggle="tooltip2" data-bs-placement="top" data-bs-title={t('ttm.table.pub_aff')}/>
<FontAwesomeIcon icon={SimpleIconsScore} size="xl" style={{color: showScore ? "#00c700" : "#6c757d", cursor: "pointer"}} <FontAwesomeIcon icon={SimpleIconsScore} size="xl" style={{color: showScore ? "#00c700" : "#6c757d", cursor: "pointer"}}
onClick={handleScore} onClick={handleScore}
data-bs-toggle="tooltip2" data-bs-placement="top" data-bs-title="Afficher les scores sur l'affichage public"/> data-bs-toggle="tooltip2" data-bs-placement="top" data-bs-title={t('ttm.table.pub_score')}/>
</>, document.getElementById("actionMenu"))} </>, document.getElementById("actionMenu"))}
{externalWindow.current && createPortal(<PubAffWindow document={externalWindow.current.document}/>, containerEl.current)} {externalWindow.current && createPortal(<PubAffWindow document={externalWindow.current.document}/>, containerEl.current)}
@ -213,15 +216,15 @@ function Menu({menuActions}) {
<form onSubmit={handleOBSSubmit}> <form onSubmit={handleOBSSubmit}>
<div className="modal-body"> <div className="modal-body">
<div className="input-group mb-3"> <div className="input-group mb-3">
<span className="input-group-text">Préfix des sources</span> <span className="input-group-text">{t('obs.préfixDesSources')}</span>
<span className="input-group-text">sub</span> <span className="input-group-text">sub</span>
<input type="text" className="form-control" placeholder="1" aria-label="" size={1} minLength={1} maxLength={1} <input type="text" className="form-control" placeholder="1" aria-label="" size={1} minLength={1} maxLength={1}
defaultValue={localStorage.getItem("obs_prefix") || "1"} required/> defaultValue={localStorage.getItem("obs_prefix") || "1"} required/>
</div> </div>
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button> <button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Sauvegarde</button> <button type="submit" className="btn btn-primary" data-bs-dismiss="modal">{t('sauvegarde')}</button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -15,7 +15,8 @@ import {CSS} from '@dnd-kit/utilities';
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faTrash} from "@fortawesome/free-solid-svg-icons"; import {faTrash} from "@fortawesome/free-solid-svg-icons";
import {win} from "../../../utils/Tools.js"; import {getToastMessage, win} from "../../../utils/Tools.js";
import {useTranslation} from "react-i18next";
const vite_url = import.meta.env.VITE_URL; const vite_url = import.meta.env.VITE_URL;
@ -169,6 +170,7 @@ function AddComb({groups, setGroups, removeGroup, menuActions}) {
const combDispatch = useCombsDispatch() const combDispatch = useCombsDispatch()
const {dispatch} = useWS() const {dispatch} = useWS()
const [modalId, setModalId] = useState(null) const [modalId, setModalId] = useState(null)
const {t} = useTranslation("cm");
useEffect(() => { useEffect(() => {
const sendRegister = ({data}) => { const sendRegister = ({data}) => {
@ -218,7 +220,7 @@ function AddComb({groups, setGroups, removeGroup, menuActions}) {
return <> return <>
<GroupsList groups={groups} setModalId={setModalId}/> <GroupsList groups={groups} setModalId={setModalId}/>
<button type="button" className="btn btn-primary mt-3 w-100" data-bs-toggle="modal" data-bs-target="#selectCombModal" <button type="button" className="btn btn-primary mt-3 w-100" data-bs-toggle="modal" data-bs-target="#selectCombModal"
disabled={data === null}>Ajouter des combattants disabled={data === null}>{t('ajouterDesCombattants')}
</button> </button>
<div className="modal fade" id="selectCombModal" tabIndex="-1" aria-labelledby="selectCombModalLabel" aria-hidden="true"> <div className="modal fade" id="selectCombModal" tabIndex="-1" aria-labelledby="selectCombModalLabel" aria-hidden="true">
@ -241,6 +243,7 @@ function AddComb({groups, setGroups, removeGroup, menuActions}) {
function GroupsList({groups, setModalId}) { function GroupsList({groups, setModalId}) {
const {getComb} = useCombs(); const {getComb} = useCombs();
const {t} = useTranslation("cm");
const groups2 = groups.map(g => { const groups2 = groups.map(g => {
const comb = getComb(g.id); const comb = getComb(g.id);
@ -261,7 +264,7 @@ function GroupsList({groups, setModalId}) {
return <> return <>
{Object.keys(groups2).map((poule) => ( {Object.keys(groups2).map((poule) => (
<div key={poule} className="mb-3"> <div key={poule} className="mb-3">
<h5>{poule !== '-' ? "Poule: " + poule : "Sans poule"}</h5> <h5>{poule !== '-' ? (t('poule') +" : " + poule) : t('sansPoule')}</h5>
<ol className="list-group list-group-numbered"> <ol className="list-group list-group-numbered">
{groups2[poule].map((comb) => ( {groups2[poule].map((comb) => (
<li key={comb.id} className="list-group-item list-group-item-action d-flex justify-content-between align-items-start" <li key={comb.id} className="list-group-item list-group-item-action d-flex justify-content-between align-items-start"
@ -279,6 +282,7 @@ function GroupsList({groups, setModalId}) {
function GroupModalContent({combId, groups, setGroups, removeGroup}) { function GroupModalContent({combId, groups, setGroups, removeGroup}) {
const inputRef = useRef(null); const inputRef = useRef(null);
const [pouleInput, setPouleInput] = useState(groups.find(g => g.id === combId)?.poule || "") const [pouleInput, setPouleInput] = useState(groups.find(g => g.id === combId)?.poule || "")
const {t} = useTranslation("cm");
useEffect(() => { useEffect(() => {
setPouleInput(groups.find(g => g.id === combId)?.poule || "") setPouleInput(groups.find(g => g.id === combId)?.poule || "")
@ -308,7 +312,7 @@ function GroupModalContent({combId, groups, setGroups, removeGroup}) {
return <> return <>
<div className="modal-header"> <div className="modal-header">
<h5 className="modal-title">Poule pour: <CombName combId={combId}/></h5> <h5 className="modal-title">{t('poulePour')}<CombName combId={combId}/></h5>
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div className="modal-body"> <div className="modal-body">
@ -319,11 +323,11 @@ function GroupModalContent({combId, groups, setGroups, removeGroup}) {
}}/> }}/>
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Close</button> <button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
<button type="button" className="btn btn-primary" onClick={handleClick}>Enregister</button> <button type="button" className="btn btn-primary" onClick={handleClick}>{t('enregister')}</button>
<button type="button" className="btn btn-danger" data-bs-dismiss="modal" onClick={() => { <button type="button" className="btn btn-danger" data-bs-dismiss="modal" onClick={() => {
removeGroup(combId) removeGroup(combId)
}}>Supprimer }}>{t('supprimer')}
</button> </button>
</div> </div>
</> </>
@ -334,6 +338,7 @@ function ListMatch({cat, matches, groups, reducer}) {
const {sendRequest} = useWS(); const {sendRequest} = useWS();
const [type, setType] = useState(1); const [type, setType] = useState(1);
const bthRef = useRef(null); const bthRef = useRef(null);
const {t} = useTranslation("cm");
useEffect(() => { useEffect(() => {
if ((cat.type & type) === 0) if ((cat.type & type) === 0)
@ -369,18 +374,13 @@ function ListMatch({cat, matches, groups, reducer}) {
matchOrderToUpdate: Object.fromEntries(matchOrderToUpdate), matchOrderToUpdate: Object.fromEntries(matchOrderToUpdate),
matchPouleToUpdate: Object.fromEntries(matchPouleToUpdate), matchPouleToUpdate: Object.fromEntries(matchPouleToUpdate),
matchesToRemove: matchesToRemove.map(m => m.id) matchesToRemove: matchesToRemove.map(m => m.id)
}), }), getToastMessage("toast.matchs.create"))
{
pending: 'Création des matchs en cours...',
success: 'Matchs créés avec succès !',
error: 'Erreur lors de la création des matchs',
})
.finally(() => { .finally(() => {
console.log("Finished creating matches"); console.log("Finished creating matches");
}) })
} catch (e) { } catch (e) {
toast.error("Erreur lors de la création des matchs: " + e.message); toast.error(t('erreurLorsDeLaCréationDesMatchs') + e.message);
} }
} }
@ -389,12 +389,12 @@ function ListMatch({cat, matches, groups, reducer}) {
<ul className="nav nav-tabs"> <ul className="nav nav-tabs">
<li className="nav-item"> <li className="nav-item">
<div className={"nav-link" + (type === 1 ? " active" : "")} aria-current={(type === 1 ? " page" : "false")} <div className={"nav-link" + (type === 1 ? " active" : "")} aria-current={(type === 1 ? " page" : "false")}
onClick={_ => setType(1)}>Poule onClick={_ => setType(1)}>{t('poule')}
</div> </div>
</li> </li>
<li className="nav-item"> <li className="nav-item">
<div className={"nav-link" + (type === 2 ? " active" : "")} aria-current={(type === 2 ? " page" : "false")} <div className={"nav-link" + (type === 2 ? " active" : "")} aria-current={(type === 2 ? " page" : "false")}
onClick={_ => setType(2)}>Tournois onClick={_ => setType(2)}>{t('tournois')}
</div> </div>
</li> </li>
</ul> </ul>
@ -402,7 +402,7 @@ function ListMatch({cat, matches, groups, reducer}) {
{type === 1 && <> {type === 1 && <>
<MatchList matches={matches} groups={groups} cat={cat} reducer={reducer}/> <MatchList matches={matches} groups={groups} cat={cat} reducer={reducer}/>
<button className="btn btn-primary float-end" onClick={handleCreatMatch}>Créer les matchs</button> <button className="btn btn-primary float-end" onClick={handleCreatMatch}>{t('créerLesMatchs')}</button>
</>} </>}
{type === 2 && <> {type === 2 && <>
@ -414,21 +414,21 @@ function ListMatch({cat, matches, groups, reducer}) {
<div className="modal-dialog"> <div className="modal-dialog">
<div className="modal-content"> <div className="modal-content">
<div className="modal-header"> <div className="modal-header">
<h4 className="modal-title" id="makeMatchModeLabel">Attention</h4> <h4 className="modal-title" id="makeMatchModeLabel">{t('attention')}</h4>
</div> </div>
<div className="modal-body"> <div className="modal-body">
Il y a déjà des matchs dans cette poule, que voulez-vous faire avec ? {t('msg1')}
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
<div className="btn-group" role="group" aria-label="Basic mixed styles example"> <div className="btn-group" role="group" aria-label="Basic mixed styles example">
<button className="btn btn-primary" data-dismiss="modal" data-bs-dismiss="modal" onClick={() => recalculateMatch(2)}> <button className="btn btn-primary" data-dismiss="modal" data-bs-dismiss="modal" onClick={() => recalculateMatch(2)}>
Tout conserver {t('toutConserver')}
</button> </button>
<button className="btn btn-success" data-bs-dismiss="modal" onClick={() => recalculateMatch(1)}> <button className="btn btn-success" data-bs-dismiss="modal" onClick={() => recalculateMatch(1)}>
Conserver uniquement les matchs terminés {t('conserverUniquementLesMatchsTerminés')}
</button> </button>
<button className="btn btn-danger" data-bs-dismiss="modal" onClick={() => recalculateMatch(0)}> <button className="btn btn-danger" data-bs-dismiss="modal" onClick={() => recalculateMatch(0)}>
Ne rien conserver {t('neRienConserver')}
</button> </button>
</div> </div>
</div> </div>
@ -447,6 +447,7 @@ function MatchList({matches, cat, groups, reducer}) {
const [combSelect, setCombSelect] = useState(0) const [combSelect, setCombSelect] = useState(0)
const [combC1nm, setCombC1nm] = useState(null) const [combC1nm, setCombC1nm] = useState(null)
const [combC2nm, setCombC2nm] = useState(null) const [combC2nm, setCombC2nm] = useState(null)
const {t} = useTranslation("cm");
const liceName = (cat.liceName || "N/A").split(";"); const liceName = (cat.liceName || "N/A").split(";");
const marches2 = matches.filter(m => m.categorie_ord !== -42) const marches2 = matches.filter(m => m.categorie_ord !== -42)
@ -474,7 +475,7 @@ function MatchList({matches, cat, groups, reducer}) {
if (!combC1nm || !combC2nm) if (!combC1nm || !combC2nm)
return; return;
if (combC1nm === combC2nm) { if (combC1nm === combC2nm) {
toast.error("Un combattant ne peut pas s'affronter lui-même !"); toast.error(t('err1'));
return; return;
} }
@ -525,7 +526,7 @@ function MatchList({matches, cat, groups, reducer}) {
} }
if (data.c1 === data.c2) { if (data.c1 === data.c2) {
toast.error("Un combattant ne peut pas s'affronter lui-même !"); toast.error(t('err1'));
onClickVoid() onClickVoid()
return; return;
} }
@ -543,7 +544,7 @@ function MatchList({matches, cat, groups, reducer}) {
if (!match) if (!match)
return; return;
if (!confirm("Ce match a déjà des résultats, êtes-vous sûr de vouloir le supprimer ?")) if (!confirm(t('confirm1')))
return; return;
setLoading(1) setLoading(1)
@ -581,14 +582,14 @@ function MatchList({matches, cat, groups, reducer}) {
<table className="table table-striped"> <table className="table table-striped">
<thead> <thead>
<tr> <tr>
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">N°</th> <th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">{t('no')}</th>
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">Poule</th> <th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">{t('poule')}</th>
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">Zone</th> <th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">{t('zone')}</th>
<th style={{textAlign: "center"}} scope="col"></th> <th style={{textAlign: "center"}} scope="col"></th>
<th style={{textAlign: "center"}} scope="col">Rouge</th> <th style={{textAlign: "center"}} scope="col">{t('rouge')}</th>
<th style={{textAlign: "center"}} scope="col">Blue</th> <th style={{textAlign: "center"}} scope="col">{t('blue')}</th>
<th style={{textAlign: "center"}} scope="col"></th> <th style={{textAlign: "center"}} scope="col"></th>
<th style={{textAlign: "center"}} scope="col">Résultat</th> <th style={{textAlign: "center"}} scope="col">{t('résultat')}</th>
<th style={{textAlign: "center"}} scope="col"></th> <th style={{textAlign: "center"}} scope="col"></th>
<th style={{textAlign: "center"}} scope="col"></th> <th style={{textAlign: "center"}} scope="col"></th>
</tr> </tr>
@ -637,7 +638,7 @@ function MatchList({matches, cat, groups, reducer}) {
<select ref={selectRef} className="form-select" style={{position: "absolute", top: 0, left: 0, display: "none"}} <select ref={selectRef} className="form-select" style={{position: "absolute", top: 0, left: 0, display: "none"}}
value={combSelect} onChange={e => setCombSelect(Number(e.target.value))}> value={combSelect} onChange={e => setCombSelect(Number(e.target.value))}>
<option value={0}>-- Sélectionner un combattant --</option> <option value={0}>{t('--SélectionnerUnCombattant--')}</option>
{combsIDs.map((combId) => ( {combsIDs.map((combId) => (
<option key={combId} value={combId}><CombName combId={combId}/></option> <option key={combId} value={combId}><CombName combId={combId}/></option>
))} ))}
@ -683,6 +684,7 @@ function BuildTree({treeData, matches, groups}) {
const {getComb} = useCombs() const {getComb} = useCombs()
const {sendRequest} = useWS(); const {sendRequest} = useWS();
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {t} = useTranslation("cm");
function parseTree(data_in) { function parseTree(data_in) {
if (data_in?.data == null) if (data_in?.data == null)
@ -774,7 +776,7 @@ function BuildTree({treeData, matches, groups}) {
<DrawGraph root={initTree(treeData)} scrollRef={scrollRef} onMatchClick={onMatchClick} onClickVoid={onClickVoid}/> <DrawGraph root={initTree(treeData)} scrollRef={scrollRef} onMatchClick={onMatchClick} onClickVoid={onClickVoid}/>
<select ref={selectRef} className="form-select" style={{position: "absolute", top: 0, left: 0, display: "none"}} <select ref={selectRef} className="form-select" style={{position: "absolute", top: 0, left: 0, display: "none"}}
value={combSelect} onChange={e => setCombSelect(Number(e.target.value))}> value={combSelect} onChange={e => setCombSelect(Number(e.target.value))}>
<option value={0}>-- Sélectionner un combattant --</option> <option value={0}>{t('--SélectionnerUnCombattant--')}</option>
{combsIDs.map((combId) => ( {combsIDs.map((combId) => (
<option key={combId} value={combId}><CombName combId={combId}/></option> <option key={combId} value={combId}><CombName combId={combId}/></option>
))} ))}

View File

@ -9,12 +9,14 @@ import {CMTable} from "./CMTable.jsx";
import {ThreeDots} from "react-loader-spinner"; import {ThreeDots} from "react-loader-spinner";
import {AxiosError} from "../../../components/AxiosError.jsx"; import {AxiosError} from "../../../components/AxiosError.jsx";
import {useFetch} from "../../../hooks/useFetch.js"; import {useFetch} from "../../../hooks/useFetch.js";
import {useTranslation} from "react-i18next";
const vite_url = import.meta.env.VITE_URL; const vite_url = import.meta.env.VITE_URL;
export default function CompetitionManagerRoot() { export default function CompetitionManagerRoot() {
const {t} = useTranslation("cm");
return <> return <>
<h1>Compétition manager</h1> <h1>{t('compétitionManager')}</h1>
<LoadingProvider> <LoadingProvider>
<Routes> <Routes>
<Route path="/" element={<Home/>}/> <Route path="/" element={<Home/>}/>
@ -40,9 +42,10 @@ function Home() {
} }
function MakeCentralPanel({data, navigate}) { function MakeCentralPanel({data, navigate}) {
const {t} = useTranslation("cm");
return <> return <>
<div className="mb-4"> <div className="mb-4">
<h4>Compétition:</h4> <h4>{t('compétition')}:</h4>
<div className="list-group"> <div className="list-group">
{data.sort((a, b) => new Date(b.date.split('T')[0]) - new Date(a.date.split('T')[0])).map((o) => ( {data.sort((a, b) => new Date(b.date.split('T')[0]) - new Date(a.date.split('T')[0])).map((o) => (
<li className="list-group-item list-group-item-action" key={o.id} <li className="list-group-item list-group-item-action" key={o.id}
@ -77,6 +80,7 @@ function HomeComp() {
function WSStatus({setPerm}) { function WSStatus({setPerm}) {
const [inWait, setInWait] = useState(false) const [inWait, setInWait] = useState(false)
const {isReady, wait_length, welcomeData} = useWS(); const {isReady, wait_length, welcomeData} = useWS();
const {t} = useTranslation("cm");
useEffect(() => { useEffect(() => {
const timer = setInterval(() => { const timer = setInterval(() => {
@ -91,7 +95,7 @@ function WSStatus({setPerm}) {
return <div className="row" style={{marginRight: "inherit"}}> return <div className="row" style={{marginRight: "inherit"}}>
<h2 className="col">{welcomeData.name}</h2> <h2 className="col">{welcomeData.name}</h2>
<div className="col-auto" style={{margin: "auto 0", padding: 0}}>Serveur: <ColoredCircle <div className="col-auto" style={{margin: "auto 0", padding: 0}}>{t('serveur')}: <ColoredCircle
color={isReady ? (inWait ? "#ffad32" : "#00c700") : "#e50000"}/> color={isReady ? (inWait ? "#ffad32" : "#00c700") : "#e50000"}/>
</div> </div>
<div className="col-auto " id="actionMenu" style={{verticalAlign: "center", textAlign: "center", margin: "auto 0", padding: 0}}></div> <div className="col-auto " id="actionMenu" style={{verticalAlign: "center", textAlign: "center", margin: "auto 0", padding: 0}}></div>
@ -100,16 +104,17 @@ function WSStatus({setPerm}) {
function Home2({perm}) { function Home2({perm}) {
const nav = useNavigate(); const nav = useNavigate();
const {t} = useTranslation("cm");
return <div className="row"> return <div className="row">
<h4 className="col-auto" style={{margin: "auto 0"}}>Sélectionne les modes d'affichage</h4> <h4 className="col-auto" style={{margin: "auto 0"}}>{t('sélectionneLesModesDaffichage')}</h4>
<div className="col"> <div className="col">
{perm === "ADMIN" && <> {perm === "ADMIN" && <>
<button className="btn btn-primary" onClick={() => nav("table")}>Secrétariats de lice</button> <button className="btn btn-primary" onClick={() => nav("table")}>{t('secrétariatsDeLice')}</button>
<button className="btn btn-primary ms-3" onClick={() => nav("admin")}>Administration</button> <button className="btn btn-primary ms-3" onClick={() => nav("admin")}>{t('administration')}</button>
</>} </>}
{perm === "TABLE" && <> {perm === "TABLE" && <>
<button className="btn btn-primary" onClick={() => nav("table")}>Secrétariats de lice</button> <button className="btn btn-primary" onClick={() => nav("table")}>{t('secrétariatsDeLice')}</button>
</>} </>}
</div> </div>
</div> </div>

View File

@ -3,6 +3,7 @@ import {usePubAffState} from "../../../hooks/useExternalWindow.jsx";
import {SmartLogoBackgroundMemo} from "../../../components/SmartLogoBackground.jsx"; import {SmartLogoBackgroundMemo} from "../../../components/SmartLogoBackground.jsx";
import {useMemo, useRef} from 'react'; import {useMemo, useRef} from 'react';
import {useWS} from "../../../hooks/useWS.jsx"; import {useWS} from "../../../hooks/useWS.jsx";
import {useTranslation} from "react-i18next";
const vite_url = import.meta.env.VITE_URL; const vite_url = import.meta.env.VITE_URL;
@ -17,6 +18,7 @@ export function PubAffWindow({document}) {
const chronoText = useRef(null) const chronoText = useRef(null)
const state2 = useRef({lastColor: "#ffffff", lastTimeStr: "--:--"}) const state2 = useRef({lastColor: "#ffffff", lastTimeStr: "--:--"})
const state = usePubAffState(); const state = usePubAffState();
const {t} = useTranslation("cm");
document.title = "Affichage Public"; document.title = "Affichage Public";
document.body.className = "bg-dark text-white overflow-hidden"; document.body.className = "bg-dark text-white overflow-hidden";
@ -55,24 +57,24 @@ export function PubAffWindow({document}) {
<div className="row" style={noMP}> <div className="row" style={noMP}>
<div className="col" style={noMP}> <div className="col" style={noMP}>
<CombDisplay combId={state?.c1} background={"red"}> <CombDisplay combId={state?.c1} background={"red"}>
<span className="position-absolute top-0 start-0 translate-middle-y" style={text1Style}>Actuel</span> <span className="position-absolute top-0 start-0 translate-middle-y" style={text1Style}>{t('actuel')}</span>
</CombDisplay> </CombDisplay>
</div> </div>
<div className="col" style={noMP}> <div className="col" style={noMP}>
<CombDisplay combId={state?.c2} background={"blue"}> <CombDisplay combId={state?.c2} background={"blue"}>
<span className="position-absolute bottom-0 start-0 translate-middle-x" style={text2Style}>contre</span> <span className="position-absolute bottom-0 start-0 translate-middle-x" style={text2Style}>{t('contre')}</span>
</CombDisplay> </CombDisplay>
</div> </div>
</div> </div>
<div className="row" style={noMP}> <div className="row" style={noMP}>
<div className="col" style={noMP}> <div className="col" style={noMP}>
<CombDisplay combId={state?.next?.[0]?.c1} background={"red"}> <CombDisplay combId={state?.next?.[0]?.c1} background={"red"}>
<span className="position-absolute top-0 start-0 translate-middle-y" style={text1Style}>Suivant</span> <span className="position-absolute top-0 start-0 translate-middle-y" style={text1Style}>{t('suivant')}</span>
</CombDisplay> </CombDisplay>
</div> </div>
<div className="col" style={noMP}> <div className="col" style={noMP}>
<CombDisplay combId={state?.next?.[0]?.c2} background={"blue"}> <CombDisplay combId={state?.next?.[0]?.c2} background={"blue"}>
<span className="position-absolute bottom-0 start-0 translate-middle-x" style={text2Style}>contre</span> <span className="position-absolute bottom-0 start-0 translate-middle-x" style={text2Style}>{t('contre')}</span>
</CombDisplay> </CombDisplay>
</div> </div>
</div> </div>

View File

@ -3,6 +3,7 @@ import {useEffect, useReducer, useState} from "react";
import {CatList, getCatName} from "../../../utils/Tools.js"; import {CatList, getCatName} from "../../../utils/Tools.js";
import {CombName} from "../../../hooks/useComb.jsx"; import {CombName} from "../../../hooks/useComb.jsx";
import {useWS} from "../../../hooks/useWS.jsx"; import {useWS} from "../../../hooks/useWS.jsx";
import {useTranslation} from "react-i18next";
function SelectReducer(state, action) { function SelectReducer(state, action) {
switch (action.type) { switch (action.type) {
@ -54,6 +55,7 @@ function SelectReducer(state, action) {
export function SelectCombModalContent({data, setGroups}) { export function SelectCombModalContent({data, setGroups}) {
const country = useCountries('fr') const country = useCountries('fr')
const {t} = useTranslation("cm");
const {dispatch} = useWS() const {dispatch} = useWS()
const [dispo, dispoReducer] = useReducer(SelectReducer, {}) const [dispo, dispoReducer] = useReducer(SelectReducer, {})
const [select, selectReducer] = useReducer(SelectReducer, {}) const [select, selectReducer] = useReducer(SelectReducer, {})
@ -153,20 +155,20 @@ export function SelectCombModalContent({data, setGroups}) {
return <> return <>
<div className="modal-header"> <div className="modal-header">
<h1 className="modal-title fs-5" id="CategorieModalLabel">Sélectionner des combatants</h1> <h1 className="modal-title fs-5" id="CategorieModalLabel">{t('select.sélectionnerDesCombatants')}</h1>
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div className="modal-body"> <div className="modal-body">
<div className="d-flex flex-wrap justify-content-around mb-1"> <div className="d-flex flex-wrap justify-content-around mb-1">
<div className="col-md-5"> <div className="col-md-5">
<label htmlFor="input4" className="form-label">Recherche</label> <label htmlFor="input4" className="form-label">{t('select.recherche')}</label>
<input type="text" className="form-control" id="input4" value={search} onChange={(e) => setSearch(e.target.value)}/> <input type="text" className="form-control" id="input4" value={search} onChange={(e) => setSearch(e.target.value)}/>
</div> </div>
<div style={{width: "12em"}}> <div style={{width: "12em"}}>
<label htmlFor="inputState0" className="form-label">Pays</label> <label htmlFor="inputState0" className="form-label">{t('pays')}</label>
<select id="inputState0" className="form-select" value={country_} onChange={(e) => setCountry_(e.target.value)}> <select id="inputState0" className="form-select" value={country_} onChange={(e) => setCountry_(e.target.value)}>
<option value={""}>-- Tous --</option> <option value={""}>{t('--Tous--')}</option>
{country && Object.keys(country).sort((a, b) => { {country && Object.keys(country).sort((a, b) => {
if (a < b) return -1 if (a < b) return -1
if (a > b) return 1 if (a > b) return 1
@ -178,9 +180,9 @@ export function SelectCombModalContent({data, setGroups}) {
</div> </div>
<div> <div>
<label htmlFor="inputState1" className="form-label">Club</label> <label htmlFor="inputState1" className="form-label">{t('club')}</label>
<select id="inputState1" className="form-select" value={club} onChange={(e) => setClub(e.target.value)}> <select id="inputState1" className="form-select" value={club} onChange={(e) => setClub(e.target.value)}>
<option value={""}>-- Tous --</option> <option value={""}>{t('--Tous--')}</option>
{clubList.sort((a, b) => a.localeCompare(b)).map((club) => ( {clubList.sort((a, b) => a.localeCompare(b)).map((club) => (
<option key={club} value={club}>{club}</option>))} <option key={club} value={club}>{club}</option>))}
</select> </select>
@ -189,50 +191,50 @@ export function SelectCombModalContent({data, setGroups}) {
<div className="d-flex flex-wrap justify-content-around mb-1"> <div className="d-flex flex-wrap justify-content-around mb-1">
<div> <div>
<label className="form-label">Genre</label> <label className="form-label">{t('genre')}</label>
<div className="d-flex align-items-center"> <div className="d-flex align-items-center">
<div className="form-check" style={{marginRight: '10px'}}> <div className="form-check" style={{marginRight: '10px'}}>
<input className="form-check-input" type="checkbox" id="gridCheck" checked={gender.H} <input className="form-check-input" type="checkbox" id="gridCheck" checked={gender.H}
onChange={e => setGender((prev) => { onChange={e => setGender((prev) => {
return {...prev, H: e.target.checked} return {...prev, H: e.target.checked}
})}/> })}/>
<label className="form-check-label" htmlFor="gridCheck">H</label> <label className="form-check-label" htmlFor="gridCheck">{t('genre.h')}</label>
</div> </div>
<div className="form-check" style={{marginRight: '10px'}}> <div className="form-check" style={{marginRight: '10px'}}>
<input className="form-check-input" type="checkbox" id="gridCheck2" checked={gender.F} <input className="form-check-input" type="checkbox" id="gridCheck2" checked={gender.F}
onChange={e => setGender((prev) => { onChange={e => setGender((prev) => {
return {...prev, F: e.target.checked} return {...prev, F: e.target.checked}
})}/> })}/>
<label className="form-check-label" htmlFor="gridCheck2">F</label> <label className="form-check-label" htmlFor="gridCheck2">{t('genre.f')}</label>
</div> </div>
<div className="form-check"> <div className="form-check">
<input className="form-check-input" type="checkbox" id="gridCheck3" checked={gender.NA} <input className="form-check-input" type="checkbox" id="gridCheck3" checked={gender.NA}
onChange={e => setGender((prev) => { onChange={e => setGender((prev) => {
return {...prev, NA: e.target.checked} return {...prev, NA: e.target.checked}
})}/> })}/>
<label className="form-check-label" htmlFor="gridCheck3">NA</label> <label className="form-check-label" htmlFor="gridCheck3">{t('genre.na')}</label>
</div> </div>
</div> </div>
</div> </div>
<div> <div>
<label htmlFor="inputState2" className="form-label">Catégorie</label> <label htmlFor="inputState2" className="form-label">{t('catégorie')}</label>
<select id="inputState2" className="form-select" value={cat} onChange={(e) => setCat(Number(e.target.value))}> <select id="inputState2" className="form-select" value={cat} onChange={(e) => setCat(Number(e.target.value))}>
<option value={-1}>-- Tous --</option> <option value={-1}>{t('--Tous--')}</option>
{CatList.map((cat, index) => { {CatList.map((cat, index) => {
return (<option key={index} value={index}>{getCatName(cat)}</option>) return (<option key={index} value={index}>{getCatName(cat)}</option>)
})} })}
</select> </select>
</div> </div>
<div> <div>
<label htmlFor="input5" className="form-label">Poids</label> <label htmlFor="input5" className="form-label">{t('poids')}</label>
<div className="row-cols-sm-auto d-flex align-items-center"> <div className="row-cols-sm-auto d-flex align-items-center">
<div style={{width: "4.25em"}}><input type="number" className="form-control" id="input5" value={weightMin} min="0" <div style={{width: "4.25em"}}><input type="number" className="form-control" id="input5" value={weightMin} min="0"
name="999" name="999"
onChange={e => setWeightMin(Number(e.target.value))}/></div> onChange={e => setWeightMin(Number(e.target.value))}/></div>
<div><span>à</span></div> <div><span>{t('select.à')}</span></div>
<div style={{width: "4.25em"}}><input type="number" className="form-control" value={weightMax} min="0" name="999" <div style={{width: "4.25em"}}><input type="number" className="form-control" value={weightMax} min="0" name="999"
onChange={e => setWeightMax(Number(e.target.value))}/></div> onChange={e => setWeightMax(Number(e.target.value))}/></div>
<div><small>(0 = désactivé)</small></div> <div><small>{t('select.msg1')}</small></div>
</div> </div>
</div> </div>
<button style={{display: "none"}} onClick={event => event.preventDefault()}></button> <button style={{display: "none"}} onClick={event => event.preventDefault()}></button>
@ -240,9 +242,9 @@ export function SelectCombModalContent({data, setGroups}) {
<hr/> <hr/>
<div className="row g-3"> <div className="row g-3">
<div className="col-md"> <div className="col-md">
<div style={{textAlign: "center"}}>Inscrit</div> <div style={{textAlign: "center"}}>{t('inscrit')}</div>
<div className="list-group overflow-y-auto" style={{maxHeight: "50vh"}}> <div className="list-group overflow-y-auto" style={{maxHeight: "50vh"}}>
{dispoFiltered && Object.keys(dispoFiltered).length === 0 && <div>Aucun combattant disponible</div>} {dispoFiltered && Object.keys(dispoFiltered).length === 0 && <div>{t('select.aucunCombattantDisponible')}</div>}
{Object.keys(dispoFiltered).sort((a, b) => nameCompare(data, a, b)).map((id) => ( {Object.keys(dispoFiltered).sort((a, b) => nameCompare(data, a, b)).map((id) => (
<button key={id} type="button" className={"list-group-item list-group-item-action " + (dispoFiltered[id] ? "active" : "")} <button key={id} type="button" className={"list-group-item list-group-item-action " + (dispoFiltered[id] ? "active" : "")}
onClick={() => dispoReducer({type: 'TOGGLE_ID', payload: id})}> onClick={() => dispoReducer({type: 'TOGGLE_ID', payload: id})}>
@ -259,9 +261,9 @@ export function SelectCombModalContent({data, setGroups}) {
</div> </div>
</div> </div>
<div className="col-md"> <div className="col-md">
<div style={{textAlign: "center"}}>Sélectionner</div> <div style={{textAlign: "center"}}>{t('sélectionner')}</div>
<div className="list-group overflow-y-auto" style={{maxHeight: "50vh"}}> <div className="list-group overflow-y-auto" style={{maxHeight: "50vh"}}>
{selectFiltered && Object.keys(selectFiltered).length === 0 && <div>Aucun combattant sélectionné</div>} {selectFiltered && Object.keys(selectFiltered).length === 0 && <div>{t('select.aucunCombattantSélectionné')}</div>}
{Object.keys(selectFiltered).sort((a, b) => nameCompare(data, a, b)).map((id) => ( {Object.keys(selectFiltered).sort((a, b) => nameCompare(data, a, b)).map((id) => (
<button key={id} type="button" <button key={id} type="button"
className={"list-group-item list-group-item-action " + (selectFiltered[id] ? "active" : "")} className={"list-group-item list-group-item-action " + (selectFiltered[id] ? "active" : "")}
@ -273,15 +275,15 @@ export function SelectCombModalContent({data, setGroups}) {
</div> </div>
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button> <button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
<div className="vr"></div> <div className="vr"></div>
<label htmlFor="input6" className="form-label">Poule</label> <label htmlFor="input6" className="form-label">{t('poule')}</label>
<input type="text" className="form-control" id="input6" style={{width: "3em"}} maxLength={1} value={targetGroupe} <input type="text" className="form-control" id="input6" style={{width: "3em"}} maxLength={1} value={targetGroupe}
onChange={(e) => { onChange={(e) => {
if (/^[a-zA-Z0-9]$/.test(e.target.value)) if (/^[a-zA-Z0-9]$/.test(e.target.value))
setTargetGroupe(e.target.value) setTargetGroupe(e.target.value)
}}/> }}/>
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal" onClick={handleSubmit}>Ajouter</button> <button type="submit" className="btn btn-primary" data-bs-dismiss="modal" onClick={handleSubmit}>{t('ajouter')}</button>
</div> </div>
</> </>
} }