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

View File

@ -1,6 +1,7 @@
import React, {useEffect, useRef, useState} from "react";
import {usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx";
import {timePrint} from "../../../utils/Tools.js";
import {useTranslation} from "react-i18next";
export function ChronoPanel() {
const [config, setConfig] = useState({
@ -11,6 +12,7 @@ export function ChronoPanel() {
const chronoText = useRef(null)
const state = useRef({chronoState: 0, countBlink: 20, lastColor: "#000000", lastTimeStr: "00:00"})
const publicAffDispatch = usePubAffDispatch();
const {t} = useTranslation("cm");
const addTime = (time) => setChrono(prev => ({...prev, time: prev.time - time}))
const isRunning = () => chrono.startTime !== 0
@ -111,11 +113,11 @@ export function ChronoPanel() {
onClick={__ => isRunning() ?
setChrono(prev => ({...prev, time: prev.time + Date.now() - prev.startTime, startTime: 0})) :
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={__ => {
setChrono(prev => ({...prev, time: 0, startTime: 0}))
state.current.chronoState = 0
}}>Réinitialiser
}}>{t('réinitialiser')}
</button>
</div>
<div className="row" style={{marginTop: "0.5em"}}>
@ -125,29 +127,29 @@ export function ChronoPanel() {
</div>
<div className="col" style={{margin: "auto 0"}}>
<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)}>+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)}>{t('chrono.+10S')}</button>
</div>
<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)}>+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)}>{t('chrono.+1S')}</button>
</div>
<div className="row" style={{marginTop: "0.5em"}}>
<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)
return;
addTime(parseInt(timeStr, 10) * 1000);
}}>+/- ... s
}}>{t('chrono.+/-...S')}
</button>
</div>
</div>
</div>
<div className="row" style={{marginTop: "0.5em"}}>
<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>
<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>
</div>
@ -155,25 +157,25 @@ export function ChronoPanel() {
<div className="modal-dialog">
<div className="modal-content">
<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>
</div>
<form onSubmit={handleSubmit}>
<div className="modal-body">
<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)}/>
<span className="input-group-text">(mm:ss)</span>
</div>
<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)}/>
<span className="input-group-text">(mm:ss)</span>
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Valider</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">{t('valider')}</button>
</div>
</form>
</div>

View File

@ -6,12 +6,13 @@ import {DrawGraph} from "../../result/DrawGraph.jsx";
import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {useRequestWS, useWS} from "../../../hooks/useWS.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 {faCircleQuestion} from "@fortawesome/free-regular-svg-icons";
import {toast} from "react-toastify";
import "./CMTMatchPanel.css"
import {useOBS} from "../../../hooks/useOBS.jsx";
import {useTranslation} from "react-i18next";
function CupImg() {
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 {dispatch} = useWS();
const {connected, setText} = useOBS();
const {t} = useTranslation("cm");
useEffect(() => {
const categoryListener = ({data}) => {
@ -55,7 +57,7 @@ export function CategorieSelect({catId, setCatId, menuActions}) {
return <>
<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}>
{cats && <option value={-1}></option>}
{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}) {
const [type, setType] = useState(1);
const {t} = useTranslation("cm");
useEffect(() => {
if (!cat)
@ -165,12 +168,12 @@ function ListMatch({cat, matches, trees, menuActions}) {
<ul className="nav nav-tabs">
<li className="nav-item">
<div className={"nav-link" + (type === 1 ? " active" : "")} aria-current={(type === 1 ? " page" : "false")}
onClick={_ => setType(1)}>Poule
onClick={_ => setType(1)}>{t('poule')}
</div>
</li>
<li className="nav-item">
<div className={"nav-link" + (type === 2 ? " active" : "")} aria-current={(type === 2 ? " page" : "false")}
onClick={_ => setType(2)}>Tournois
onClick={_ => setType(2)}>{t('tournois')}
</div>
</li>
</ul>
@ -189,8 +192,9 @@ function ListMatch({cat, matches, trees, menuActions}) {
function MatchList({matches, cat, menuActions}) {
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 {t} = useTranslation("cm");
const liceName = (cat.liceName || "N/A").split(";");
const marches2 = matches.filter(m => m.categorie_ord !== -42)
@ -241,7 +245,7 @@ function MatchList({matches, cat, menuActions}) {
return <>
{liceName.length > 1 &&
<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 => {
setLice(e.target.value);
localStorage.setItem("cm_lice", e.target.value);
@ -259,10 +263,10 @@ function MatchList({matches, cat, menuActions}) {
<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">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">Rouge</th>
<th style={{textAlign: "center"}} scope="col">Blue</th>
<th style={{textAlign: "center"}} scope="col">{t('rouge')}</th>
<th style={{textAlign: "center"}} scope="col">{t('blue')}</th>
<th style={{textAlign: "center"}} scope="col"></th>
</tr>
</thead>
@ -288,7 +292,8 @@ function MatchList({matches, cat, menuActions}) {
</table>
</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>
{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>
}
@ -379,6 +385,7 @@ function ScorePanel({matchId, matchs, match, menuActions}) {
function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) {
const {sendRequest} = useWS()
const setLoading = useLoadingSwitcher()
const {t} = useTranslation("cm");
const [end, setEnd] = useState(match?.end || false)
const [scoreIn, setScoreIn] = useState("")
@ -396,13 +403,7 @@ function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) {
menuActions.current.saveScore = (scoreRed, scoreBlue) => {
const maxRound = (Math.max(...match.scores.map(s => s.n_round), -1) + 1) || 0;
const newScore = {n_round: maxRound, s1: scoreRed, s2: scoreBlue};
toast.promise(sendRequest('updateMatchScore', {matchId: matchId, ...newScore}),
{
pending: 'Sauvegarde du score...',
success: 'Score sauvegardé !',
error: 'Erreur lors de la sauvegarde du score'
}
);
toast.promise(sendRequest('updateMatchScore', {matchId: matchId, ...newScore}), getToastMessage("toast.updateMatchScore"));
}
return () => menuActions.current.saveScore = undefined;
}, [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);
console.log("Updating score", matchId, round, comb, scoreIn_, score);
let newScore;
if (score) {
if (comb === 1)
@ -460,8 +459,6 @@ function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) {
return
}
console.log("Updating score", matchId, newScore);
setLoading(1)
sendRequest('updateMatchScore', {matchId: matchId, ...newScore})
.finally(() => {
@ -485,7 +482,7 @@ function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) {
if (end) {
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);
return;
}
@ -529,21 +526,18 @@ function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) {
const o = [...tooltipTriggerList]
o.map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
const tt = "Score speciaux : <br/>" +
"-997 : disqualifié <br/>" +
"-998 : absent <br/>" +
"-999 : forfait"
const tt = t('score.spe')
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"}}>
<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>
<table className="table table-striped">
<thead>
<tr>
<th style={{textAlign: "center"}} scope="col">Manche</th>
<th style={{textAlign: "center", minWidth: "4em"}} scope="col">Rouge</th>
<th style={{textAlign: "center", minWidth: "4em"}} scope="col">Bleu</th>
<th style={{textAlign: "center"}} scope="col">{t('manche')}</th>
<th style={{textAlign: "center", minWidth: "4em"}} scope="col">{t('rouge')}</th>
<th style={{textAlign: "center", minWidth: "4em"}} scope="col">{t('bleu')}</th>
</tr>
</thead>
<tbody className="table-group-divider">
@ -568,7 +562,7 @@ function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) {
<div className="form-check" style={{display: "inline-block"}}>
<input className="form-check-input" type="checkbox" id="checkboxEnd" name="checkboxEnd" checked={end}
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>
<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 {faChevronDown, faChevronUp} from "@fortawesome/free-solid-svg-icons";
import {usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx";
import {useTranslation} from "react-i18next";
export function PointPanel({menuActions}) {
const [revers, setRevers] = useState(false)
const [scoreRouge, setScoreRouge] = useState(0)
const [scoreBleu, setScoreBleu] = useState(0)
const publicAffDispatch = usePubAffDispatch()
const {t} = useTranslation("cm");
menuActions.current.switchSore = () => {
setRevers(!revers)
@ -44,8 +46,8 @@ export function PointPanel({menuActions}) {
{revers && red}
<div className="col row align-items-center">
<button className="btn btn-danger" onClick={handleReset}>Réinitialiser</button>
<button className="btn btn-success" onClick={handleSave}>Sauvegarder</button>
<button className="btn btn-danger" onClick={handleReset}>{t('réinitialiser')}</button>
<button className="btn btn-success" onClick={handleSave}>{t('sauvegarder')}</button>
</div>
</div>
}

View File

@ -14,12 +14,14 @@ import {PointPanel} from "./CMTPoint.jsx";
import {importOBSConfiguration, OBSProvider, useOBS} from "../../../hooks/useOBS.jsx";
import {SimpleIconsOBS} from "../../../assets/SimpleIconsOBS.ts";
import {toast} from "react-toastify";
import {useTranslation} from "react-i18next";
export function CMTable() {
const combDispatch = useCombsDispatch()
const [catId, setCatId] = useState(-1);
const menuActions = useRef({});
const {data} = useRequestWS("getRegister", null)
const {t} = useTranslation("cm");
useEffect(() => {
if (data === null)
@ -33,14 +35,14 @@ export function CMTable() {
<div className="row">
<div className="col-md-12 col-lg">
<div className="card mb-3">
<div className="card-header">Chronomètre</div>
<div className="card-header">{t('chronomètre')}</div>
<div className="card-body">
<ChronoPanel/>
</div>
</div>
<div className="card mb-3">
<div className="card-header">Score</div>
<div className="card-header">{t('score')}</div>
<div className="card-body">
<PointPanel menuActions={menuActions}/>
</div>
@ -48,7 +50,7 @@ export function CMTable() {
</div>
<div className="col-md-12 col-xl-6 col-xxl-5">
<div className="card mb-3">
<div className="card-header">Matches</div>
<div className="card-header">{t('matches')}</div>
<div className="card-body">
<CategorieSelect catId={catId} setCatId={setCatId} menuActions={menuActions}/>
</div>
@ -74,6 +76,7 @@ function Menu({menuActions}) {
const {connected, connect, disconnect} = useOBS();
const longPress = useRef({time: null, timer: null, button: null});
const obsModal = useRef(null);
const {t} = useTranslation("cm");
const externalWindow = useRef(null)
const containerEl = useRef(document.createElement("div"))
@ -154,7 +157,7 @@ function Menu({menuActions}) {
connect("ws://" + config.adresse + "/", config.password, config.assets_dir);
})
.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>
<FontAwesomeIcon icon={faArrowRightArrowLeft} size="xl" style={{color: "#6c757d", cursor: "pointer", marginRight: "0.25em"}}
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"
style={{color: connected ? "#00c700" : "#6c757d", cursor: "pointer"}}
onMouseDown={() => longPressDown("obs")}
onMouseUp={() => longPressUp("obs")}
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>
<FontAwesomeIcon icon={faDisplay} size="xl"
style={{color: showPubAff ? "#00c700" : "#6c757d", cursor: "pointer", marginRight: "0.25em"}}
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"}}
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"))}
{externalWindow.current && createPortal(<PubAffWindow document={externalWindow.current.document}/>, containerEl.current)}
@ -213,15 +216,15 @@ function Menu({menuActions}) {
<form onSubmit={handleOBSSubmit}>
<div className="modal-body">
<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>
<input type="text" className="form-control" placeholder="1" aria-label="" size={1} minLength={1} maxLength={1}
defaultValue={localStorage.getItem("obs_prefix") || "1"} required/>
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Sauvegarde</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">{t('sauvegarde')}</button>
</div>
</form>
</div>

View File

@ -15,7 +15,8 @@ import {CSS} from '@dnd-kit/utilities';
import {toast} from "react-toastify";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
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;
@ -169,6 +170,7 @@ function AddComb({groups, setGroups, removeGroup, menuActions}) {
const combDispatch = useCombsDispatch()
const {dispatch} = useWS()
const [modalId, setModalId] = useState(null)
const {t} = useTranslation("cm");
useEffect(() => {
const sendRegister = ({data}) => {
@ -218,7 +220,7 @@ function AddComb({groups, setGroups, removeGroup, menuActions}) {
return <>
<GroupsList groups={groups} setModalId={setModalId}/>
<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>
<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}) {
const {getComb} = useCombs();
const {t} = useTranslation("cm");
const groups2 = groups.map(g => {
const comb = getComb(g.id);
@ -261,7 +264,7 @@ function GroupsList({groups, setModalId}) {
return <>
{Object.keys(groups2).map((poule) => (
<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">
{groups2[poule].map((comb) => (
<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}) {
const inputRef = useRef(null);
const [pouleInput, setPouleInput] = useState(groups.find(g => g.id === combId)?.poule || "")
const {t} = useTranslation("cm");
useEffect(() => {
setPouleInput(groups.find(g => g.id === combId)?.poule || "")
@ -308,7 +312,7 @@ function GroupModalContent({combId, groups, setGroups, removeGroup}) {
return <>
<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>
</div>
<div className="modal-body">
@ -319,11 +323,11 @@ function GroupModalContent({combId, groups, setGroups, removeGroup}) {
}}/>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" className="btn btn-primary" onClick={handleClick}>Enregister</button>
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</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={() => {
removeGroup(combId)
}}>Supprimer
}}>{t('supprimer')}
</button>
</div>
</>
@ -334,6 +338,7 @@ function ListMatch({cat, matches, groups, reducer}) {
const {sendRequest} = useWS();
const [type, setType] = useState(1);
const bthRef = useRef(null);
const {t} = useTranslation("cm");
useEffect(() => {
if ((cat.type & type) === 0)
@ -369,18 +374,13 @@ function ListMatch({cat, matches, groups, reducer}) {
matchOrderToUpdate: Object.fromEntries(matchOrderToUpdate),
matchPouleToUpdate: Object.fromEntries(matchPouleToUpdate),
matchesToRemove: matchesToRemove.map(m => m.id)
}),
{
pending: 'Création des matchs en cours...',
success: 'Matchs créés avec succès !',
error: 'Erreur lors de la création des matchs',
})
}), getToastMessage("toast.matchs.create"))
.finally(() => {
console.log("Finished creating matches");
})
} 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">
<li className="nav-item">
<div className={"nav-link" + (type === 1 ? " active" : "")} aria-current={(type === 1 ? " page" : "false")}
onClick={_ => setType(1)}>Poule
onClick={_ => setType(1)}>{t('poule')}
</div>
</li>
<li className="nav-item">
<div className={"nav-link" + (type === 2 ? " active" : "")} aria-current={(type === 2 ? " page" : "false")}
onClick={_ => setType(2)}>Tournois
onClick={_ => setType(2)}>{t('tournois')}
</div>
</li>
</ul>
@ -402,7 +402,7 @@ function ListMatch({cat, matches, groups, reducer}) {
{type === 1 && <>
<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 && <>
@ -414,21 +414,21 @@ function ListMatch({cat, matches, groups, reducer}) {
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<h4 className="modal-title" id="makeMatchModeLabel">Attention</h4>
<h4 className="modal-title" id="makeMatchModeLabel">{t('attention')}</h4>
</div>
<div className="modal-body">
Il y a déjà des matchs dans cette poule, que voulez-vous faire avec ?
{t('msg1')}
</div>
<div className="modal-footer">
<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)}>
Tout conserver
{t('toutConserver')}
</button>
<button className="btn btn-success" data-bs-dismiss="modal" onClick={() => recalculateMatch(1)}>
Conserver uniquement les matchs terminés
{t('conserverUniquementLesMatchsTerminés')}
</button>
<button className="btn btn-danger" data-bs-dismiss="modal" onClick={() => recalculateMatch(0)}>
Ne rien conserver
{t('neRienConserver')}
</button>
</div>
</div>
@ -447,6 +447,7 @@ function MatchList({matches, cat, groups, reducer}) {
const [combSelect, setCombSelect] = useState(0)
const [combC1nm, setCombC1nm] = useState(null)
const [combC2nm, setCombC2nm] = useState(null)
const {t} = useTranslation("cm");
const liceName = (cat.liceName || "N/A").split(";");
const marches2 = matches.filter(m => m.categorie_ord !== -42)
@ -474,7 +475,7 @@ function MatchList({matches, cat, groups, reducer}) {
if (!combC1nm || !combC2nm)
return;
if (combC1nm === combC2nm) {
toast.error("Un combattant ne peut pas s'affronter lui-même !");
toast.error(t('err1'));
return;
}
@ -525,7 +526,7 @@ function MatchList({matches, cat, groups, reducer}) {
}
if (data.c1 === data.c2) {
toast.error("Un combattant ne peut pas s'affronter lui-même !");
toast.error(t('err1'));
onClickVoid()
return;
}
@ -543,7 +544,7 @@ function MatchList({matches, cat, groups, reducer}) {
if (!match)
return;
if (!confirm("Ce match a déjà des résultats, êtes-vous sûr de vouloir le supprimer ?"))
if (!confirm(t('confirm1')))
return;
setLoading(1)
@ -581,14 +582,14 @@ function MatchList({matches, cat, groups, reducer}) {
<table className="table table-striped">
<thead>
<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">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('no')}</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">{t('zone')}</th>
<th style={{textAlign: "center"}} scope="col"></th>
<th style={{textAlign: "center"}} scope="col">Rouge</th>
<th style={{textAlign: "center"}} scope="col">Blue</th>
<th style={{textAlign: "center"}} scope="col">{t('rouge')}</th>
<th style={{textAlign: "center"}} scope="col">{t('blue')}</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>
</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"}}
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) => (
<option key={combId} value={combId}><CombName combId={combId}/></option>
))}
@ -683,6 +684,7 @@ function BuildTree({treeData, matches, groups}) {
const {getComb} = useCombs()
const {sendRequest} = useWS();
const setLoading = useLoadingSwitcher()
const {t} = useTranslation("cm");
function parseTree(data_in) {
if (data_in?.data == null)
@ -774,7 +776,7 @@ function BuildTree({treeData, matches, groups}) {
<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"}}
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) => (
<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 {AxiosError} from "../../../components/AxiosError.jsx";
import {useFetch} from "../../../hooks/useFetch.js";
import {useTranslation} from "react-i18next";
const vite_url = import.meta.env.VITE_URL;
export default function CompetitionManagerRoot() {
const {t} = useTranslation("cm");
return <>
<h1>Compétition manager</h1>
<h1>{t('compétitionManager')}</h1>
<LoadingProvider>
<Routes>
<Route path="/" element={<Home/>}/>
@ -40,9 +42,10 @@ function Home() {
}
function MakeCentralPanel({data, navigate}) {
const {t} = useTranslation("cm");
return <>
<div className="mb-4">
<h4>Compétition:</h4>
<h4>{t('compétition')}:</h4>
<div className="list-group">
{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}
@ -77,6 +80,7 @@ function HomeComp() {
function WSStatus({setPerm}) {
const [inWait, setInWait] = useState(false)
const {isReady, wait_length, welcomeData} = useWS();
const {t} = useTranslation("cm");
useEffect(() => {
const timer = setInterval(() => {
@ -91,7 +95,7 @@ function WSStatus({setPerm}) {
return <div className="row" style={{marginRight: "inherit"}}>
<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"}/>
</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}) {
const nav = useNavigate();
const {t} = useTranslation("cm");
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">
{perm === "ADMIN" && <>
<button className="btn btn-primary" onClick={() => nav("table")}>Secrétariats de lice</button>
<button className="btn btn-primary ms-3" onClick={() => nav("admin")}>Administration</button>
<button className="btn btn-primary" onClick={() => nav("table")}>{t('secrétariatsDeLice')}</button>
<button className="btn btn-primary ms-3" onClick={() => nav("admin")}>{t('administration')}</button>
</>}
{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>

View File

@ -3,6 +3,7 @@ import {usePubAffState} from "../../../hooks/useExternalWindow.jsx";
import {SmartLogoBackgroundMemo} from "../../../components/SmartLogoBackground.jsx";
import {useMemo, useRef} from 'react';
import {useWS} from "../../../hooks/useWS.jsx";
import {useTranslation} from "react-i18next";
const vite_url = import.meta.env.VITE_URL;
@ -17,6 +18,7 @@ export function PubAffWindow({document}) {
const chronoText = useRef(null)
const state2 = useRef({lastColor: "#ffffff", lastTimeStr: "--:--"})
const state = usePubAffState();
const {t} = useTranslation("cm");
document.title = "Affichage Public";
document.body.className = "bg-dark text-white overflow-hidden";
@ -55,24 +57,24 @@ export function PubAffWindow({document}) {
<div className="row" style={noMP}>
<div className="col" style={noMP}>
<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>
</div>
<div className="col" style={noMP}>
<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>
</div>
</div>
<div className="row" style={noMP}>
<div className="col" style={noMP}>
<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>
</div>
<div className="col" style={noMP}>
<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>
</div>
</div>

View File

@ -3,6 +3,7 @@ import {useEffect, useReducer, useState} from "react";
import {CatList, getCatName} from "../../../utils/Tools.js";
import {CombName} from "../../../hooks/useComb.jsx";
import {useWS} from "../../../hooks/useWS.jsx";
import {useTranslation} from "react-i18next";
function SelectReducer(state, action) {
switch (action.type) {
@ -54,6 +55,7 @@ function SelectReducer(state, action) {
export function SelectCombModalContent({data, setGroups}) {
const country = useCountries('fr')
const {t} = useTranslation("cm");
const {dispatch} = useWS()
const [dispo, dispoReducer] = useReducer(SelectReducer, {})
const [select, selectReducer] = useReducer(SelectReducer, {})
@ -153,20 +155,20 @@ export function SelectCombModalContent({data, setGroups}) {
return <>
<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>
</div>
<div className="modal-body">
<div className="d-flex flex-wrap justify-content-around mb-1">
<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)}/>
</div>
<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)}>
<option value={""}>-- Tous --</option>
<option value={""}>{t('--Tous--')}</option>
{country && Object.keys(country).sort((a, b) => {
if (a < b) return -1
if (a > b) return 1
@ -178,9 +180,9 @@ export function SelectCombModalContent({data, setGroups}) {
</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)}>
<option value={""}>-- Tous --</option>
<option value={""}>{t('--Tous--')}</option>
{clubList.sort((a, b) => a.localeCompare(b)).map((club) => (
<option key={club} value={club}>{club}</option>))}
</select>
@ -189,50 +191,50 @@ export function SelectCombModalContent({data, setGroups}) {
<div className="d-flex flex-wrap justify-content-around mb-1">
<div>
<label className="form-label">Genre</label>
<label className="form-label">{t('genre')}</label>
<div className="d-flex align-items-center">
<div className="form-check" style={{marginRight: '10px'}}>
<input className="form-check-input" type="checkbox" id="gridCheck" checked={gender.H}
onChange={e => setGender((prev) => {
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 className="form-check" style={{marginRight: '10px'}}>
<input className="form-check-input" type="checkbox" id="gridCheck2" checked={gender.F}
onChange={e => setGender((prev) => {
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 className="form-check">
<input className="form-check-input" type="checkbox" id="gridCheck3" checked={gender.NA}
onChange={e => setGender((prev) => {
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>
<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))}>
<option value={-1}>-- Tous --</option>
<option value={-1}>{t('--Tous--')}</option>
{CatList.map((cat, index) => {
return (<option key={index} value={index}>{getCatName(cat)}</option>)
})}
</select>
</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 style={{width: "4.25em"}}><input type="number" className="form-control" id="input5" value={weightMin} min="0"
name="999"
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"
onChange={e => setWeightMax(Number(e.target.value))}/></div>
<div><small>(0 = désactivé)</small></div>
<div><small>{t('select.msg1')}</small></div>
</div>
</div>
<button style={{display: "none"}} onClick={event => event.preventDefault()}></button>
@ -240,9 +242,9 @@ export function SelectCombModalContent({data, setGroups}) {
<hr/>
<div className="row g-3">
<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"}}>
{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) => (
<button key={id} type="button" className={"list-group-item list-group-item-action " + (dispoFiltered[id] ? "active" : "")}
onClick={() => dispoReducer({type: 'TOGGLE_ID', payload: id})}>
@ -259,9 +261,9 @@ export function SelectCombModalContent({data, setGroups}) {
</div>
</div>
<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"}}>
{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) => (
<button key={id} type="button"
className={"list-group-item list-group-item-action " + (selectFiltered[id] ? "active" : "")}
@ -273,15 +275,15 @@ export function SelectCombModalContent({data, setGroups}) {
</div>
</div>
<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>
<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}
onChange={(e) => {
if (/^[a-zA-Z0-9]$/.test(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>
</>
}