i18n #99

Merged
Thibaut merged 5 commits from i18n into master 2026-01-16 12:18:54 +00:00
51 changed files with 1818 additions and 1090 deletions
Showing only changes of commit 9170157dbb - Show all commits

View File

@ -18,12 +18,16 @@
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"axios": "^1.6.5", "axios": "^1.6.5",
"browser-image-compression": "^2.0.2", "browser-image-compression": "^2.0.2",
"i18next": "^25.7.4",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"obs-websocket-js": "^5.0.7", "obs-websocket-js": "^5.0.7",
"proj4": "^2.11.0", "proj4": "^2.11.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-i18next": "^16.5.2",
"react-is": "^19.0.0", "react-is": "^19.0.0",
"react-leaflet": "^4.2.1", "react-leaflet": "^4.2.1",
"react-loader-spinner": "^6.1.6", "react-loader-spinner": "^6.1.6",
@ -345,12 +349,9 @@
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.26.7", "version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==", "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
@ -1871,6 +1872,14 @@
"node": ">=0.8" "node": ">=0.8"
} }
}, },
"node_modules/cross-fetch": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
"integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==",
"dependencies": {
"node-fetch": "^2.6.12"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -2990,6 +2999,60 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
"dependencies": {
"void-elements": "3.1.0"
}
},
"node_modules/i18next": {
"version": "25.7.4",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.4.tgz",
"integrity": "sha512-hRkpEblXXcXSNbw8mBNq9042OEetgyB/ahc/X17uV/khPwzV+uB8RHceHh3qavyrkPJvmXFKXME2Sy1E0KjAfw==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"dependencies": {
"@babel/runtime": "^7.28.4"
},
"peerDependencies": {
"typescript": "^5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/i18next-browser-languagedetector": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz",
"integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==",
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/i18next-http-backend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz",
"integrity": "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==",
"dependencies": {
"cross-fetch": "4.0.0"
}
},
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.3.0", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",
@ -3634,6 +3697,25 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true "dev": true
}, },
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.14", "version": "2.0.14",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
@ -4028,6 +4110,32 @@
"react": "^18.2.0" "react": "^18.2.0"
} }
}, },
"node_modules/react-i18next": {
"version": "16.5.2",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.2.tgz",
"integrity": "sha512-GG/SBVxx9dvrO1uCs8VYdKfOP8NEBUhNP+2VDQLCifRJ8DL1qPq296k2ACNGyZMDe7iyIlz/LMJTQOs8HXSRvw==",
"dependencies": {
"@babel/runtime": "^7.28.4",
"html-parse-stringify": "^3.0.1",
"use-sync-external-store": "^1.6.0"
},
"peerDependencies": {
"i18next": ">= 25.6.2",
"react": ">= 16.8.0",
"typescript": "^5"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "19.0.0", "version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz",
@ -4221,11 +4329,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/regexp.prototype.flags": { "node_modules/regexp.prototype.flags": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
@ -4704,6 +4807,11 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/tslib": { "node_modules/tslib": {
"version": "2.5.0", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
@ -4852,6 +4960,14 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -4938,6 +5054,28 @@
} }
} }
}, },
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@ -20,12 +20,16 @@
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"axios": "^1.6.5", "axios": "^1.6.5",
"browser-image-compression": "^2.0.2", "browser-image-compression": "^2.0.2",
"i18next": "^25.7.4",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"obs-websocket-js": "^5.0.7", "obs-websocket-js": "^5.0.7",
"proj4": "^2.11.0", "proj4": "^2.11.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-i18next": "^16.5.2",
"react-is": "^19.0.0", "react-is": "^19.0.0",
"react-leaflet": "^4.2.1", "react-leaflet": "^4.2.1",
"react-loader-spinner": "^6.1.6", "react-loader-spinner": "^6.1.6",

View File

@ -0,0 +1,419 @@
{
"(optionnelle)": "(optional)",
"---SansClub---": "--- no club ---",
"---ToutLesClubs---": "--- all clubs ---",
"---ToutLesPays---": "--- all countries ---",
"---TouteLesCatégories---": "--- all categories ---",
"--NonLicencier--": "-- Unlicensed --",
"activer": "Activate",
"admin": "Administration",
"adresse": "Address",
"adresseAdministrative": "Administrative address",
"aff.ancienNom": "Former name: {{name}}",
"aff.byMembreSim": "By similar member",
"aff.byNewMenbre": "By new member",
"aff.byNoLicence": "By licence number",
"aff.info1": "This club has already been affiliated (affiliation no. {{no}})",
"aff.membreNo": "Member no. {{no}}",
"aff.nomDuClub": "Club name",
"aff.raisonDuRefus": "Reason for refusal",
"aff.raisonDuRefus.msg": "Please indicate the reason for refusal",
"aff.refusConfirm": "Are you sure you want to refuse this request?",
"aff.refuserLaDemande": "Refuse the request",
"aff.refuserLaDemande.detail": "Are you sure you want to refuse this request?",
"aff.submit.error1": "Please enter a valid licence number for member {{id}}",
"aff.submit.error2": "Please enter a valid licence number for member {{id}}",
"aff.submit.error3": "Please enter a valid email for member {{id}}",
"aff.toast.accept.error": "Failed to accept affiliation",
"aff.toast.accept.pending": "Accepting affiliation in progress",
"aff.toast.accept.success": "Affiliation accepted successfully 🎉",
"aff.toast.del.error": "Failed to delete affiliation request",
"aff.toast.del.pending": "Deleting affiliation request in progress",
"aff.toast.del.success": "Affiliation request deleted successfully 🎉",
"aff.toast.del2.error": "Failed to delete affiliation",
"aff.toast.del2.pending": "Deleting affiliation in progress",
"aff.toast.del2.success": "Affiliation deleted successfully 🎉",
"aff.toast.save.error": "Failed to save affiliation request",
"aff.toast.save.pending": "Saving affiliation request in progress",
"aff.toast.save.success": "Affiliation request saved successfully 🎉",
"aff.toast.save2.error": "Failed to save affiliation",
"aff.toast.save2.pending": "Saving affiliation in progress",
"aff.toast.save2.success": "Affiliation saved successfully 🎉",
"aff_req.appuyerSurRechercher": "Press search to complete",
"aff_req.association": "The association",
"aff_req.button.cancel": "Cancel my request",
"aff_req.button.confirm": "Confirm my affiliation request",
"aff_req.button.save": "Save changes",
"aff_req.denomination": "Denomination",
"aff_req.disposeLicence": "Already has a licence",
"aff_req.error1": "The role of member {{i}} is required",
"aff_req.error2": "The SIRET/RNA format is invalid",
"aff_req.nomDeLassociation": "Association name",
"aff_req.text1": "Affiliation is annual and valid for one sports season: from September 1st to August 31st of the following year.",
"aff_req.text2": "To affiliate, a sports association must meet the following conditions:",
"aff_req.text2.li": [
"Have its headquarters in France or the Principality of Monaco",
"Be constituted in accordance with Chapter 1 of Title II of Book 1 of the Sports Code",
"Pursue a social purpose that falls within the definition of Article 1 of the Federation's statutes",
"Have statutes compatible with the principles of organization and operation of the Federation",
"Ensure freedom of opinion and respect for the rights of the defense within it, and prohibit any discrimination",
"Comply with the rules of supervision, hygiene, and safety established by the Federation's regulations"
],
"aff_req.text3": "After your request is validated, you will receive a temporary ID and password to access your FFSAF space",
"aff_req.text4": "Note that to finalize your affiliation, you will need to:",
"aff_req.text4.li1": "Have at least three licensed members, including the president",
"aff_req.text4.li2": "Have paid the fees provided for by the federal regulations",
"aff_req.text5": "You can later add publicly visible addresses for your training locations",
"aff_req.text6": "Leave blank to make no changes. (If a coat of arms has already been sent with this request, it will be used; otherwise, we will use the one from the previous affiliation)",
"aff_req.text7": "Affiliation request sent successfully",
"aff_req.text8": "Once your request is validated, you will receive a temporary ID and password to access your FFSAF space",
"aff_req.toast.undo.error": "Failed to cancel affiliation request",
"aff_req.toast.undo.pending": "Cancelling affiliation request in progress",
"aff_req.toast.undo.success": "Affiliation request cancelled successfully 🎉",
"afficherLétatDesAffiliation": "Display affiliation status",
"affiliation": "Affiliation",
"affiliationNo": "Affiliation no. {{no}}",
"ajouterUnClub": "Add a club",
"ajouterUnMembre": "Add a member",
"all_season": "--- all seasons ---",
"aucunMembreSélectionné": "No member selected",
"back": "« back",
"blason": "Coat of arms",
"bureau": "Board",
"button.accepter": "Accept",
"button.ajouter": "Add",
"button.annuler": "Cancel",
"button.appliquer": "Apply",
"button.confirmer": "Confirm",
"button.créer": "Create",
"button.enregistrer": "Save",
"button.fermer": "Close",
"button.refuser": "Refuse",
"button.suivant": "Next",
"button.supprimer": "Delete",
"cat.benjamin": "Benjamin",
"cat.cadet": "Cadet",
"cat.catégorieInconnue": "Unknown category",
"cat.junior": "Junior",
"cat.miniPoussin": "Mini Chick",
"cat.minime": "Minime",
"cat.poussin": "Chick",
"cat.senior1": "Senior 1",
"cat.senior2": "Senior 2",
"cat.superMini": "Super Mini",
"cat.vétéran1": "Veteran 1",
"cat.vétéran2": "Veteran 2",
"catégorie": "Category",
"certificatMédical": "Medical certificate",
"chargement...": "Loading...",
"chargerLexcel": "Upload Excel",
"chargerLexcel.msg": "Please use the file above as a base; do not rename the columns or modify the licence numbers.",
"choisir...": "Choose...",
"club.aff_renew.msg": "Please select 0 to 3 board members to fill out the pre-request. (If a non-board member will become one next year, you can enter them in the next step)",
"club.change.status": "To modify the above information, please contact FFSAF by email.",
"club.contact.tt": {
"AUTRE": "Other club contact",
"COURRIEL": "Club email address<br>Example: contact@ffsaf.fr",
"FACEBOOK": "Club Facebook page starting with 'https://www.facebook.com/'<br>Example: https://www.facebook.com/ffmsf",
"INSTAGRAM": "Club Instagram account starting with 'https://www.instagram.com/'<br>Example: https://www.instagram.com/ff_msf",
"SITE": "Club website with or without 'https://'<br>Example: ffsaf.fr<br>Or https://ffsaf.fr",
"TELEPHONE": "Club phone number<br>Example: 06 12 13 78 55"
},
"club.toast.aff.error": "Failed to load affiliations",
"club.toast.aff.pending": "Loading affiliations in progress",
"club.toast.aff.success": "Affiliations loaded successfully 🎉",
"club.toast.del.error": "Failed to delete club",
"club.toast.del.pending": "Deleting club in progress",
"club.toast.del.success": "Club deleted successfully 🎉",
"club.toast.new.error": "Failed to create club",
"club.toast.new.pending": "Creating club in progress",
"club.toast.new.success": "Club created successfully 🎉",
"club.toast.save.error": "Failed to save club",
"club.toast.save.pending": "Saving club in progress",
"club.toast.save.success": "Club saved successfully 🎉",
"clubExterne": "External club",
"club_one": "Club",
"club_other": "Clubs",
"club_zero": "No club",
"comp_manage": "Competitions Manager",
"competition_one": "Competition",
"competition_other": "Competitions",
"compte": "Account",
"conserverLancienEmail": "Keep the old email",
"contactAdministratif": "Administrative contact",
"contactInterne": "Internal contact",
"contact_one": "Contact",
"contact_other": "Contacts",
"dateDeNaissance": "Date of birth",
"days": [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"
],
"de": "of",
"demandeDaffiliationEnCours": "Affiliation request in progress",
"demandeDeLicence": "Licence request",
"demander": "Request",
"dlAff": "Download affiliation certificate",
"donnéesAdministratives": "Administrative data",
"définirLidDuCompte": "Set account ID",
"editionDeL'affiliation": "Editing affiliation",
"editionDeLaDemande": "Editing request",
"editionDeLaLicence": "Editing licence",
"editionDeLaSéléction": "Editing selection",
"editionDeLadresse": "Editing address",
"email": "Email",
"en": "in",
"enAttente": "Pending",
"erreurDePaiement": "Payment error😕",
"erreurDePaiement.detail": "Error message:",
"erreurDePaiement.msg": "An error occurred while processing your payment. Please try again later.",
"espaceAdministration": "Administration space",
"faitPar": "Done by",
"femme": "Female",
"filtre": "Filter",
"genre": "Gender",
"gestionGroupée": "Group management",
"gradeDarbitrage": "Referee grade",
"home": {
"header1": "For licensed members",
"header2": "For clubs",
"text1": "Here you will find all your information as well as the status of your registration with the federation. You can also download your registration certificate, sign up for competitions, and view your results, provided that the organizing club has entered them.<br/><br/>During your first registration, you will receive an email containing your login details; this email will be sent once your licence is validated by the secretariat.",
"text2": "This is where you can take out federal licences for your members, request or renew your affiliation, enter your schedules, training locations, and social networks, which will then be displayed on the ffsaf.fr website.<br/>You will also have the possibility to publish registration forms for your competitions and record the results.<br/><br/>Not yet affiliated with the federation? Click <1>here</1> to make your first request.",
"welcome_message": "Welcome to the intranet of the Fédération France Soft Armored Fighting"
},
"homme": "Male",
"horairesD'entraînements": "Training schedules",
"information": "Information",
"keepEmpty": "Leave blank to make no changes.",
"le": "the",
"licence": "Licence",
"licenceNo": "Licence no. {{no}}",
"lieuxDentraînements": "Training locations",
"loading": "Loading...",
"me": {
"result": {
"PRIVATE": "Private (visible only to me)",
"PUBLIC": "Public (visible to all)",
"REGISTERED_ONLY": "Logged-in members (visible to federation members)",
"REGISTERED_ONLY_NO_DETAILS": "Logged-in members - hide details (visible to federation members)"
},
"toast.settings.error": "Failed to update settings 😕",
"toast.settings.pending": "Updating settings in progress...",
"toast.settings.success": "Settings updated successfully 🎉"
},
"me.changerMonMotDePasse": "Change my password",
"me.formationDarbitrage": "Referee training",
"me.paramètresDuCompte": "Account settings",
"me.rôleAuSienDuClub": "Role within the club",
"me.visibilitéDesRésultats": "Results visibility",
"member_one": "Member",
"member_other": "Members",
"membre.emailVideàLaLigne": "Empty email on line {{no}}",
"membre.emailVérifié": "Verified email",
"membre.filtre.inactif": "Show inactive fighters",
"membre.filtre.licence": "Show licence status",
"membre.filtre.licences": [
"No request or valid licence",
"With request or valid licence",
"Request in progress",
"Licence validated",
"All licence statuses",
"Complete request",
"Incomplete request"
],
"membre.filtre.payement": [
"No payment",
"With payment",
"All payment statuses"
],
"membre.identifiant": "Identifier",
"membre.import.err1": "Invalid certificate date format on line {{no}}",
"membre.import.err2": "Invalid certificate date format on line {{no}}",
"membre.import.err3": "Empty date of birth on line {{no}}",
"membre.import.err4": "Invalid date of birth format on line {{no}}",
"membre.import.err5": "Invalid email on line {{no}}",
"membre.import.errTT_one": "{{count}} error in the file, operation cancelled",
"membre.import.errTT_other": "{{count}} errors in the file, operation cancelled",
"membre.import.warn_one": "{{count}} medical certificate not filled",
"membre.import.warn_other": "{{count}} medical certificates not filled",
"membre.info.emailInfo": "The email is used to create an account to log in to the site and must be unique.<br/>For minors, the parents' email can be used multiple times using the following syntax: {'email.parent+<alphanumeric characters>@example.com'}.<br/>Examples: mail.parent+1@example.com, mail.parent+titouan@example.com, mail.parent+cedrique@example.com",
"membre.info.error1": "Please select a valid country 😕",
"membre.info.error2": "Please select a valid club 😕",
"membre.initaccount": "Initialize account",
"membre.initaccount.text1": "Enter the account UUID",
"membre.initaccount.text2": "Warning: only change a member's ID if you are sure of what you are doing...",
"membre.noAccount": "This member does not have an account...",
"membre.noAccount.clubMsg": "An account will be created by the federation when their first licence is validated",
"membre.nomVideàLaLigne": "Empty name on line {{no}}",
"membre.prénomVideàLaLigne": "Empty first name on line {{no}}",
"membre.toast.compte.created": "Account created successfully 🎉",
"membre.toast.compte.error": "Failed to create account",
"membre.toast.compte.pending": "Creating account in progress",
"membre.toast.del.error": "Failed to delete account",
"membre.toast.del.pending": "Deleting account in progress",
"membre.toast.del.success": "Account deleted successfully 🎉",
"membre.toast.id.error": "Failed to set identifier",
"membre.toast.id.pending": "Setting identifier in progress",
"membre.toast.id.success": "Identifier set successfully 🎉",
"membre.toast.licence.ask.del.error": "Failed to delete licence request",
"membre.toast.licence.ask.del.pending": "Deleting licence request in progress",
"membre.toast.licence.ask.del.success": "Licence request deleted successfully 🎉",
"membre.toast.licence.ask.error": "Failed to request licence",
"membre.toast.licence.ask.pending": "Recording licence request in progress",
"membre.toast.licence.ask.success": "Licence request recorded successfully 🎉",
"membre.toast.licence.del.error": "Failed to delete licence",
"membre.toast.licence.del.pending": "Deleting licence in progress",
"membre.toast.licence.del.success": "Licence deleted successfully 🎉",
"membre.toast.licence.save.error": "Failed to save licence",
"membre.toast.licence.save.pending": "Saving licence in progress",
"membre.toast.licence.save.success": "Licence saved successfully 🎉",
"membre.toast.licences.export.error": "Failed to export licences",
"membre.toast.licences.export.pending": "Exporting licences in progress",
"membre.toast.licences.export.success": "Licences exported successfully 🎉",
"membre.toast.licences.import.error": "Failed to send changes",
"membre.toast.licences.import.pending": "Sending changes in progress",
"membre.toast.licences.import.success": "Changes sent successfully 🎉",
"membre.toast.licences.load.error": "Failed to load licences",
"membre.toast.licences.load.pending": "Loading licences in progress",
"membre.toast.licences.load.success": "Licences loaded successfully 🎉",
"membre.toast.perm.error": "Failed to update permissions 😕",
"membre.toast.perm.pending": "Updating permissions in progress...",
"membre.toast.perm.success": "Permissions updated successfully 🎉",
"membre.toast.save.error": "Failed to update profile 😕",
"membre.toast.save.pending": "Updating profile in progress...",
"membre.toast.save.success": "Profile updated successfully 🎉",
"membre.toast.select.del.error": "Failed to delete selection",
"membre.toast.select.del.pending": "Deleting selection in progress",
"membre.toast.select.del.success": "Selection deleted successfully 🎉",
"membre.toast.select.save.error": "Failed to save selection",
"membre.toast.select.save.pending": "Saving selection in progress",
"membre.toast.select.save.success": "Selection saved successfully 🎉",
"mettreàJours": "Update",
"nationalité": "Nationality",
"nav": {
"account": "My account",
"aff_request": "Affiliation request",
"club": {
"my": "My club"
},
"competitions": {
"results": "My results"
},
"home": "Home",
"login": "Login",
"logout": "Logout",
"space": "My space",
"title": "FFSAF Intranet"
},
"noLicence": "Licence no.",
"noSiretOuRna": "SIRET or RNA no.",
"nom": "Last name",
"nombreDeLicences": "Number of licences",
"nombreDeLicencesParCatégorie": "Number of licences by category for {{saison}}",
"non": "No",
"nonDéfinie": "Not defined",
"nonValidée": "Not validated",
"nouveauClub": "New club",
"nouveauMembre": "New member",
"nouvelEmail": "New email",
"oui": "Yes",
"outdated_session": {
"login_button": "Log in again",
"message": "Your session has expired. Please log in again to continue using the application.",
"title": "Session expired"
},
"pageClub": "Club page",
"pageMembre": "Member page",
"pageNouveauClub": "New club page",
"page_info_full": "Line {{line}} to {{tt_line}} (page {{page}} of {{tt_page}})",
"page_info_ligne": "{{show}} line(s) displayed out of {{total}}",
"paiementDeLaLicence": "Licence payment",
"paiementDesLicences": "Licence payments",
"pasDeLicence": "No licence",
"payment.ha.info": "HelloAsso's solidarity model guarantees that 100% of your payment will be transferred to the chosen association. You can support the help they provide to associations by leaving a voluntary contribution to HelloAsso at the time of your payment.",
"payment.info": "About HelloAsso",
"payment.paiementSécurisé": "Secure payment",
"payment.payerAvec": "Pay with",
"payment.recap": "{{count}} licence(s) selected<br/>Total to pay: {{total}} €",
"paymentDesLicences": "Licence payments",
"paymentDesLicences.msg_one": "Are you sure you want to mark the licence as paid?",
"paymentDesLicences.msg_other": "Are you sure you want to mark the {{count}} licences as paid?",
"paymentDesLicences.msg_zero": "$t(paymentDesLicences.msg_other)",
"paymentOk": "🎉 Your payment has been processed successfully. 🎉",
"paymentOk.msg": "Thank you for your payment. The licences should be activated within the next hour, provided the medical certificate is completed.",
"pays": "Country",
"perm.administrateurDeLaFédération": "Federation administrator",
"perm.créerDesCompétion": "Create competitions",
"perm.ffsafIntra": "FFSAF intra",
"permission": "Permission",
"photos": "Photos",
"prenom": "First name",
"rechercher": "Search",
"rechercher...": "Search...",
"registration_one": "Registration",
"registration_other": "Registrations",
"renouveler": "Renew",
"renouvellementDeLaffiliation": "Affiliation renewal",
"result_one": "Result",
"result_other": "Results",
"retouràLaListeDeMembres": "Back to member list",
"role": "Role",
"role.membre": "Member",
"role.membreDuBureau": "Board member",
"role.président": "President",
"role.secrétaire": "Secretary",
"role.trésorier": "Treasurer",
"role.vise-président": "Vice-President",
"role.vise-secrétaire": "Vice-Secretary",
"role.vise-trésorier": "Vice-Treasurer",
"saison": "Season",
"selectionner...": "Select...",
"siretOuRna": "SIRET or RNA",
"stats": "Statistics",
"statue": "Statue",
"status": "Status",
"statuts": "Statutes",
"supprimerLeClub": "Delete club",
"supprimerLeClub.msg": "Are you sure you want to delete this club?",
"supprimerLeCompte": "Delete account",
"supprimerLeCompte.msg": "Are you sure you want to delete this account?",
"sélectionEnéquipeDeFrance": "Selection in the French team",
"sélectionner...": "Select...",
"toast.edit.error": "Failed to save changes",
"toast.edit.pending": "Saving changes in progress",
"toast.edit.success": "Changes saved successfully 🎉",
"toast.licence.bulk.pay.error": "Failed to mark licences as paid",
"toast.licence.bulk.pay.pending": "Marking licences as paid in progress",
"toast.licence.bulk.pay.success": "Licences marked as paid successfully 🎉",
"toast.licence.bulk.valid.error": "Failed to validate licences",
"toast.licence.bulk.valid.pending": "Validating licences in progress",
"toast.licence.bulk.valid.success": "Licences validated successfully 🎉",
"toast.licence.order.error": "Failed to create order",
"toast.licence.order.pending": "Creating order in progress",
"toast.licence.order.success": "Order created successfully 🎉",
"trie": "Sort",
"téléchargerLexcelDesMembres": "Download members' Excel",
"téléchargerLexcelDesMembres.info": "Use as a template to update information",
"téléchargéeLaLicence": "Download licence",
"validationDeLaLicence": "Licence validation",
"validationDesLicences": "Licence validations",
"validerDesLicences": "Validate licences",
"validerLePayement_one": "Validate payment for the selected licence",
"validerLePayement_other": "Validate payment for the {{count}} selected licences",
"validerLePayement_zero": "$t(validerLePayement_other)",
"validerLicence.msg_one": "Are you sure you want to validate the licence?",
"validerLicence.msg_other": "Are you sure you want to validate the {{count}} licences?",
"validerLicence.msg_zero": "$t(validerLicence.msg_other)",
"validerLicence_one": "Validate the selected licence",
"validerLicence_other": "Validate the {{count}} selected licences",
"validerLicence_zero": "$t(validerLicence_other)",
"validée": "Validated",
"voirLesStatues": "View statues",
"à": "at",
"étatDeLaDemande": "Request status"
}

View File

@ -0,0 +1,419 @@
{
"(optionnelle)": "(optionnelle)",
"---SansClub---": "--- sans club ---",
"---ToutLesClubs---": "--- tout les clubs ---",
"---ToutLesPays---": "--- tout les pays ---",
"---TouteLesCatégories---": "--- toute les catégories ---",
"--NonLicencier--": "-- Non licencier --",
"activer": "Activer",
"admin": "Administration",
"adresse": "Adresse",
"adresseAdministrative": "Adresse administrative",
"aff.ancienNom": "Ancien nom: {{name}}",
"aff.byMembreSim": "Par Membre similaire",
"aff.byNewMenbre": "Par Nouveau membre",
"aff.byNoLicence": "Par n° de licence",
"aff.info1": "Ce club a déjà été affilié (affiliation n°{{no}})",
"aff.membreNo": "Membre n°{{no}}",
"aff.nomDuClub": "Nom du club",
"aff.raisonDuRefus": "Raison du refus",
"aff.raisonDuRefus.msg": "Veuillez indiquer la raison du refus",
"aff.refusConfirm": "Êtes-vous sûr de vouloir refuser cette demande ?",
"aff.refuserLaDemande": "Refuser la demande",
"aff.refuserLaDemande.detail": "Êtes-vous sûr de vouloir refuser cette demande ?",
"aff.submit.error1": "Veuillez saisir un numéro de licence valide pour le membre {{id}}",
"aff.submit.error2": "Veuillez saisir un numéro de licence valide pour le membre {{id}}",
"aff.submit.error3": "Veuillez saisir un email valide pour le membre {{id}}",
"aff.toast.accept.error": "Échec de l'acceptation de l'affiliation",
"aff.toast.accept.pending": "Acceptation de l'affiliation en cours",
"aff.toast.accept.success": "Affiliation acceptée avec succès 🎉",
"aff.toast.del.error": "Échec de la suppression de la demande d'affiliation",
"aff.toast.del.pending": "Suppression de la demande d'affiliation en cours",
"aff.toast.del.success": "Demande d'affiliation supprimée avec succès 🎉",
"aff.toast.del2.error": "Échec de la suppression de l'affiliation",
"aff.toast.del2.pending": "Suppression de l'affiliation en cours",
"aff.toast.del2.success": "Affiliation supprimée avec succès 🎉",
"aff.toast.save.error": "Échec de l'enregistrement de la demande d'affiliation",
"aff.toast.save.pending": "Enregistrement de la demande d'affiliation en cours",
"aff.toast.save.success": "Demande d'affiliation enregistrée avec succès 🎉",
"aff.toast.save2.error": "Échec de l'enregistrement de l'affiliation",
"aff.toast.save2.pending": "Enregistrement de l'affiliation en cours",
"aff.toast.save2.success": "Affiliation enregistrée avec succès 🎉",
"aff_req.appuyerSurRechercher": "Appuyer sur rechercher pour compléter",
"aff_req.association": "L'association",
"aff_req.button.cancel": "Annuler ma demande",
"aff_req.button.confirm": "Confirmer ma demande d'affiliation",
"aff_req.button.save": "Enregistrer les modifications",
"aff_req.denomination": "Dénomination",
"aff_req.disposeLicence": "Dispose déjà d'une licence",
"aff_req.error1": "Le rôle du membre {{i}} est obligatoire",
"aff_req.error2": "Le format du SIRET/RNA est invalide",
"aff_req.nomDeLassociation": "Nom de l'association",
"aff_req.text1": "L'affiliation est annuelle et valable pour une saison sportive : du 1er septembre au 31 août de lannée suivante.",
"aff_req.text2": "Pour saffilier, une association sportive doit réunir les conditions suivantes :",
"aff_req.text2.li": [
"Avoir son siège social en France ou Principauté de Monaco",
"Être constituée conformément au chapitre 1er du titre II du livre 1er du Code du Sport",
"Poursuivre un objet social entrant dans la définition de larticle 1 des statuts de la Fédération",
"Disposer de statuts compatibles avec les principes dorganisation et de fonctionnement de la Fédération",
"Assurer en son sein la liberté dopinion et le respect des droits de la défense, et sinterdire toute discrimination",
"Respecter les règles dencadrement, dhygiène et de sécurité établies par les règlements de la Fédération"
],
"aff_req.text3": "Après validation de votre demande, vous recevrez un identifiant et mot de passe provisoire pour accéder à votre espace FFSAF",
"aff_req.text4": "Notez que pour finaliser votre affiliation, il vous faudra :",
"aff_req.text4.li1": "Disposer dau moins trois membres licenciés, dont le président",
"aff_req.text4.li2": "S'être acquitté des cotisations prévues par les règlements fédéraux",
"aff_req.text5": "Vous pourrez par la suite, ajouter des adresses visibles publiquement pour vos lieux d'entrainement",
"aff_req.text6": "Laissez vide pour ne rien changer. (Si un blason a déjà été envoyé lors de cette demande, il sera utilisé, sinon nous utiliserons celui de la précédant affiliation)",
"aff_req.text7": "Demande d'affiliation envoyée avec succès",
"aff_req.text8": "Une fois votre demande validée, vous recevrez un identifiant et mot de passe provisoire pour accéder à votre espace FFSAF",
"aff_req.toast.undo.error": "Échec de l'annulation de la demande d'affiliation",
"aff_req.toast.undo.pending": "Annulation de la demande d'affiliation en cours",
"aff_req.toast.undo.success": "Demande d'affiliation annulée avec succès 🎉",
"afficherLétatDesAffiliation": "Afficher l'état des affiliation",
"affiliation": "Affiliation",
"affiliationNo": "Affiliation n°{{no}}",
"ajouterUnClub": "Ajouter un club",
"ajouterUnMembre": "Ajouter un membre",
"all_season": "--- tout les saisons ---",
"aucunMembreSélectionné": "Aucun membre sélectionné",
"back": "« retour",
"blason": "Blason",
"bureau": "Bureau",
"button.accepter": "Accepter",
"button.ajouter": "Ajouter",
"button.annuler": "Annuler",
"button.appliquer": "Appliquer",
"button.confirmer": "Confirmer",
"button.créer": "Créer",
"button.enregistrer": "Enregistrer",
"button.fermer": "Fermer",
"button.refuser": "Refuser",
"button.suivant": "Suivant",
"button.supprimer": "Supprimer",
"cat.benjamin": "Benjamin",
"cat.cadet": "Cadet",
"cat.catégorieInconnue": "Catégorie inconnue",
"cat.junior": "Junior",
"cat.miniPoussin": "Mini Poussin",
"cat.minime": "Minime",
"cat.poussin": "Poussin",
"cat.senior1": "Senior 1",
"cat.senior2": "Senior 2",
"cat.superMini": "Super Mini",
"cat.vétéran1": "Vétéran 1",
"cat.vétéran2": "Vétéran 2",
"catégorie": "Catégorie",
"certificatMédical": "Certificat médical",
"chargement...": "Chargement...",
"chargerLexcel": "Charger l'Excel",
"chargerLexcel.msg": "Merci d'utiliser le fichier ci-dessus comme base, ne pas renommer les colonnes ni modifier les n° de licences.",
"choisir...": "Choisir...",
"club.aff_renew.msg": "Veuillez sélectionner 0 à 3 membres du bureau pour remplir la pré-demande. (Si un membre non-bureau va le devenir l'an prochain, vous pourrez les renseigner à la prochaine étape)",
"club.change.status": "Pour modifier les informations ci-dessus, merci de contacter la FFSAF par mail.",
"club.contact.tt": {
"AUTRE": "Autre contact du club",
"COURRIEL": "Adresse e-mail du club<br>Exemple: contact@ffsaf.fr",
"FACEBOOK": "Page Facebook du club débutant par 'https://www.facebook.com/'</br>Exemple: https://www.facebook.com/ffmsf",
"INSTAGRAM": "Compte Instagram du club débutant par 'https://www.instagram.com/'</br>Exemple: https://www.instagram.com/ff_msf",
"SITE": "Site web du club avec ou sans le 'https://'</br>Exemple: ffsaf.fr</br>Ou https://ffsaf.fr",
"TELEPHONE": "Numéro de téléphone du club<br>Exemple: 06 12 13 78 55"
},
"club.toast.aff.error": "Impossible de charger les affiliations",
"club.toast.aff.pending": "Chargement des affiliations en cours",
"club.toast.aff.success": "Affiliations chargées avec succès 🎉",
"club.toast.del.error": "Échec de la suppression du club",
"club.toast.del.pending": "Suppression du club en cours",
"club.toast.del.success": "Club supprimé avec succès 🎉",
"club.toast.new.error": "Échec de la création du club",
"club.toast.new.pending": "Création du club en cours",
"club.toast.new.success": "Club créé avec succès 🎉",
"club.toast.save.error": "Échec de l'enregistrement du club",
"club.toast.save.pending": "Enregistrement du club en cours",
"club.toast.save.success": "Club enregistré avec succès 🎉",
"clubExterne": "Club externe",
"club_one": "Club",
"club_other": "Clubs",
"club_zero": "Sans club",
"comp_manage": "Compétitions Manager",
"competition_one": "Compétition",
"competition_other": "Compétitions",
"compte": "Compte",
"conserverLancienEmail": "Conserver l'ancien email",
"contactAdministratif": "Contact administratif",
"contactInterne": "Contact interne",
"contact_one": "Contact",
"contact_other": "Contacts",
"dateDeNaissance": "Date de naissance",
"days": [
"Lundi",
"Mardi",
"Mercredi",
"Jeudi",
"Vendredi",
"Samedi",
"Dimanche"
],
"de": "de",
"demandeDaffiliationEnCours": "Demande d'affiliation en cours",
"demandeDeLicence": "Demande de licence ",
"demander": "Demander",
"dlAff": "Téléchargée l'attestation d'affiliation",
"donnéesAdministratives": "Données administratives",
"définirLidDuCompte": "Définir l'id du compte",
"editionDeL'affiliation": "Edition de l'affiliation",
"editionDeLaDemande": "Edition de la demande ",
"editionDeLaLicence": "Edition de la licence",
"editionDeLaSéléction": "Edition de la séléction",
"editionDeLadresse": "Edition de l'adresse",
"email": "Email",
"en": "en",
"enAttente": "En attente",
"erreurDePaiement": "Erreur de paiement😕",
"erreurDePaiement.detail": "Message d'erreur :",
"erreurDePaiement.msg": "Une erreur est survenue lors du traitement de votre paiement. Veuillez réessayer plus tard.",
"espaceAdministration": "Espace administration",
"faitPar": "Fait par",
"femme": "Femme",
"filtre": "Filtre",
"genre": "Genre",
"gestionGroupée": "Gestion groupée",
"gradeDarbitrage": "Grade d'arbitrage",
"home": {
"header1": "Pour les licenciés",
"header2": "Pour les clubs",
"text1": "Vous y retrouverez toutes vos informations ainsi que l'état de votre inscription à la fédération. Vous pouvez également télécharger votre attestation d'inscription, vous inscrire aux compétitions ainsi que consulter vos résultats sous réserve que le club organisateur les ait renseignés.<br/><br/>Lors de votre première inscription, vous recevrez un email contenant vos informations d'identification, ce mail sera envoyé une fois votre licence validée par le secrétariat.",
"text2": "C'est ici que vous pouvez prendre les licences fédérales pour vos adhérents, que vous pouvez demander ou renouveler votre affiliation, renseigner vos horaires, lieux d'entraînement et réseaux sociaux qui seront par la suite affichés sur le site ffsaf.fr.<br/>Vous aurez par ailleurs la possibilité de publier des formulaires d'inscriptions pour vos compétitions ainsi que d'enregistrer les résultats.<br/><br/> Vous n'êtes pas encore affilié à la fédération ? Cliquez <1>içi</1> pour faire votre première demande.",
"welcome_message": "Bienvenue sur lintranet de la Fédération France Soft Armored Fighting"
},
"homme": "Homme",
"horairesD'entraînements": "Horaires d'entraînements",
"information": "Information",
"keepEmpty": "Laissez vide pour ne rien changer.",
"le": "le",
"licence": "Licence",
"licenceNo": "Licence n°{{no}}",
"lieuxDentraînements": "Lieux d'entraînements",
"loading": "Chargement...",
"me": {
"result": {
"PRIVATE": "Privé (visible uniquement par moi)",
"PUBLIC": "Public (visible par tous)",
"REGISTERED_ONLY": "Membres connectés (visibles par les membres de la fédération)",
"REGISTERED_ONLY_NO_DETAILS": "Membres connectés - masquer les détails (visibles par les membres de la fédération)"
},
"toast.settings.error": "Échec de la mise à jours des paramètres 😕",
"toast.settings.pending": "Mise à jours des paramètres en cours...",
"toast.settings.success": "Paramètres mis à jours avec succès 🎉"
},
"me.changerMonMotDePasse": "Changer mon mot de passe",
"me.formationDarbitrage": "Formation d'arbitrage",
"me.paramètresDuCompte": "Paramètres du compte",
"me.rôleAuSienDuClub": "Rôle au sien du club",
"me.visibilitéDesRésultats": "Visibilité des résultats",
"member_one": "Membre",
"member_other": "Membres",
"membre.emailVideàLaLigne": "Email vide à la ligne {{no}}",
"membre.emailVérifié": "Email vérifié",
"membre.filtre.inactif": "Afficher les combattants inactifs",
"membre.filtre.licence": "Afficher l'état des licences",
"membre.filtre.licences": [
"Sans demande ni licence validée",
"Avec demande ou licence validée",
"Demande en cours",
"Licence validée",
"Tout les états de licences",
"Demande complet",
"Demande incomplet"
],
"membre.filtre.payement": [
"Sans paiement",
"Avec paiement",
"Tout les états de paiement"
],
"membre.identifiant": "Identifiant",
"membre.import.err1": "Format de la date de certificat invalide à la ligne {{no}}",
"membre.import.err2": "Format de la date de certificat invalide à la ligne {{no}}",
"membre.import.err3": "Date de naissance vide à la ligne {{no}}",
"membre.import.err4": "Format de la date de naissance invalide à la ligne {{no}}",
"membre.import.err5": "Email invalide à la ligne {{no}}",
"membre.import.errTT_one": "{{count}} erreur dans le fichier, opération annulée",
"membre.import.errTT_other": "{{count}} erreurs dans le fichier, opération annulée",
"membre.import.warn_one": "{{count}} certificat médical non rempli",
"membre.import.warn_other": "{{count}} certificats médicaux non remplis",
"membre.info.emailInfo": "L'email sert à la création de compte pour se connecter au site et doit être unique.<br/>Pour les mineurs, l'email des parents peut être utilisé plusieurs fois grâce à la syntaxe suivante : {'email.parent+<caractères alphanumériques>@exemple.com'}.<br/>Exemples : mail.parent+1@exemple.com, mail.parent+titouan@exemple.com, mail.parent+cedrique@exemple.com",
"membre.info.error1": "Veuillez sélectionner un pays valide 😕",
"membre.info.error2": "Veuillez sélectionner un club valide 😕",
"membre.initaccount": "Initialiser le compte",
"membre.initaccount.text1": "Entré l'UUID du compte",
"membre.initaccount.text2": "Attention ne changée l'id d'un membre que si vous êtes sûr de ce que vos faites...",
"membre.noAccount": "Ce membre ne dispose pas de compte...",
"membre.noAccount.clubMsg": "Un compte sera créé par la fédération lors de la validation de sa première licence",
"membre.nomVideàLaLigne": "Nom vide à la ligne {{no}}",
"membre.prénomVideàLaLigne": "Prénom vide à la ligne {{no}}",
"membre.toast.compte.created": "Compte créé avec succès 🎉",
"membre.toast.compte.error": "Échec de la création du compte",
"membre.toast.compte.pending": "Création du compte en cours",
"membre.toast.del.error": "Échec de la suppression du compte",
"membre.toast.del.pending": "Suppression du compte en cours",
"membre.toast.del.success": "Compte supprimé avec succès 🎉",
"membre.toast.id.error": "Échec de la définition de l'identifient",
"membre.toast.id.pending": "Définition de l'identifient en cours",
"membre.toast.id.success": "Identifient défini avec succès 🎉",
"membre.toast.licence.ask.del.error": "Échec de la suppression de la demande de licence",
"membre.toast.licence.ask.del.pending": "Suppression de la demande de licence en cours",
"membre.toast.licence.ask.del.success": "Demande de licence supprimée avec succès 🎉",
"membre.toast.licence.ask.error": "Échec de la demande de licence",
"membre.toast.licence.ask.pending": "Enregistrement de la demande de licence en cours",
"membre.toast.licence.ask.success": "Demande de licence enregistrée avec succès 🎉",
"membre.toast.licence.del.error": "Échec de la suppression de la licence",
"membre.toast.licence.del.pending": "Suppression de la licence en cours",
"membre.toast.licence.del.success": "Licence supprimée avec succès 🎉",
"membre.toast.licence.save.error": "Échec de l'enregistrement de la licence",
"membre.toast.licence.save.pending": "Enregistrement de la licence en cours",
"membre.toast.licence.save.success": "Licence enregistrée avec succès 🎉",
"membre.toast.licences.export.error": "Échec de l'export des licences",
"membre.toast.licences.export.pending": "Export des licences en cours",
"membre.toast.licences.export.success": "Licences exportées avec succès 🎉",
"membre.toast.licences.import.error": "Échec de l'envoie des changements",
"membre.toast.licences.import.pending": "Envoie des changements en cours",
"membre.toast.licences.import.success": "Changements envoyés avec succès 🎉",
"membre.toast.licences.load.error": "Impossible de charger les licences",
"membre.toast.licences.load.pending": "Chargement des licences en cours",
"membre.toast.licences.load.success": "Licences chargées avec succès 🎉",
"membre.toast.perm.error": "Échec de la mise à jours des permissions 😕",
"membre.toast.perm.pending": "Mise à jours des permissions en cours...",
"membre.toast.perm.success": "Permission mise à jours avec succès 🎉",
"membre.toast.save.error": "Échec de la mise à jours du profil 😕",
"membre.toast.save.pending": "Mise à jours du profil en cours...",
"membre.toast.save.success": "Profil mis à jours avec succès 🎉",
"membre.toast.select.del.error": "Échec de la suppression de la séléction",
"membre.toast.select.del.pending": "Suppression de la séléction en cours",
"membre.toast.select.del.success": "Séléction supprimée avec succès 🎉",
"membre.toast.select.save.error": "Échec de l'enregistrement de la séléction",
"membre.toast.select.save.pending": "Enregistrement de la séléction en cours",
"membre.toast.select.save.success": "Séléction enregistrée avec succès 🎉",
"mettreàJours": "Mettre à jours",
"nationalité": "Nationalité",
"nav": {
"account": "Mon compte",
"aff_request": "Demande d'affiliation",
"club": {
"my": "Mon club"
},
"competitions": {
"results": "Mes résultats"
},
"home": "Accueil",
"login": "Connexion",
"logout": "Déconnexion",
"space": "Mon espace",
"title": "FFSAF Intranet"
},
"noLicence": "N° Licence",
"noSiretOuRna": "N° $t(siretOuRna)",
"nom": "Nom",
"nombreDeLicences": "Nombre de licences",
"nombreDeLicencesParCatégorie": "Nombre de licences par catégorie pour {{saison}}",
"non": "Non",
"nonDéfinie": "Non définie",
"nonValidée": "Non validée",
"nouveauClub": "Nouveau club",
"nouveauMembre": "Nouveau membre",
"nouvelEmail": "Nouvel email",
"oui": "Oui",
"outdated_session": {
"login_button": "Se reconnecter",
"message": "Votre session a expirée, veuillez vous reconnecter pour continuer à utiliser l'application.",
"title": "Session expirée"
},
"pageClub": "Page club",
"pageMembre": "Page membre",
"pageNouveauClub": "Page nouveau club",
"page_info_full": "Ligne {{line}} à {{tt_line}} (page {{page}} sur {{tt_page}})",
"page_info_ligne": "{{show}} ligne(s) affichée(s) sur {{total}}",
"paiementDeLaLicence": "Paiement de la licence",
"paiementDesLicences": "Paiement des licences",
"pasDeLicence": "Pas de licence",
"payment.ha.info": "Le modèle solidaire de HelloAsso garantit que 100% de votre paiement sera versé à lassociation choisie. Vous pouvez soutenir laide quils apportent aux associations en laissant une contribution volontaire à HelloAsso au moment de votre paiement.",
"payment.info": "A propos de HelloAsso",
"payment.paiementSécurisé": "Paiement sécurisé",
"payment.payerAvec": "Payer avec",
"payment.recap": "{{count}} licence(s) sélectionnée<br/>Total à régler : {{total}} €",
"paymentDesLicences": "Payment des licences",
"paymentDesLicences.msg_one": "Êtes-vous sûr de vouloir marquer comme payées la licence ?",
"paymentDesLicences.msg_other": "Êtes-vous sûr de vouloir marquer comme payées les {{count}} licences ?",
"paymentDesLicences.msg_zero": "$t(paymentDesLicences.msg_other)",
"paymentOk": "🎉Votre paiement a été traité avec succès.🎉",
"paymentOk.msg": "Merci pour votre paiement. Les licences devraient être activées dans l'heure qui vient, à condition que le certificat médical soit rempli.",
"pays": "Pays",
"perm.administrateurDeLaFédération": "Administrateur de la fédération",
"perm.créerDesCompétion": "Créer des compétion",
"perm.ffsafIntra": "FFSAF intra",
"permission": "Permission",
"photos": "Photos",
"prenom": "Prénom",
"rechercher": "Rechercher",
"rechercher...": "Rechercher...",
"registration_one": "Inscription",
"registration_other": "Inscriptions",
"renouveler": "Renouveler",
"renouvellementDeLaffiliation": "Renouvellement de l'affiliation",
"result_one": "Résultat",
"result_other": "Résultats",
"retouràLaListeDeMembres": "Retour à la liste de membres",
"role": "Rôle",
"role.membre": "Membre",
"role.membreDuBureau": "Membre du bureau",
"role.président": "Président",
"role.secrétaire": "Secrétaire",
"role.trésorier": "Trésorier",
"role.vise-président": "Vise-Président",
"role.vise-secrétaire": "Vise-Secrétaire",
"role.vise-trésorier": "Vise-Trésorier",
"saison": "Saison",
"selectionner...": "Sélectionner...",
"siretOuRna": "SIRET ou RNA",
"stats": "Statistiques",
"statue": "Statue",
"status": "Status",
"statuts": "Statuts",
"supprimerLeClub": "Supprimer le club",
"supprimerLeClub.msg": "Êtes-vous sûr de vouloir supprimer ce club ?",
"supprimerLeCompte": "Supprimer le compte",
"supprimerLeCompte.msg": "Êtes-vous sûr de vouloir supprimer ce compte ?",
"sélectionEnéquipeDeFrance": "Sélection en équipe de France",
"sélectionner...": "Sélectionner...",
"toast.edit.error": "Échec de l'enregistrement des modifications",
"toast.edit.pending": "Enregistrement des modifications en cours",
"toast.edit.success": "Modifications enregistrées avec succès 🎉",
"toast.licence.bulk.pay.error": "Échec du marquage des licences comme payées",
"toast.licence.bulk.pay.pending": "Marquage des licences comme payées en cours",
"toast.licence.bulk.pay.success": "Licences marquées comme payées avec succès 🎉",
"toast.licence.bulk.valid.error": "Échec de la validation des licences",
"toast.licence.bulk.valid.pending": "Validation des licences en cours",
"toast.licence.bulk.valid.success": "Licences validées avec succès 🎉",
"toast.licence.order.error": "Échec de le création de la commande",
"toast.licence.order.pending": "Création de la commande en cours",
"toast.licence.order.success": "Commande créée avec succès 🎉",
"trie": "Trie",
"téléchargerLexcelDesMembres": "Télécharger l'Excel des membres",
"téléchargerLexcelDesMembres.info": "À utiliser comme template pour mettre à jour les informations",
"téléchargéeLaLicence": "Téléchargée la licence",
"validationDeLaLicence": "Validation de la licence",
"validationDesLicences": "Validation des licences",
"validerDesLicences": "Valider des licences",
"validerLePayement_one": "Valider le payement de la licence sélectionnée",
"validerLePayement_other": "Valider le payement des {{count}} licences sélectionnées",
"validerLePayement_zero": "$t(validerLePayement_other)",
"validerLicence.msg_one": "Êtes-vous sûr de vouloir valider la licence ?",
"validerLicence.msg_other": "Êtes-vous sûr de vouloir valider les {{count}} licences ?",
"validerLicence.msg_zero": "$t(validerLicence.msg_other)",
"validerLicence_one": "Valider la licence sélectionnée",
"validerLicence_other": "Valider les {{count}} licences sélectionnées",
"validerLicence_zero": "$t(validerLicence_other)",
"validée": "Validée",
"voirLesStatues": "Voir les statues",
"à": "à",
"étatDeLaDemande": "État de la demande"
}

View File

@ -14,8 +14,8 @@ import {ClubRoot, getClubChildren} from "./pages/club/ClubRoot.jsx";
import {DemandeAff, DemandeAffOk} from "./pages/DemandeAff.jsx"; import {DemandeAff, DemandeAffOk} from "./pages/DemandeAff.jsx";
import {MePage} from "./pages/MePage.jsx"; import {MePage} from "./pages/MePage.jsx";
import {CompetitionRoot, getCompetitionChildren} from "./pages/competition/CompetitionRoot.jsx"; import {CompetitionRoot, getCompetitionChildren} from "./pages/competition/CompetitionRoot.jsx";
import {FallingLines} from "react-loader-spinner";
import {getResultChildren, ResultRoot} from "./pages/result/ResultRoot.jsx"; import {getResultChildren, ResultRoot} from "./pages/result/ResultRoot.jsx";
import {useTranslation} from "react-i18next";
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
@ -79,10 +79,12 @@ const router = createBrowserRouter([
function GetCompetitionMangerLazy() { function GetCompetitionMangerLazy() {
const CMLazy = lazy(() => import('./pages/competition/editor/CompetitionManagerRoot.jsx')) const CMLazy = lazy(() => import('./pages/competition/editor/CompetitionManagerRoot.jsx'))
const {t} = useTranslation();
return <Suspense return <Suspense
fallback={<div> fallback={<div>
<h1>Compétition manager</h1> <h1>{t("comp_manage")}</h1>
<p>Chargement...</p> <p>{t("loading")}</p>
</div>}> </div>}>
<CMLazy/> <CMLazy/>
</Suspense> </Suspense>
@ -144,6 +146,7 @@ function Root() {
function ReAuthMsg() { function ReAuthMsg() {
const {is_authenticated} = useAuth() const {is_authenticated} = useAuth()
const location = useLocation() const location = useLocation()
const {t} = useTranslation();
const notAuthPaths = [ const notAuthPaths = [
/^\/$/s, /^\/$/s,
@ -161,15 +164,14 @@ function ReAuthMsg() {
}}> }}>
<div className="card"> <div className="card">
<div className="card-header"> <div className="card-header">
<h5>Session expirée</h5> <h5>{t("outdated_session.title")}</h5>
</div> </div>
<div className="card-body"> <div className="card-body">
<p className="card-text">Votre session a expirée, veuillez vous reconnecter pour continuer à <p className="card-text">{t("outdated_session.message")}</p>
utiliser l'application.</p>
</div> </div>
<div className="card-footer"> <div className="card-footer">
<button className="btn btn-primary" onClick={() => login()} style={{marginRight: "0.5em"}}>Se reconnecter</button> <button className="btn btn-primary" onClick={() => login()} style={{marginRight: "0.5em"}}>{t("outdated_session.login_button")}</button>
<a className="btn btn-secondary" href="/">Accueil</a> <a className="btn btn-secondary" href="/">{t("nav.home")}</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -2,19 +2,12 @@ import {useEffect, useReducer, useState} from "react";
import {SimpleReducer} from "../../utils/SimpleReducer.jsx"; import {SimpleReducer} from "../../utils/SimpleReducer.jsx";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faAdd, faCircleQuestion, faTrashCan} from "@fortawesome/free-solid-svg-icons"; import {faAdd, faCircleQuestion, faTrashCan} from "@fortawesome/free-solid-svg-icons";
import {useTranslation} from "react-i18next";
export function ContactEditor({data}) { export function ContactEditor({data}) {
const [state, dispatch] = useReducer(SimpleReducer, []) const [state, dispatch] = useReducer(SimpleReducer, [])
const [out_data, setOutData] = useState({}) const [out_data, setOutData] = useState({})
const {t} = useTranslation();
const tooltipText = {
SITE: "Site web du club avec ou sans le 'https://'</br>Exemple: ffsaf.fr</br>Ou https://ffsaf.fr",
FACEBOOK: "Page Facebook du club débutant par 'https://www.facebook.com/'</br>Exemple: https://www.facebook.com/ffmsf",
TELEPHONE: "Numéro de téléphone du club<br>Exemple: 06 12 13 78 55",
INSTAGRAM: "Compte Instagram du club débutant par 'https://www.instagram.com/'</br>Exemple: https://www.instagram.com/ff_msf",
COURRIEL: "Adresse e-mail du club<br>Exemple: contact@ffsaf.fr",
AUTRE: "Autre contact du club",
}
useEffect(() => { useEffect(() => {
let i = 0; let i = 0;
@ -38,7 +31,7 @@ export function ContactEditor({data}) {
return <div className="row mb-3"> return <div className="row mb-3">
<input name="contact" value={JSON.stringify(out_data)} readOnly hidden/> <input name="contact" value={JSON.stringify(out_data)} readOnly hidden/>
<span className="input-group-text">Contacts</span> <span className="input-group-text">{t('contact', {count : 2})}</span>
<ul className="list-group form-control"> <ul className="list-group form-control">
{state.map((d, index) => { {state.map((d, index) => {
if (d.data === undefined || d.data.value === undefined) if (d.data === undefined || d.data.value === undefined)
@ -62,7 +55,7 @@ export function ContactEditor({data}) {
dispatch({type: 'UPDATE_OR_ADD', payload: {id: d.id, data: {type: d.data.type, value: e.target.value}}}) dispatch({type: 'UPDATE_OR_ADD', payload: {id: d.id, data: {type: d.data.type, value: e.target.value}}})
}}/> }}/>
<button type="button" className="btn btn-outline-info" data-bs-toggle="tooltip" data-bs-placement="top" <button type="button" className="btn btn-outline-info" data-bs-toggle="tooltip" data-bs-placement="top"
data-bs-title={tooltipText[d.data.type]} data-bs-html="true"> data-bs-title={t("club.contact.tt." + d.data.type)} data-bs-html="true">
<FontAwesomeIcon icon={faCircleQuestion}/> <FontAwesomeIcon icon={faCircleQuestion}/>
</button> </button>
<button className="btn btn-danger" type="button" <button className="btn btn-danger" type="button"

View File

@ -2,6 +2,7 @@ import {useEffect, useReducer, useState} from "react";
import {SimpleReducer} from "../../utils/SimpleReducer.jsx"; import {SimpleReducer} from "../../utils/SimpleReducer.jsx";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faAdd, faTrashCan} from "@fortawesome/free-solid-svg-icons"; import {faAdd, faTrashCan} from "@fortawesome/free-solid-svg-icons";
import {useTranslation} from "react-i18next";
function timeNumberToSting(nbMin) { function timeNumberToSting(nbMin) {
return String(Math.floor(nbMin / 60)).padStart(2, '0') + ":" + String(nbMin % 60).padStart(2, '0') return String(Math.floor(nbMin / 60)).padStart(2, '0') + ":" + String(nbMin % 60).padStart(2, '0')
@ -15,6 +16,7 @@ function timeStringToNumber(time) {
export function HoraireEditor({data}) { export function HoraireEditor({data}) {
const [state, dispatch] = useReducer(SimpleReducer, []) const [state, dispatch] = useReducer(SimpleReducer, [])
const [out_data, setOutData] = useState({}) const [out_data, setOutData] = useState({})
const {t} = useTranslation();
useEffect(() => { useEffect(() => {
if (data.training_day_time === null) if (data.training_day_time === null)
@ -38,7 +40,7 @@ export function HoraireEditor({data}) {
return <div className="row mb-3"> return <div className="row mb-3">
<input name="training_day_time" value={JSON.stringify(out_data)} readOnly hidden/> <input name="training_day_time" value={JSON.stringify(out_data)} readOnly hidden/>
<span className="input-group-text">Horaires d'entraînements</span> <span className="input-group-text">{t("horairesD'entraînements")}</span>
<ul className="list-group form-control"> <ul className="list-group form-control">
{state.map((d, index) => { {state.map((d, index) => {
return <div key={index} className="input-group"> return <div key={index} className="input-group">
@ -48,22 +50,22 @@ export function HoraireEditor({data}) {
dispatch({type: 'UPDATE_OR_ADD', payload: {id: d.id, data: d.data}}) dispatch({type: 'UPDATE_OR_ADD', payload: {id: d.id, data: d.data}})
dispatch({type: 'SORT', payload: sortHoraire}) dispatch({type: 'SORT', payload: sortHoraire})
}}> }}>
<option value="0">Lundi</option> <option value="0">{t('days.0')}</option>
<option value="1">Mardi</option> <option value="1">{t('days.1')}</option>
<option value="2">Mercredi</option> <option value="2">{t('days.2')}</option>
<option value="3">Jeudi</option> <option value="3">{t('days.3')}</option>
<option value="4">Vendredi</option> <option value="4">{t('days.4')}</option>
<option value="5">Samedi</option> <option value="5">{t('days.5')}</option>
<option value="6">Dimanche</option> <option value="6">{t('days.6')}</option>
</select> </select>
<span className="input-group-text">de</span> <span className="input-group-text">{t('de')}</span>
<input type="time" className="form-control" value={timeNumberToSting(d.data.time_start)} required <input type="time" className="form-control" value={timeNumberToSting(d.data.time_start)} required
onChange={(e) => { onChange={(e) => {
d.data.time_start = timeStringToNumber(e.target.value) d.data.time_start = timeStringToNumber(e.target.value)
dispatch({type: 'UPDATE_OR_ADD', payload: {id: d.id, data: d.data}}) dispatch({type: 'UPDATE_OR_ADD', payload: {id: d.id, data: d.data}})
dispatch({type: 'SORT', payload: sortHoraire}) dispatch({type: 'SORT', payload: sortHoraire})
}}/> }}/>
<span className="input-group-text">à</span> <span className="input-group-text">{t('à')}</span>
<input type="time" className="form-control" value={timeNumberToSting(d.data.time_end)} required <input type="time" className="form-control" value={timeNumberToSting(d.data.time_end)} required
onChange={(e) => { onChange={(e) => {
d.data.time_end = timeStringToNumber(e.target.value) d.data.time_end = timeStringToNumber(e.target.value)

View File

@ -5,10 +5,12 @@ import proj4 from "proj4";
import {useFetch} from "../../hooks/useFetch.js"; import {useFetch} from "../../hooks/useFetch.js";
import {MapContainer, Marker, TileLayer} from "react-leaflet"; import {MapContainer, Marker, TileLayer} from "react-leaflet";
import {SimpleReducer} from "../../utils/SimpleReducer.jsx"; import {SimpleReducer} from "../../utils/SimpleReducer.jsx";
import {useTranslation} from "react-i18next";
export function LocationEditor({data, setModal, sendData}) { export function LocationEditor({data, setModal, sendData}) {
const [state, dispatch] = useReducer(SimpleReducer, []) const [state, dispatch] = useReducer(SimpleReducer, [])
const [out_data, setOutData] = useState({}) const [out_data, setOutData] = useState({})
const {t} = useTranslation();
useEffect(() => { useEffect(() => {
if (data.training_location === null) if (data.training_location === null)
@ -42,7 +44,7 @@ export function LocationEditor({data, setModal, sendData}) {
return <div className="row mb-3"> return <div className="row mb-3">
<input name="training_location" value={JSON.stringify(out_data)} readOnly hidden/> <input name="training_location" value={JSON.stringify(out_data)} readOnly hidden/>
<span className="input-group-text">Lieux d'entraînements</span> <span className="input-group-text">{t('lieuxDentraînements')}</span>
<ul className="list-group form-control"> <ul className="list-group form-control">
{state.map((d, index) => { {state.map((d, index) => {
return <div key={index} className="input-group"> return <div key={index} className="input-group">
@ -83,8 +85,9 @@ export function LocationEditorModal({modal, sendData}) {
const [location, setLocation] = useState("") const [location, setLocation] = useState("")
const [locationObj, setLocationObj] = useState({text: "", lng: undefined, lat: undefined}) const [locationObj, setLocationObj] = useState({text: "", lng: undefined, lat: undefined})
const [mapPosition, setMapPosition] = useState([46.652195, 2.430226]) const [mapPosition, setMapPosition] = useState([46.652195, 2.430226])
const {data, error, refresh} = useFetch(null) const {data, refresh} = useFetch(null)
const map = useRef(null) const map = useRef(null)
const {t} = useTranslation();
useEffect(() => { useEffect(() => {
if (modal.data !== undefined) { if (modal.data !== undefined) {
@ -136,7 +139,7 @@ export function LocationEditorModal({modal, sendData}) {
<form onSubmit={e => sendData.current(e)}> <form onSubmit={e => sendData.current(e)}>
<div className="modal-content"> <div className="modal-content">
<div className="modal-header"> <div className="modal-header">
<h1 className="modal-title fs-5" id="EditModalLabel">Edition de l'adresse</h1> <h1 className="modal-title fs-5" id="EditModalLabel">{t('editionDeLadresse')}</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>
@ -147,7 +150,7 @@ export function LocationEditorModal({modal, sendData}) {
<input name="loc_lng" value={locationObj.lng ? locationObj.lng : -142} readOnly hidden/> <input name="loc_lng" value={locationObj.lng ? locationObj.lng : -142} readOnly hidden/>
<div className="row"> <div className="row">
<div className="input-group mb-3"> <div className="input-group mb-3">
<label className="input-group-text">Adresse</label> <label className="input-group-text">{t('adresse')}</label>
<input className="form-control" aria-autocomplete="list" aria-expanded="true" <input className="form-control" aria-autocomplete="list" aria-expanded="true"
placeholder="Chercher une adresse..." aria-label="Recherche" list="addr" value={location} placeholder="Chercher une adresse..." aria-label="Recherche" list="addr" value={location}
onChange={e => setLocation(e.target.value)}/> onChange={e => setLocation(e.target.value)}/>
@ -174,9 +177,9 @@ export function LocationEditorModal({modal, sendData}) {
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal" <button type="submit" className="btn btn-primary" data-bs-dismiss="modal"
disabled={locationObj.lng === undefined}>Enregistrer disabled={locationObj.lng === undefined}>{t('button.enregistrer')}
</button> </button>
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">Annuler</button> <button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">{t('button.annuler')}</button>
</div> </div>
</div> </div>
</form> </form>

View File

@ -1,6 +1,7 @@
import {LoadingProvider, useLoadingSwitcher} from "../hooks/useLoading.jsx"; import {LoadingProvider, useLoadingSwitcher} from "../hooks/useLoading.jsx";
import {useFetch} from "../hooks/useFetch.js"; import {useFetch} from "../hooks/useFetch.js";
import {AxiosError} from "./AxiosError.jsx"; import {AxiosError} from "./AxiosError.jsx";
import {useTranslation} from "react-i18next";
export function ClubSelect({defaultValue, name, na = false, disabled = false}) { export function ClubSelect({defaultValue, name, na = false, disabled = false}) {
return <LoadingProvider> return <LoadingProvider>
@ -11,6 +12,7 @@ export function ClubSelect({defaultValue, name, na = false, disabled = false}) {
function ClubSelect_({defaultValue, name, na, disabled}) { function ClubSelect_({defaultValue, name, na, disabled}) {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/club/no_detail`, setLoading, 1) const {data, error} = useFetch(`/club/no_detail`, setLoading, 1)
const {t} = useTranslation();
return <> return <>
{data {data
@ -18,8 +20,8 @@ function ClubSelect_({defaultValue, name, na, disabled}) {
<label className="input-group-text" id="inputGroupSelect02">Club</label> <label className="input-group-text" id="inputGroupSelect02">Club</label>
<select className="form-select" id="inputGroupSelect02" disabled={disabled} <select className="form-select" id="inputGroupSelect02" disabled={disabled}
defaultValue={defaultValue ? defaultValue : -1} name={name}> defaultValue={defaultValue ? defaultValue : -1} name={name}>
<option>Sélectionner...</option> <option>{t('sélectionner...')}</option>
{na && <option value={-1}>-- Non licencier --</option>} {na && <option value={-1}>{t('--NonLicencier--')}</option>}
{data.map(club => (<option key={club.id} value={club.id}>{club.name}</option>))} {data.map(club => (<option key={club.id} value={club.id}>{club.name}</option>))}
</select> </select>
</div> </div>
@ -31,11 +33,13 @@ function ClubSelect_({defaultValue, name, na, disabled}) {
} }
function Def() { function Def() {
const {t} = useTranslation();
return <div className="input-group mb-3"> return <div className="input-group mb-3">
<label className="input-group-text" id="inputGroupSelect02">Club</label> <label className="input-group-text" id="inputGroupSelect02">{t("club", {count: 1})}</label>
<select className="form-select" id="inputGroupSelect02" <select className="form-select" id="inputGroupSelect02"
defaultValue="Chargement..."> defaultValue={t('chargement...')}>
<option>Chargement...</option> <option>{t('chargement...')}</option>
</select> </select>
</div>; </div>;
} }

View File

@ -1,5 +1,6 @@
import {Fragment} from "react"; import {Fragment} from "react";
import './ColoredCircle.css' import './ColoredCircle.css'
import i18n from "i18next";
export const ColoredCircle = ({color, boolean}) => { export const ColoredCircle = ({color, boolean}) => {
const styles = {backgroundColor: '#F00'}; const styles = {backgroundColor: '#F00'};
@ -15,7 +16,7 @@ export const ColoredCircle = ({color, boolean}) => {
</Fragment> </Fragment>
}; };
export const ColoredText = ({boolean, text={true: "Oui", false: "Non"}}) => { export const ColoredText = ({boolean, text={true: i18n.t('oui'), false: i18n.t('non')}}) => {
const styles = {color: '#F00'}; const styles = {color: '#F00'};
if (boolean !== undefined) { if (boolean !== undefined) {

View File

@ -1,5 +1,7 @@
import {useTranslation} from "react-i18next";
export function ConfirmDialog({title, message, onConfirm = () => {}, onCancel = () => {}, id = "confirm-delete"}) { export function ConfirmDialog({title, message, onConfirm = () => {}, onCancel = () => {}, id = "confirm-delete"}) {
const {t} = useTranslation();
return <div className="modal fade" id={id} tabIndex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"> return <div className="modal fade" id={id} tabIndex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div className="modal-dialog"> <div className="modal-dialog">
<div className="modal-content"> <div className="modal-content">
@ -10,8 +12,8 @@ export function ConfirmDialog({title, message, onConfirm = () => {}, onCancel =
{message} {message}
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
<button type="button" className="btn btn-default" data-dismiss="modal" data-bs-dismiss="modal" onClick={onCancel}>Annuler</button> <button type="button" className="btn btn-default" data-dismiss="modal" data-bs-dismiss="modal" onClick={onCancel}>{t('button.annuler')}</button>
<a className="btn btn-danger btn-ok" data-bs-dismiss="modal" onClick={onConfirm}>Confirmer</a> <a className="btn btn-danger btn-ok" data-bs-dismiss="modal" onClick={onConfirm}>{t('button.confirmer')}</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,11 +0,0 @@
export function Input({placeholder, value, onChange}) {
return <div>
<input
type="text"
className="form-control"
value={value}
placeholder={placeholder}
onChange={(e) => onChange(e.target.value)}
/>
</div>
}

View File

@ -1,83 +0,0 @@
import {useEffect, useReducer, useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faPen, faTrashCan} from "@fortawesome/free-solid-svg-icons";
import {AxiosError} from "./AxiosError.jsx";
function SimpleReducer(datas, action) {
switch (action.type) {
case 'ADD':
return [
...datas,
action.payload
]
case 'REMOVE':
return datas.filter(data => data.id !== action.payload)
case 'UPDATE_OR_ADD':
const index = datas.findIndex(data => data.id === action.payload.id)
if (index === -1) {
return [
...datas,
action.payload
]
} else {
datas[index] = action.payload
return [...datas]
}
default:
throw new Error()
}
}
export function ListEditorTest() {
const [html, dispatch] = ListEditor(ListHTML)
useEffect(() => {
dispatch({type: 'UPDATE_OR_ADD', payload: {id: 1, content: "data in"}})
}, []);
return html
}
export function ListEditor(ListItem) {
const [modal, setModal] = useState({id: -1})
const [state, dispatch] = useReducer(SimpleReducer, [])
const sendAffiliation = (e) => {
dispatch({type: 'UPDATE_OR_ADD', payload: e})
}
return [<>
<ul className="list-group">
{state.map((d, index) => {
return <div key={index} className={"list-group-item d-flex justify-content-between align-items-start"}>
<ListItem data={d}/>
<button className="badge btn btn-primary rounded-pill" data-bs-toggle="modal"
data-bs-target="#EditModal" onClick={_ => setModal(d)}>
<FontAwesomeIcon icon={faPen}/></button>
<button className="badge btn btn-danger rounded-pill"
onClick={() => dispatch({type: 'REMOVE', payload: d.id})}>
<FontAwesomeIcon icon={faTrashCan}/></button>
</div>
})}
</ul>
<div className="modal fade" id="EditModal" tabIndex="-1" aria-labelledby="EditModalLabel"
aria-hidden="true">
<div className="modal-dialog">
<div className="modal-content">
<form onSubmit={e => sendAffiliation(e, dispatch)}>
<input name="id" value={modal.id} readOnly hidden/>
</form>
<ModalContent affiliation={modalAffiliation} dispatch={dispatch}/>
</div>
</div>
</div>
</>
, dispatch]
}
function ListHTML({
data
}) {
return <div className="me-auto">{data.content}</div>
}

View File

@ -1,11 +1,15 @@
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {getCategoryFormBirthDate, getCatName} from "../utils/Tools.js"; import {getCategoryFormBirthDate, getCatName} from "../utils/Tools.js";
import {useCountries} from "../hooks/useCountries.jsx"; import {useCountries} from "../hooks/useCountries.jsx";
import i18n from "../config/i18n.js";
import {useTranslation} from "react-i18next";
export function BirthDayField({inti_date, inti_category, required = true}) { export function BirthDayField({inti_date, inti_category, required = true}) {
const [date, setDate] = useState(inti_date) const [date, setDate] = useState(inti_date)
const [category, setCategory] = useState(inti_category) const [category, setCategory] = useState(inti_category)
const [canUpdate, setCanUpdate] = useState(false) const [canUpdate, setCanUpdate] = useState(false)
const {t} = useTranslation();
useEffect(() => { useEffect(() => {
const b = category !== getCategoryFormBirthDate(new Date(date)) const b = category !== getCategoryFormBirthDate(new Date(date))
if (b !== canUpdate) if (b !== canUpdate)
@ -18,19 +22,19 @@ export function BirthDayField({inti_date, inti_category, required = true}) {
return <> return <>
<div className="input-group mb-3"> <div className="input-group mb-3">
<span className="input-group-text" id="birth_date">Date de naissance</span> <span className="input-group-text" id="birth_date">{t('dateDeNaissance')}</span>
<input type="date" className="form-control" placeholder="jj/mm/aaaa" aria-label="birth_date" <input type="date" className="form-control" placeholder="jj/mm/aaaa" aria-label="birth_date"
name="birth_date" aria-describedby="birth_date" defaultValue={date} required={required} name="birth_date" aria-describedby="birth_date" defaultValue={date} required={required}
onChange={(e) => setDate(e.target.value)}/> onChange={(e) => setDate(e.target.value)}/>
</div> </div>
<div className="row"> <div className="row">
<div className="input-group mb-3"> <div className="input-group mb-3">
<span className="input-group-text" id="category">Catégorie</span> <span className="input-group-text" id="category">{t('catégorie')}</span>
<input type="text" className="form-control" placeholder="" name="category" <input type="text" className="form-control" placeholder="" name="category"
aria-label="category" value={category ? getCatName(category) : ""} aria-describedby="category" aria-label="category" value={category ? getCatName(category) : ""} aria-describedby="category"
disabled/> disabled/>
{canUpdate && <button className="btn btn-outline-secondary" type="button" id="button-addon1" {canUpdate && <button className="btn btn-outline-secondary" type="button" id="button-addon1"
onClick={updateCat}>Mettre à jours</button>} onClick={updateCat}>{t('mettreàJours')}</button>}
</div> </div>
</div> </div>
</> </>
@ -52,14 +56,14 @@ export function OptionField({name, text, values, value, disabled = false}) {
export function RoleList({name, text, value, disabled = false}) { export function RoleList({name, text, value, disabled = false}) {
return <OptionField name={name} text={text} value={value} disabled={disabled} return <OptionField name={name} text={text} value={value} disabled={disabled}
values={{ values={{
MEMBRE: 'Membre', MEMBRE: i18n.t('role.membre'),
PRESIDENT: 'Président', PRESIDENT: i18n.t('role.président'),
TRESORIER: 'Trésorier', TRESORIER: i18n.t('role.trésorier'),
SECRETAIRE: 'Secrétaire', SECRETAIRE: i18n.t('role.secrétaire'),
VPRESIDENT: 'Vise-Président', VPRESIDENT: i18n.t('role.vise-président'),
VTRESORIER: 'Vise-Trésorier', VTRESORIER: i18n.t('role.vise-trésorier'),
VSECRETAIRE: 'Vise-Secrétaire', VSECRETAIRE: i18n.t('role.vise-secrétaire'),
MEMBREBUREAU: 'Membre bureau' MEMBREBUREAU: i18n.t('role.membreDuBureau')
}}/> }}/>
} }

View File

@ -3,14 +3,16 @@ import {NavLink} from "react-router-dom";
import {useAuth} from "../hooks/useAuth.jsx"; import {useAuth} from "../hooks/useAuth.jsx";
import {login, logout} from "../utils/auth.js"; import {login, logout} from "../utils/auth.js";
import {isClubAdmin} from "../utils/Tools.js"; import {isClubAdmin} from "../utils/Tools.js";
import {useTranslation} from "react-i18next";
export function Nav() { export function Nav() {
const {t} = useTranslation();
return <nav className="navbar navbar-light navbar-expand-md bg-body-tertiary " id="main-navbar"> return <nav className="navbar navbar-light navbar-expand-md bg-body-tertiary " id="main-navbar">
<div className="container-fluid"> <div className="container-fluid">
<a className="navbar-brand" href="/"> <a className="navbar-brand" href="/">
<img className="logo" src="/FFSSAF-bord-blanc-fond-transparent.webp" alt="logo"/> <img className="logo" src="/FFSSAF-bord-blanc-fond-transparent.webp" alt="logo"/>
FFSAF Intranet {t("nav.title")}
</a> </a>
<button className="navbar-toggler" type="button" data-bs-toggle="collapse" <button className="navbar-toggler" type="button" data-bs-toggle="collapse"
@ -21,7 +23,7 @@ export function Nav() {
<div className="collapse navbar-collapse" id="navbarNavDropdown"> <div className="collapse navbar-collapse" id="navbarNavDropdown">
<div className="collapse-item"> <div className="collapse-item">
<ul className="navbar-nav"> <ul className="navbar-nav">
<li className="nav-item"><NavLink className="nav-link" to="/">Accueil</NavLink></li> <li className="nav-item"><NavLink className="nav-link" to="/">{t("nav.home")}</NavLink></li>
<CompMenu/> <CompMenu/>
<ClubMenu/> <ClubMenu/>
<AdminMenu/> <AdminMenu/>
@ -37,83 +39,87 @@ export function Nav() {
function AffiliationMenu() { function AffiliationMenu() {
const {is_authenticated} = useAuth() const {is_authenticated} = useAuth()
const {t} = useTranslation();
if (is_authenticated) if (is_authenticated)
return <></> return <></>
return <li className="nav-item"><NavLink className="nav-link" to="/affiliation">Demande d'affiliation</NavLink></li> return <li className="nav-item"><NavLink className="nav-link" to="/affiliation">{t("nav.aff_request")}</NavLink></li>
} }
function CompMenu() { function CompMenu() {
const {is_authenticated} = useAuth() const {is_authenticated} = useAuth()
const {t} = useTranslation();
if (!is_authenticated) if (!is_authenticated)
return <></> return <></>
return <li className="nav-item dropdown"> return <li className="nav-item dropdown">
<div className="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <div className="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Compétitions {t("competition", {count: 2})}
</div> </div>
<ul className="dropdown-menu"> <ul className="dropdown-menu">
<li className="nav-item"><NavLink className="nav-link" to="/competition">Inscription</NavLink></li> <li className="nav-item"><NavLink className="nav-link" to="/competition">{t("registration", {count: 1})}</NavLink></li>
<li className="nav-item"><NavLink className="nav-link" to="/result">Mes résultats</NavLink></li> <li className="nav-item"><NavLink className="nav-link" to="/result">{t("nav.competitions.results")}</NavLink></li>
<li className="nav-item"><NavLink className="nav-link" to="/competition-manager">Compétitions <li className="nav-item"><NavLink className="nav-link" to="/competition-manager">{t("comp_manage")}</NavLink></li>
Manager</NavLink></li>
</ul> </ul>
</li> </li>
} }
function ClubMenu() { function ClubMenu() {
const {is_authenticated, userinfo} = useAuth() const {is_authenticated, userinfo} = useAuth()
const {t} = useTranslation();
if (!is_authenticated || !(isClubAdmin(userinfo))) if (!is_authenticated || !(isClubAdmin(userinfo)))
return <></> return <></>
return <li className="nav-item dropdown"> return <li className="nav-item dropdown">
<div className="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <div className="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Club {t("club", {count: 1})}
</div> </div>
<ul className="dropdown-menu"> <ul className="dropdown-menu">
<li className="nav-item"><NavLink className="nav-link" to="/club/me">Mon club</NavLink></li> <li className="nav-item"><NavLink className="nav-link" to="/club/me">{t("nav.club.my")}</NavLink></li>
<li className="nav-item"><NavLink className="nav-link" to="/club/member">Membres</NavLink></li> <li className="nav-item"><NavLink className="nav-link" to="/club/member">{t("member", {count: 2})}</NavLink></li>
</ul> </ul>
</li> </li>
} }
function AdminMenu() { function AdminMenu() {
const {is_authenticated, userinfo} = useAuth() const {is_authenticated, userinfo} = useAuth()
const {t} = useTranslation();
if (!is_authenticated || !userinfo?.roles?.includes("federation_admin")) if (!is_authenticated || !userinfo?.roles?.includes("federation_admin"))
return <></> return <></>
return <li className="nav-item dropdown"> return <li className="nav-item dropdown">
<div className="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <div className="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Administration {t("admin")}
</div> </div>
<ul className="dropdown-menu"> <ul className="dropdown-menu">
<li className="nav-item"><NavLink className="nav-link" to="/admin/member">Membres</NavLink></li> <li className="nav-item"><NavLink className="nav-link" to="/admin/member">{t("member", {count: 2})}</NavLink></li>
<li className="nav-item"><NavLink className="nav-link" to="/admin/club">Club</NavLink></li> <li className="nav-item"><NavLink className="nav-link" to="/admin/club">{t("club", {count: 2})}</NavLink></li>
<li className="nav-item"><NavLink className="nav-link" to="/admin/stats">Statistiques</NavLink></li> <li className="nav-item"><NavLink className="nav-link" to="/admin/stats">{t("stats")}</NavLink></li>
</ul> </ul>
</li> </li>
} }
function LoginMenu() { function LoginMenu() {
const {is_authenticated} = useAuth() const {is_authenticated} = useAuth()
const {t} = useTranslation();
return <> return <>
{!is_authenticated ? {!is_authenticated ?
<li className="nav-item"> <li className="nav-item">
<div className="nav-link" onClick={() => login()}>Connexion</div> <div className="nav-link" onClick={() => login()}>{t("nav.login")}</div>
</li> </li>
: :
<li className="nav-item dropdown"> <li className="nav-item dropdown">
<div className="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <div className="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Mon compte {t("nav.account")}
</div> </div>
<ul className="dropdown-menu"> <ul className="dropdown-menu">
<li className="nav-item"><NavLink className="nav-link" to="/me">Mon espace</NavLink></li> <li className="nav-item"><NavLink className="nav-link" to="/me">{t("nav.space")}</NavLink></li>
<li className="nav-item"> <li className="nav-item">
<div className="nav-link" onClick={() => logout()}>Déconnexion</div> <div className="nav-link" onClick={() => logout()}>{t("nav.logout")}</div>
</li> </li>
</ul> </ul>
</li> </li>

View File

@ -1,4 +1,5 @@
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {useTranslation} from "react-i18next";
const removeDiacritics = str => { const removeDiacritics = str => {
return str return str
@ -9,6 +10,7 @@ const removeDiacritics = str => {
export function SearchBar({search, defaultValue = ""}) { export function SearchBar({search, defaultValue = ""}) {
const [searchInput, setSearchInput] = useState(defaultValue); const [searchInput, setSearchInput] = useState(defaultValue);
const {t} = useTranslation();
const handelChange = (e) => { const handelChange = (e) => {
setSearchInput(e.target.value); setSearchInput(e.target.value);
@ -33,10 +35,10 @@ export function SearchBar({search, defaultValue = ""}) {
return <div className="mb-3"> return <div className="mb-3">
<div className="input-group mb-3"> <div className="input-group mb-3">
<input type="text" className="form-control" placeholder="Rechercher..." aria-label="Rechercher..." <input type="text" className="form-control" placeholder={t('rechercher...')} aria-label={t('rechercher...')}
aria-describedby="button-addon2" value={searchInput} onChange={handelChange} onKeyDown={handleKeyDown}/> aria-describedby="button-addon2" value={searchInput} onChange={handelChange} onKeyDown={handleKeyDown}/>
<button className="btn btn-outline-secondary" type="button" id="button-addon2" <button className="btn btn-outline-secondary" type="button" id="button-addon2"
onClick={searchMember}>Rechercher onClick={searchMember}>{t('rechercher')}
</button> </button>
</div> </div>
</div> </div>

View File

@ -0,0 +1,33 @@
import i18n from 'i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import {initReactI18next} from 'react-i18next';
const options = {
order: [ 'querystring', 'cookie', 'localStorage', 'sessionStorage', 'navigator', 'htmlTag'],
caches: [],
}
i18n
// load translation using http -> see /public/locales
// learn more: https://github.com/i18next/i18next-http-backend
.use(Backend)
// detect user language
// learn more: https://github.com/i18next/i18next-browser-languageDetector
.use(LanguageDetector)
// pass the i18n instance to react-i18next.
.use(initReactI18next)
// init i18next
// for all options read: https://www.i18next.com/overview/configuration-options
.init({
fallbackLng: 'fr',
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
detection: options,
ns: ['common'],
defaultNS: 'common',
});
export default i18n;

View File

@ -2,6 +2,9 @@ import React, {lazy, Suspense} from 'react'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import App from "./App.jsx"; import App from "./App.jsx";
// import i18n (needs to be bundled ;))
import './config/i18n.js';
ReactDOM.createRoot(document.getElementById('root')).render( ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode> <React.StrictMode>
<App/> <App/>

View File

@ -1,7 +1,8 @@
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {apiAxios, errFormater, getSaison} from "../utils/Tools.js"; import {apiAxios, getToastMessage} from "../utils/Tools.js";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {useLocation, useNavigate} from "react-router-dom"; import {useLocation, useNavigate} from "react-router-dom";
import {useTranslation} from "react-i18next";
const notUpperCase = ["de", "la", "le", "les", "des", "du", "d'", "l'", "sur", 'lieu', 'dit']; const notUpperCase = ["de", "la", "le", "les", "des", "du", "d'", "l'", "sur", 'lieu', 'dit'];
@ -48,6 +49,7 @@ export function DemandeAff() {
const navigate = useNavigate(); const navigate = useNavigate();
const [initData, setInitData] = useState(null) const [initData, setInitData] = useState(null)
const [needFile, setNeedFile] = useState(true) const [needFile, setNeedFile] = useState(true)
const {t} = useTranslation();
useEffect(() => { useEffect(() => {
if (hash.startsWith("#d")) { if (hash.startsWith("#d")) {
@ -79,7 +81,7 @@ export function DemandeAff() {
let error = false; let error = false;
for (let i = 1; i <= 3; i++) { for (let i = 1; i <= 3; i++) {
if (event.target[`m${i}_role`]?.value === "0") { if (event.target[`m${i}_role`]?.value === "0") {
toast.error(`Le rôle du membre ${i} est obligatoire`) toast.error(t('aff_req.error1', {i: i}));
error = true; error = true;
} }
} }
@ -89,16 +91,7 @@ export function DemandeAff() {
if (event.nativeEvent.submitter.value === "undo") { if (event.nativeEvent.submitter.value === "undo") {
toast.promise( toast.promise(
apiAxios.delete(`/affiliation/request/${initData.id}`), apiAxios.delete(`/affiliation/request/${initData.id}`), getToastMessage('aff_req.toast.undo')
{
pending: "Annulation de la demande d'affiliation en cours",
success: "Demande d'affiliation annulée avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de l'annulation de la demande d'affiliation")
}
}
}
).then(_ => { ).then(_ => {
navigate("/club/me") navigate("/club/me")
}) })
@ -106,16 +99,7 @@ export function DemandeAff() {
formData.append("id", initData.id) formData.append("id", initData.id)
toast.promise( toast.promise(
apiAxios.put(`/affiliation/request/edit`, formData), apiAxios.put(`/affiliation/request/edit`, formData), getToastMessage('toast.edit')
{
pending: "Enregistrement des modifications en cours",
success: "Modifications enregistrées avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de l'enregistrement des modifications")
}
}
}
).then(_ => { ).then(_ => {
navigate("/club/me") navigate("/club/me")
}) })
@ -123,16 +107,7 @@ export function DemandeAff() {
formData.append("id", -1) formData.append("id", -1)
toast.promise( toast.promise(
apiAxios.post(`/affiliation/request`, formData, {headers: {'Accept': '*/*'}}), apiAxios.post(`/affiliation/request`, formData, {headers: {'Accept': '*/*'}}), getToastMessage('aff.toast.save')
{
pending: "Enregistrement de la demande d'affiliation en cours",
success: "Demande d'affiliation enregistrée avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de la demande d'affiliation")
}
}
}
).then(_ => { ).then(_ => {
navigate("/affiliation/ok") navigate("/affiliation/ok")
}) })
@ -140,62 +115,46 @@ export function DemandeAff() {
} }
return <div> return <div>
<h1>Demande d'affiliation {getSaisonToAff() + "-" + (getSaisonToAff() + 1)}</h1> <h1>{t("nav.aff_request")} {getSaisonToAff() + "-" + (getSaisonToAff() + 1)}</h1>
<p>L'affiliation est annuelle et valable pour une saison sportive : du 1er septembre au 31 août de lannée <p>{t('aff_req.text1')}</p>
suivante.</p> {t('aff_req.text2')}
Pour saffilier, une association sportive doit réunir les conditions suivantes :
<ul> <ul>
<li>Avoir son siège social en France ou Principauté de Monaco</li> {t('aff_req.text2.li', {returnObjects: true}).map((text, index) => <li key={index}>{text}</li>)}
<li>Être constituée conformément au chapitre 1er du titre II du livre 1er du Code du Sport</li>
<li>Poursuivre un objet social entrant dans la définition de larticle 1 des statuts de la Fédération</li>
<li>Disposer de statuts compatibles avec les principes dorganisation et de fonctionnement de la
Fédération
</li>
<li>Assurer en son sein la liberté dopinion et le respect des droits de la défense, et sinterdire toute
discrimination
</li>
<li>Respecter les règles dencadrement, dhygiène et de sécurité établies par les règlements de la
Fédération
</li>
</ul> </ul>
{initData && <div className="card mb-4"> {initData && <div className="card mb-4">
<form onSubmit={submit}> <form onSubmit={submit}>
<div className="card-body"> <div className="card-body">
<h4>L'association</h4> <h4>{t('aff_req.association')}</h4>
<AssoInfo initData={initData} needFile={needFile}/> <AssoInfo initData={initData} needFile={needFile}/>
<h4>Membre n°1</h4> <h4>{t("aff.membreNo", {no: 1})}</h4>
<MembreInfo role="m1" initData={initData.members?.at(0) || {}}/> <MembreInfo role="m1" initData={initData.members?.at(0) || {}}/>
<h4 style={{marginTop: '1em'}}>Membre n°2</h4> <h4 style={{marginTop: '1em'}}>{t("aff.membreNo", {no: 2})}</h4>
<MembreInfo role="m2" initData={initData.members?.at(1) || {}}/> <MembreInfo role="m2" initData={initData.members?.at(1) || {}}/>
<h4 style={{marginTop: '1em'}}>Membre n°3</h4> <h4 style={{marginTop: '1em'}}>{t("aff.membreNo", {no: 3})}</h4>
<MembreInfo role="m3" initData={initData.members?.at(2) || {}}/> <MembreInfo role="m3" initData={initData.members?.at(2) || {}}/>
<div className="mb-3" style={{marginTop: '1em'}}> <div className="mb-3" style={{marginTop: '1em'}}>
{!initData.id && <p>Après validation de votre demande, vous recevrez un identifiant et mot de passe provisoire pour {!initData.id && <p>{t('aff_req.text3')}</p>}
accéder à votre espace FFSAF</p>} {t('aff_req.text4')}
Notez que pour finaliser votre affiliation, il vous faudra :
<ul> <ul>
<li>Disposer dau moins trois membres licenciés, dont le président</li> <li>{t('aff_req.text4.li1')}</li>
<li>S'être acquitté des cotisations prévues par les règlements fédéraux</li> <li>{t('aff_req.text4.li2')}</li>
</ul> </ul>
</div> </div>
{!initData.id ? {!initData.id ?
<div className="row"> <div className="row">
<div className="d-grid gap-2 d-md-flex justify-content-md-center"> <div className="d-grid gap-2 d-md-flex justify-content-md-center">
<button type="submit" value="new" className="btn btn-primary">Confirmer ma demande d'affiliation <button type="submit" value="new" className="btn btn-primary">{t('aff_req.button.confirm')}</button>
</button>
</div> </div>
</div> : </div> :
<div className="row"> <div className="row">
<div className="d-grid gap-2 d-md-flex justify-content-md-center col"> <div className="d-grid gap-2 d-md-flex justify-content-md-center col">
<button type="submit" value="undo" className="btn btn-danger">Annuler ma demande <button type="submit" value="undo" className="btn btn-danger">{t('aff_req.button.cancel')}</button>
</button>
</div> </div>
<div className="d-grid gap-2 d-md-flex justify-content-md-center col"> <div className="d-grid gap-2 d-md-flex justify-content-md-center col">
<button type="submit" value="edit" className="btn btn-primary">Enregistrer les modifications <button type="submit" value="edit" className="btn btn-primary">{t('aff_req.button.save')}</button>
</button>
</div> </div>
</div> </div>
} }
@ -210,29 +169,21 @@ function AssoInfo({initData, needFile}) {
const [stateId, setStateId] = useState(initData.stateId ? String(initData.stateId) : (initData.state_id ? String(initData.state_id) : "")) const [stateId, setStateId] = useState(initData.stateId ? String(initData.stateId) : (initData.state_id ? String(initData.state_id) : ""))
const [adresse, setAdresse] = useState(initData.address ? initData.address : "") const [adresse, setAdresse] = useState(initData.address ? initData.address : "")
const [contact, setContact] = useState(initData.contact ? initData.contact : "") const [contact, setContact] = useState(initData.contact ? initData.contact : "")
const {t} = useTranslation();
const fetchStateId = () => { const fetchStateId = () => {
const regex = /^(?:\d{14}|W\d{9})$/; const regex = /^(?:\d{14}|W\d{9})$/;
let sid = stateId; let sid = stateId;
if (!regex.test(stateId)) { if (!regex.test(stateId)) {
toast.error("Le format du SIRET/RNA est invalide"); toast.error(t('aff_req.error2'));
return; return;
}else{ } else {
if (stateId[0] !== 'W') if (stateId[0] !== 'W')
sid = stateId.substring(0, 9); // Pour les SIRET, on ne garde que les 9 premiers chiffres (SIREN) sid = stateId.substring(0, 9); // Pour les SIRET, on ne garde que les 9 premiers chiffres (SIREN)
} }
toast.promise( toast.promise(
apiAxios.get(`asso/state_id/${sid}`), apiAxios.get(`asso/state_id/${sid}`), getToastMessage("aff_req.toast.search")
{
pending: "Recherche de l'association en cours",
success: "Association trouvée avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de la recherche de l'association")
}
}
}
).then(data => { ).then(data => {
const data2 = data.data const data2 = data.data
setDenomination(data2.identite.nom) setDenomination(data2.identite.nom)
@ -244,44 +195,42 @@ function AssoInfo({initData, needFile}) {
<input name="saison" value={getSaisonToAff()} readOnly hidden/> <input name="saison" value={getSaisonToAff()} readOnly hidden/>
<div className="input-group mb-3"> <div className="input-group mb-3">
<span className="input-group-text" id="basic-addon1">Nom de l'association*</span> <span className="input-group-text" id="basic-addon1">{t('aff_req.nomDeLassociation')}*</span>
<input type="text" className="form-control" placeholder="Nom de l'association" name="name" <input type="text" className="form-control" placeholder={t('aff_req.nomDeLassociation')} name="name"
aria-label="Nom de l'association" aria-label={t('aff_req.nomDeLassociation')}
aria-describedby="basic-addon1" required defaultValue={initData.name ? initData.name : ""}/> aria-describedby="basic-addon1" required defaultValue={initData.name ? initData.name : ""}/>
</div> </div>
<div className="input-group mb-3"> <div className="input-group mb-3">
<span className="input-group-text">N° SIRET ou RNA*</span> <span className="input-group-text">{t('noSiretOuRna')}*</span>
<input type="text" className="form-control" placeholder="N° SIRET ou RNA*" name="state_id" required value={stateId} disabled={!needFile} <input type="text" className="form-control" placeholder={t('noSiretOuRna')} name="state_id" required value={stateId} disabled={!needFile}
onChange={e => setStateId(e.target.value)}/> onChange={e => setStateId(e.target.value)}/>
<button className="btn btn-outline-secondary" type="button" id="button-addon2" <button className="btn btn-outline-secondary" type="button" id="button-addon2"
onClick={fetchStateId} hidden={false}>Rechercher onClick={fetchStateId} hidden={false}>{t('rechercher')}
</button> </button>
</div> </div>
<div className="input-group mb-3" hidden={false}> <div className="input-group mb-3" hidden={false}>
<span className="input-group-text" id="basic-addon1">Dénomination</span> <span className="input-group-text" id="basic-addon1">{t('aff_req.denomination')}</span>
<input type="text" className="form-control" placeholder="Appuyer sur rechercher pour compléter" <input type="text" className="form-control" placeholder={t('aff_req.appuyerSurRechercher')}
aria-label="Dénomination" aria-label={t('aff_req.denomination')}
aria-describedby="basic-addon1" disabled value={denomination} readOnly/> aria-describedby="basic-addon1" disabled value={denomination} readOnly/>
</div> </div>
<div className="mb-3"> <div className="mb-3">
<div className="input-group"> <div className="input-group">
<span className="input-group-text" id="basic-addon1">Adresse administrative*</span> <span className="input-group-text" id="basic-addon1">{t('adresseAdministrative')}*</span>
<input type="text" className="form-control" placeholder="Adresse administrative" aria-label="Adresse administrative" <input type="text" className="form-control" placeholder={t('adresseAdministrative')} aria-label={t('adresseAdministrative')}
aria-describedby="basic-addon1" aria-describedby="basic-addon1"
required value={adresse} name="adresse" onChange={e => setAdresse(e.target.value)}/> required value={adresse} name="adresse" onChange={e => setAdresse(e.target.value)}/>
</div> </div>
<div className="form-text" id="adresse">Vous pourrez par la suite, ajouter des adresses visibles publiquement pour vos lieux <div className="form-text" id="adresse">{t('aff_req.text5')}</div>
d'entrainement
</div>
</div> </div>
<div className="mb-3"> <div className="mb-3">
<div className="input-group"> <div className="input-group">
<span className="input-group-text" id="basic-addon1">Contact administratif*</span> <span className="input-group-text" id="basic-addon1">{t('contactAdministratif')}*</span>
<input type="email" className="form-control" placeholder="Contact administratif" aria-label="Contact administratif" <input type="email" className="form-control" placeholder={t('contactAdministratif')} aria-label={t('contactAdministratif')}
aria-describedby="basic-addon1" aria-describedby="basic-addon1"
required value={contact} name="contact" onChange={e => setContact(e.target.value)}/> required value={contact} name="contact" onChange={e => setContact(e.target.value)}/>
</div> </div>
@ -289,70 +238,69 @@ function AssoInfo({initData, needFile}) {
<div className="mb-3"> <div className="mb-3">
<div className="input-group"> <div className="input-group">
<label className="input-group-text" htmlFor="logo">Blason{needFile && "*"}</label> <label className="input-group-text" htmlFor="logo">{t('blason')}{needFile && "*"}</label>
<input type="file" className="form-control" id="logo" name="logo" accept=".jpg,.jpeg,.gif,.png,.svg" <input type="file" className="form-control" id="logo" name="logo" accept=".jpg,.jpeg,.gif,.png,.svg"
required={needFile}/> required={needFile}/>
</div> </div>
{!needFile && <div className="form-text" id="status">Laissez vide pour ne rien changer. (Si un blason a déjà été envoyé lors de cette {!needFile && <div className="form-text" id="status">{t('aff_req.text6')}</div>}
demande, il sera utilisé, sinon nous utiliserons celui de la précédant affiliation)</div>}
</div> </div>
<div className="mb-3"> <div className="mb-3">
<div className="input-group"> <div className="input-group">
<label className="input-group-text" htmlFor="status">Statuts{needFile && "*"}</label> <label className="input-group-text" htmlFor="status">{t('statuts')}{needFile && "*"}</label>
<input type="file" className="form-control" id="status" name="status" accept=".pdf,.txt" required={needFile}/> <input type="file" className="form-control" id="status" name="status" accept=".pdf,.txt" required={needFile}/>
</div> </div>
{!needFile && <div className="form-text" id="status">Laissez vide pour ne rien changer. (Si un statu a déjà été envoyé lors de cette {!needFile && <div className="form-text" id="status">{t('aff_req.text7')}</div>}
demande, il sera utilisé, sinon nous utiliserons celui de la précédant affiliation)</div>}
</div> </div>
</>; </>;
} }
function MembreInfo({role, initData}) { function MembreInfo({role, initData}) {
const [switchOn, setSwitchOn] = useState(!!initData.licence); const [switchOn, setSwitchOn] = useState(!!initData.licence);
const {t} = useTranslation();
return <> return <>
<div className="input-group mb-3"> <div className="input-group mb-3">
<label className="input-group-text" htmlFor="inputGroupSelect01">Rôle</label> <label className="input-group-text" htmlFor="inputGroupSelect01">{t('role')}</label>
<select className="form-select" id="inputGroupSelect01" defaultValue={initData.role ? initData.role : (role === "m1" ? "PRESIDENT" : 0)} <select className="form-select" id="inputGroupSelect01" defaultValue={initData.role ? initData.role : (role === "m1" ? "PRESIDENT" : 0)}
disabled={initData.role ? initData.role === "PRESIDENT" : role === "m1"} name={role + "_role"} required> disabled={initData.role ? initData.role === "PRESIDENT" : role === "m1"} name={role + "_role"} required>
<option value="0">Sélectionner...</option> <option value="0">{t('selectionner...')}</option>
<option value="PRESIDENT">Président</option> <option value="PRESIDENT">{t('role.président')}</option>
<option value="TRESORIER">Trésorier</option> <option value="TRESORIER">{t('role.trésorier')}</option>
<option value="SECRETAIRE">Secrétaire</option> <option value="SECRETAIRE">{t('role.secrétaire')}</option>
<option value="VPRESIDENT">Vise-Président</option> <option value="VPRESIDENT">{t('role.vise-président')}</option>
<option value="VTRESORIER">Vise-Trésorier</option> <option value="VTRESORIER">{t('role.vise-trésorier')}</option>
<option value="VSECRETAIRE">Vise-Secrétaire</option> <option value="VSECRETAIRE">{t('role.vise-secrétaire')}</option>
<option value="MEMBREBUREAU">Membre du bureau</option> <option value="MEMBREBUREAU">{t('role.membreDuBureau')}</option>
</select> </select>
</div> </div>
<div className="row g-3 mb-3"> <div className="row g-3 mb-3">
<div className="col-sm-3"> <div className="col-sm-3">
<div className="form-floating"> <div className="form-floating">
<input type="text" className="form-control" id="floatingInput" placeholder="Nom" name={role + "_nom"} <input type="text" className="form-control" id="floatingInput" placeholder={t('nom')} name={role + "_nom"}
defaultValue={initData.lname ? initData.lname : ""} required/> defaultValue={initData.lname ? initData.lname : ""} required/>
<label htmlFor="floatingInput">Nom*</label> <label htmlFor="floatingInput">{t('nom')}*</label>
</div> </div>
</div> </div>
<div className="col-sm-3"> <div className="col-sm-3">
<div className="form-floating"> <div className="form-floating">
<input type="text" className="form-control" id="floatingInput" placeholder="Prénom" <input type="text" className="form-control" id="floatingInput" placeholder={t('prenom')}
name={role + "_prenom"} defaultValue={initData.fname ? initData.fname : ""} required/> name={role + "_prenom"} defaultValue={initData.fname ? initData.fname : ""} required/>
<label htmlFor="floatingInput">Prénom*</label> <label htmlFor="floatingInput">{t('prenom')}*</label>
</div> </div>
</div> </div>
<div className="col-sm-5"> <div className="col-sm-5">
<div className="form-floating"> <div className="form-floating">
<input type="email" className="form-control" id="floatingInput" placeholder="name@example.com" <input type="email" className="form-control" id="floatingInput" placeholder="name@example.com"
name={role + "_mail"} defaultValue={initData.email ? initData.email : ""} required/> name={role + "_mail"} defaultValue={initData.email ? initData.email : ""} required/>
<label htmlFor="floatingInput">Email*</label> <label htmlFor="floatingInput">{t('email')}*</label>
</div> </div>
</div> </div>
</div> </div>
<div className="input-group mb-3"> <div className="input-group mb-3">
<label className="input-group-text" htmlFor="inputGroupSelect01">Dispose déjà d'une licence</label> <label className="input-group-text" htmlFor="inputGroupSelect01">{t('aff_req.disposeLicence')}</label>
<div className="input-group-text"> <div className="input-group-text">
<input type="checkbox" id="inputGroupSelect01" className="form-check-input mt-0" <input type="checkbox" id="inputGroupSelect01" className="form-check-input mt-0"
checked={switchOn} onChange={() => setSwitchOn(!switchOn)}/> checked={switchOn} onChange={() => setSwitchOn(!switchOn)}/>
@ -361,9 +309,9 @@ function MembreInfo({role, initData}) {
{switchOn && {switchOn &&
<div className="col-sm-3"> <div className="col-sm-3">
<div className="form-floating"> <div className="form-floating">
<input type="number" className="form-control" id="floatingInput" placeholder="N° Licence" <input type="number" className="form-control" id="floatingInput" placeholder={t('noLicence')}
name={role + "_licence"} defaultValue={initData.licence ? Number(initData.licence) : ""} required/> name={role + "_licence"} defaultValue={initData.licence ? Number(initData.licence) : ""} required/>
<label htmlFor="floatingInput">N° Licence</label> <label htmlFor="floatingInput">{t('noLicence')}</label>
</div> </div>
</div> </div>
} }
@ -372,11 +320,12 @@ function MembreInfo({role, initData}) {
} }
export function DemandeAffOk() { export function DemandeAffOk() {
const {t} = useTranslation();
return ( return (
<div> <div>
<h1 className="text-green-800 text-4xl">Demande d'affiliation envoyée avec succès</h1> <h1 className="text-green-800 text-4xl">{t('aff_req.text7')}</h1>
<p>Une fois votre demande validée, vous recevrez un identifiant et mot de passe provisoire pour accéder à votre <p>{t('aff_req.text8')}</p>
espace FFSAF</p>
</div> </div>
); );
} }

View File

@ -1,42 +1,31 @@
import {faUser, faUsers} from "@fortawesome/free-solid-svg-icons"; import {faUser, faUsers} from "@fortawesome/free-solid-svg-icons";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {Trans, useTranslation} from "react-i18next";
export const Home = () => { export const Home = () => {
const {t} = useTranslation();
return <> return <>
<div className="container"> <div className="container">
<div style={{textAlign: "center", margin: "2em"}}> <div style={{textAlign: "center", margin: "2em"}}>
<h1 className="text-green-800 text-4xl">Bienvenue sur lintranet de la Fédération France Soft Armored Fighting</h1> <h1 className="text-green-800 text-4xl">{t("home.welcome_message")}</h1>
</div> </div>
<div className="row" style={{marginTop: "3em"}}> <div className="row" style={{marginTop: "3em"}}>
<div className="col" style={{backgroundColor: "#FFFFFF79", padding: "0", borderRadius: "3em 3em 1em 1em", margin: "1em"}}> <div className="col" style={{backgroundColor: "#FFFFFF79", padding: "0", borderRadius: "3em 3em 1em 1em", margin: "1em"}}>
<div className="align-content-center" <div className="align-content-center"
style={{textAlign: "center", backgroundColor: "#FFFFFF79", padding: "1em 1em 0em 1em", borderRadius: "3em 3em 0 0"}}> style={{textAlign: "center", backgroundColor: "#FFFFFF79", padding: "1em 1em 0em 1em", borderRadius: "3em 3em 0 0"}}>
<h2><FontAwesomeIcon icon={faUser} size="2xl"/></h2> <h2><FontAwesomeIcon icon={faUser} size="2xl"/></h2>
<h2>Pour les licenciés</h2> <h2>{t("home.header1")}</h2>
</div> </div>
<p style={{padding: "0.5em 1em 0.5em 1em"}}> <p style={{padding: "0.5em 1em 0.5em 1em"}}><Trans i18nKey="home.text1"></Trans></p>
Vous y retrouverez toutes vos informations ainsi que l'état de votre inscription à la fédération. Vous pouvez également
télécharger votre attestation d'inscription, vous inscrire aux compétitions ainsi que consulter vos résultats sous réserve que
le club organisateur les ait renseignés. <br/>
<br/>
Lors de votre première inscription, vous recevrez un email contenant vos informations d'identification, ce mail sera envoyé
une fois votre licence validée par le secrétariat.
</p>
</div> </div>
<div className="col" style={{backgroundColor: "#FFFFFF79", padding: "0", borderRadius: "3em 3em 1em 1em", margin: "1em"}}> <div className="col" style={{backgroundColor: "#FFFFFF79", padding: "0", borderRadius: "3em 3em 1em 1em", margin: "1em"}}>
<div className="align-content-center" <div className="align-content-center"
style={{textAlign: "center", backgroundColor: "#FFFFFF79", padding: "1em 1em 0em 1em", borderRadius: "3em 3em 0 0"}}> style={{textAlign: "center", backgroundColor: "#FFFFFF79", padding: "1em 1em 0em 1em", borderRadius: "3em 3em 0 0"}}>
<h2><FontAwesomeIcon icon={faUsers} size="2xl"/></h2> <h2><FontAwesomeIcon icon={faUsers} size="2xl"/></h2>
<h2>Pour les clubs</h2> <h2>{t("home.header2")}</h2>
</div> </div>
<p style={{padding: "0.5em 1em 0.5em 1em"}}> <p style={{padding: "0.5em 1em 0.5em 1em"}}><Trans i18nKey="home.text2">Cliquez <a href="/affiliation">içi</a> pour </Trans></p>
C'est ici que vous pouvez prendre les licences fédérales pour vos adhérents, que vous pouvez demander ou renouveler votre
affiliation, renseigner vos horaires, lieux d'entraînement et réseaux sociaux qui seront par la suite affichés sur
le site ffsaf.fr.<br/>
Vous aurez par ailleurs la possibilité de publier des formulaires d'inscriptions pour vos compétitions ainsi
que d'enregistrer les résultats.<br/><br/>
Vous n'êtes pas encore affilié à la fédération ? Cliquez <a href="/affiliation">içi</a> pour faire votre première demande.
</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -12,19 +12,20 @@ import {
faUserGroup, faUserGroup,
faVenus faVenus
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import {CheckField} from "../components/MemberCustomFiels.jsx";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {apiAxios, getCatName} from "../utils/Tools.js"; import {apiAxios, getCatName, getToastMessage} from "../utils/Tools.js";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {useTranslation} from "react-i18next";
const vite_url = import.meta.env.VITE_URL; const vite_url = import.meta.env.VITE_URL;
export function MePage() { export function MePage() {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/member/me`, setLoading, 1) const {data, error} = useFetch(`/member/me`, setLoading, 1)
const {t} = useTranslation();
return <div> return <div>
<h1>Mon espace</h1> <h1>{t("nav.space")}</h1>
{data {data
? <div> ? <div>
@ -52,10 +53,12 @@ export function MePage() {
} }
export function LicenceCard({userData}) { export function LicenceCard({userData}) {
const {t} = useTranslation();
return <div className="card mb-4 mb-md-0"> return <div className="card mb-4 mb-md-0">
<div className="card-header container-fluid"> <div className="card-header container-fluid">
<div className="row"> <div className="row">
<div className="col">Licence</div> <div className="col">{t('licence')}</div>
</div> </div>
</div> </div>
<div className="card-body"> <div className="card-body">
@ -73,8 +76,10 @@ export function LicenceCard({userData}) {
} }
function PhotoCard({data}) { function PhotoCard({data}) {
const {t} = useTranslation();
return <div className="card mb-4"> return <div className="card mb-4">
<div className="card-header">Licence n°{data.licence}</div> <div className="card-header">{t("licenceNo", {no: data.licence})}</div>
<div className="card-body text-center"> <div className="card-body text-center">
<div className="input-group mb-3"> <div className="input-group mb-3">
<img <img
@ -85,8 +90,8 @@ function PhotoCard({data}) {
<a href={`${vite_url}/api/member/me/licence`} target='#'> <a href={`${vite_url}/api/member/me/licence`} target='#'>
<button className="btn btn-primary" type="button" id="button-addon1" <button className="btn btn-primary" type="button" id="button-addon1"
onClick={e => null}> onClick={() => null}>
Téléchargée la licence <FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon> {t('téléchargéeLaLicence')} <FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon>
</button> </button>
</a> </a>
</div> </div>
@ -94,13 +99,15 @@ function PhotoCard({data}) {
} }
function SelectCard({userData}) { function SelectCard({userData}) {
const {t} = useTranslation();
return <div className="card mb-4 mb-md-0"> return <div className="card mb-4 mb-md-0">
<div className="card-header">Sélection en équipe de France</div> <div className="card-header">{t('sélectionEnéquipeDeFrance')}</div>
<div className="card-body"> <div className="card-body">
<ul className="list-group"> <ul className="list-group">
{userData?.selections && userData.selections.sort((a, b) => b.saison - a.saison).map((selection, index) => { {userData?.selections && userData.selections.sort((a, b) => b.saison - a.saison).map((selection, index) => {
return <div key={index} className="list-group-item d-flex justify-content-between align-items-start"> return <div key={index} className="list-group-item d-flex justify-content-between align-items-start">
<div className="me-auto">{selection?.saison}-{selection?.saison + 1} en {getCatName(selection?.categorie)}</div> <div className="me-auto">{selection?.saison}-{selection?.saison + 1} {t('en')} {getCatName(selection?.categorie)}</div>
</div> </div>
})} })}
</ul> </ul>
@ -110,6 +117,7 @@ function SelectCard({userData}) {
function SettingsCard({data}) { function SettingsCard({data}) {
const [privacy, setPrivacy] = useState("PUBLIC"); const [privacy, setPrivacy] = useState("PUBLIC");
const {t} = useTranslation();
useEffect(() => { useEffect(() => {
if (data?.resultPrivacy) { if (data?.resultPrivacy) {
@ -120,28 +128,22 @@ function SettingsCard({data}) {
const handleChange = (e) => { const handleChange = (e) => {
const formData = new FormData(); const formData = new FormData();
formData.append("resultPrivacy", e.target.value); formData.append("resultPrivacy", e.target.value);
toast.promise(apiAxios.put(`/member/me/setting`, formData), toast.promise(apiAxios.put(`/member/me/setting`, formData), getToastMessage("me.toast.settings"))
{
pending: 'Mise à jours des paramètres en cours...',
success: 'Paramètres mis à jours avec succès 🎉',
error: 'Échec de la mise à jours des paramètres 😕'
})
.then(() => { .then(() => {
setPrivacy(String(formData.get("resultPrivacy"))); setPrivacy(String(formData.get("resultPrivacy")));
}); });
} }
return <div className="card mb-4"> return <div className="card mb-4">
<div className="card-header">Paramètres du compte</div> <div className="card-header">{t('me.paramètresDuCompte')}</div>
<div className="card-body"> <div className="card-body">
<div className="input-group mb-3"> <div className="input-group mb-3">
<label className="input-group-text" htmlFor="email_notifications">Visibilité des résultats</label> <label className="input-group-text" htmlFor="email_notifications">{t('me.visibilitéDesRésultats')}</label>
<select className="form-select" id="result_visibility" name="result_visibility" required value={privacy} onChange={handleChange}> <select className="form-select" id="result_visibility" name="result_visibility" required value={privacy} onChange={handleChange}>
<option value="PUBLIC">Public (visible par tous)</option> <option value="PUBLIC">{t('me.result.PUBLIC')}</option>
<option value="REGISTERED_ONLY">Membres connectés (visibles par les membres de la fédération)</option> <option value="REGISTERED_ONLY">{t('me.result.REGISTERED_ONLY')}</option>
<option value="REGISTERED_ONLY_NO_DETAILS">Membres connectés - masquer les détails (visibles par les membres de la fédération) <option value="REGISTERED_ONLY_NO_DETAILS">{t('me.result.REGISTERED_ONLY_NO_DETAILS')}</option>
</option> <option value="PRIVATE">{t('me.result.PRIVATE')}</option>
<option value="PRIVATE">Privé (visible uniquement par moi)</option>
</select> </select>
</div> </div>
</div> </div>
@ -150,9 +152,10 @@ function SettingsCard({data}) {
function InformationForm({data}) { function InformationForm({data}) {
const style = {marginRight: '0.7em'} const style = {marginRight: '0.7em'}
const {t} = useTranslation();
return <div className="card mb-4"> return <div className="card mb-4">
<div className="card-header">Information</div> <div className="card-header">{t('information')}</div>
<div className="card-body"> <div className="card-body">
<div className="row mb-2"> <div className="row mb-2">
<p> <p>
@ -163,16 +166,16 @@ function InformationForm({data}) {
|| <FontAwesomeIcon icon={faMarsAndVenus} style={style}/>}{data.genre}<br/> || <FontAwesomeIcon icon={faMarsAndVenus} style={style}/>}{data.genre}<br/>
<FontAwesomeIcon icon={faCalendarDay} style={style}/>{data.birth_date ? data.birth_date.split('T')[0] : ''}<br/> <FontAwesomeIcon icon={faCalendarDay} style={style}/>{data.birth_date ? data.birth_date.split('T')[0] : ''}<br/>
<FontAwesomeIcon icon={faUserGroup} style={style}/>{data.categorie}<br/> <FontAwesomeIcon icon={faUserGroup} style={style}/>{data.categorie}<br/>
<FontAwesomeIcon icon={faFlag} style={style}/>Nationalité : <img src={"/flags/flags_" + data.country.toLowerCase() + ".png"} <FontAwesomeIcon icon={faFlag} style={style}/>{t('nationalité')} : <img src={"/flags/flags_" + data.country.toLowerCase() + ".png"}
alt=""/><br/> alt=""/><br/>
<FontAwesomeIcon icon={faInfoCircle} style={style}/>Club : {data.club}<br/> <FontAwesomeIcon icon={faInfoCircle} style={style}/>{t("club", {count: 1})} : {data.club}<br/>
<FontAwesomeIcon icon={faInfoCircle} style={style}/>Rôle au sien du club : {data.role}<br/> <FontAwesomeIcon icon={faInfoCircle} style={style}/>{t('me.rôleAuSienDuClub')} : {data.role}<br/>
<FontAwesomeIcon icon={faInfoCircle} style={style}/>Formation d'arbitrage : {data.grade_arbitrage} <FontAwesomeIcon icon={faInfoCircle} style={style}/>{t('me.formationDarbitrage')} : {data.grade_arbitrage}
</p> </p>
<div> <div>
<button className="btn btn-primary" type="button" id="button-addon1" <button className="btn btn-primary" type="button" id="button-addon1"
onClick={_ => window.location.href = "https://auth.ffsaf.fr/realms/ffsaf/login-actions/reset-credentials?client_id=ffsaf-client"}> onClick={_ => window.location.href = "https://auth.ffsaf.fr/realms/ffsaf/login-actions/reset-credentials?client_id=ffsaf-client"}>
Changer mon mot de passe {t('me.changerMonMotDePasse')}
</button> </button>
</div> </div>
</div> </div>

View File

@ -6,12 +6,13 @@ import {useEffect, useState} from "react";
import {useLocation, useNavigate} from "react-router-dom"; import {useLocation, useNavigate} from "react-router-dom";
import {Checkbox} from "../components/MemberCustomFiels.jsx"; import {Checkbox} from "../components/MemberCustomFiels.jsx";
import * as Tools from "../utils/Tools.js"; import * as Tools from "../utils/Tools.js";
import {apiAxios, errFormater, getCatName} from "../utils/Tools.js"; import {apiAxios, getCatName, getToastMessage} from "../utils/Tools.js";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {SearchBar} from "../components/SearchBar.jsx"; import {SearchBar} from "../components/SearchBar.jsx";
import * as XLSX from "xlsx-js-style"; import * as XLSX from "xlsx-js-style";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faEuroSign} from "@fortawesome/free-solid-svg-icons"; import {faEuroSign} from "@fortawesome/free-solid-svg-icons";
import {useTranslation} from "react-i18next";
let lastRefresh = ""; let lastRefresh = "";
@ -19,6 +20,7 @@ let lastRefresh = "";
export function MemberList({source}) { export function MemberList({source}) {
const {hash} = useLocation(); const {hash} = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const {t} = useTranslation();
const [memberData, setMemberData] = useState([]); const [memberData, setMemberData] = useState([]);
const [licenceData, setLicenceData] = useState([]); const [licenceData, setLicenceData] = useState([]);
@ -82,16 +84,7 @@ export function MemberList({source}) {
return; return;
toast.promise( toast.promise(
apiAxios.get(`/licence/current/${source}`), apiAxios.get(`/licence/current/${source}`), getToastMessage("membre.toast.licences.load"))
{
pending: "Chargement des licences...",
success: "Licences chargées",
error: {
render({data}) {
return errFormater(data, "Impossible de charger les licences")
}
}
})
.then(data => { .then(data => {
setLicenceData(data.data); setLicenceData(data.data);
}); });
@ -118,24 +111,24 @@ export function MemberList({source}) {
</div> </div>
<div className="col-lg-3"> <div className="col-lg-3">
<div className="mb-4"> <div className="mb-4">
<button className="btn btn-primary" onClick={() => navigate("new")}>Ajouter un membre</button> <button className="btn btn-primary" onClick={() => navigate("new")}>{t('ajouterUnMembre')}</button>
{source === "admin" && {source === "admin" &&
<button className="btn btn-primary" onClick={() => navigate("validate")} style={{marginTop: "0.5rem"}}>Valider des <button className="btn btn-primary" onClick={() => navigate("validate")}
licences</button>} style={{marginTop: "0.5rem"}}>{t('validerDesLicences')}</button>}
{source === "club" && false && // TODO: enable when payment is ready {source === "club" && false && // TODO: enable when payment is ready
<button className="btn btn-primary" onClick={() => navigate("pay")} style={{marginTop: "0.5rem"}}>Paiement des <button className="btn btn-primary" onClick={() => navigate("pay")}
licences</button>} style={{marginTop: "0.5rem"}}>{t('paiementDesLicences')}</button>}
</div> </div>
<div className="card mb-4"> <div className="card mb-4">
<div className="card-header">Trie</div> <div className="card-header">{t('trie')}</div>
<div className="card-body"> <div className="card-body">
<OrderBar onOrderChange={e => setFilter({...filter, order: e.join(",")})} defaultValues={filter.order} source={source}/> <OrderBar onOrderChange={e => setFilter({...filter, order: e.join(",")})} defaultValues={filter.order} source={source}/>
</div> </div>
</div> </div>
<div className="card mb-4"> <div className="card mb-4">
<div className="card-header">Filtre</div> <div className="card-header">{t('filtre')}</div>
<div className="card-body"> <div className="card-body">
<FiltreBar showLicenceState={showLicenceState} <FiltreBar showLicenceState={showLicenceState}
setShowLicenceState={setShowLicenceState} setShowLicenceState={setShowLicenceState}
@ -155,7 +148,7 @@ export function MemberList({source}) {
{source === "club" && {source === "club" &&
<div className="card mb-4"> <div className="card mb-4">
<div className="card-header">Gestion groupée</div> <div className="card-header">{t('gestionGroupée')}</div>
<div className="card-body"> <div className="card-body">
<FileOutput/> <FileOutput/>
<div style={{marginTop: "1.5em"}}></div> <div style={{marginTop: "1.5em"}}></div>
@ -169,6 +162,7 @@ export function MemberList({source}) {
} }
function FileOutput() { function FileOutput() {
const {t} = useTranslation();
function formatColumnDate(worksheet, col) { function formatColumnDate(worksheet, col) {
const range = XLSX.utils.decode_range(worksheet['!ref']) const range = XLSX.utils.decode_range(worksheet['!ref'])
// note: range.s.r + 1 skips the header row // note: range.s.r + 1 skips the header row
@ -185,16 +179,7 @@ function FileOutput() {
const handleFileDownload = () => { const handleFileDownload = () => {
toast.promise( toast.promise(
apiAxios.get(`/member/club/export`), apiAxios.get(`/member/club/export`), getToastMessage("membre.toast.licences.export"))
{
pending: "Exportation des licences...",
success: "Licences exportées",
error: {
render({data}) {
return errFormater(data, "Impossible d'exporté les licences")
}
}
})
.then(data => { .then(data => {
const dataOut = [] const dataOut = []
for (const e of data.data) { for (const e of data.data) {
@ -241,14 +226,15 @@ function FileOutput() {
return ( return (
<div> <div>
<button className="btn btn-primary" onClick={handleFileDownload}>Télécharger l'Excel des membres</button> <button className="btn btn-primary" onClick={handleFileDownload}>{t('téléchargerLexcelDesMembres')}</button>
<small>À utiliser comme template pour mettre à jour les informations</small> <small>{t('téléchargerLexcelDesMembres.info')}</small>
</div> </div>
); );
} }
function FileInput() { function FileInput() {
const {t} = useTranslation();
const re = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i; const re = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i;
function excelDateToJSDate(serial) { function excelDateToJSDate(serial) {
@ -284,21 +270,21 @@ function FileInput() {
} }
if (tmp.nom === undefined || tmp.nom === "") { if (tmp.nom === undefined || tmp.nom === "") {
toast.error("Nom vide à la ligne " + (i + 2)) toast.error(t('membre.nomVideàLaLigne', {no: i + 2}))
error++; error++;
} }
if (tmp.prenom === undefined || tmp.prenom === "") { if (tmp.prenom === undefined || tmp.prenom === "") {
toast.error("Prénom vide à la ligne " + (i + 2)) toast.error(t('membre.prénomVideàLaLigne', {no: i + 2}))
error++; error++;
} }
if (tmp.licenceCurrent) { // need check full data if (tmp.licenceCurrent) { // need check full data
if (tmp.email === undefined || tmp.email === "") { if (tmp.email === undefined || tmp.email === "") {
toast.error("Email vide à la ligne " + (i + 2)) toast.error(t('membre.emailVideàLaLigne', {no: i + 2}))
error++; error++;
} }
if (!re.test(tmp.email)) { if (!re.test(tmp.email)) {
toast.error("Email invalide à la ligne " + (i + 2)) toast.error(t('membre.import.err5', {no: i + 2}))
error++; error++;
} }
// noinspection NonAsciiCharacters,JSNonASCIINames // noinspection NonAsciiCharacters,JSNonASCIINames
@ -309,20 +295,20 @@ function FileInput() {
try { try {
const date = excelDateToJSDate(line["Date certificat"]); const date = excelDateToJSDate(line["Date certificat"]);
if (Number.isNaN(date.getFullYear())) { if (Number.isNaN(date.getFullYear())) {
toast.error("Format de la date de certificat invalide à la ligne " + (i + 2)) toast.error(t('membre.import.err1', {no: i + 2}))
error++; error++;
} else { } else {
// noinspection JSNonASCIINames // noinspection JSNonASCIINames
tmp.certif = line["Nom médecin certificat"] + "¤" + date.getFullYear() + "-" + ("0" + (date.getMonth() + 1)).slice(-2) + "-" + ("0" + date.getDate()).slice(-2); tmp.certif = line["Nom médecin certificat"] + "¤" + date.getFullYear() + "-" + ("0" + (date.getMonth() + 1)).slice(-2) + "-" + ("0" + date.getDate()).slice(-2);
} }
} catch (e) { } catch (e) {
toast.error("Format de la date de certificat invalide à la ligne " + (i + 2)) toast.error(t('membre.import.err2', {no: i + 2}))
error++; error++;
} }
} }
if (tmp.birthdate === undefined || tmp.birthdate === "") { if (tmp.birthdate === undefined || tmp.birthdate === "") {
toast.error("Date de naissance vide à la ligne " + (i + 2)) toast.error(t('membre.import.err3', {no: i + 2}))
error++; error++;
} }
} }
@ -332,7 +318,7 @@ function FileInput() {
try { try {
tmp.birthdate = excelDateToJSDate(tmp.birthdate).toISOString(); tmp.birthdate = excelDateToJSDate(tmp.birthdate).toISOString();
} catch (e) { } catch (e) {
toast.error("Format de la date de naissance invalide à la ligne " + (i + 2)) toast.error(t('membre.import.err4', {no: i + 2}))
error++; error++;
} }
} }
@ -341,24 +327,15 @@ function FileInput() {
} }
if (error > 0) { if (error > 0) {
toast.error(`${error} erreur(s) dans le fichier, opération annulée`) toast.error(t('membre.import.errTT', {count: error}))
} else { } else {
console.log(dataOut); console.log(dataOut);
toast.promise( toast.promise(
apiAxios.put(`/member/club/import`, dataOut), apiAxios.put(`/member/club/import`, dataOut), getToastMessage("membre.toast.licences.import")
{
pending: "Envoie des changement en cours",
success: "Changement envoyé avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de l'envoie des changements")
}
}
}
).then(_ => { ).then(_ => {
if (cetifNotFill > 0) if (cetifNotFill > 0)
toast.warn(`${cetifNotFill} certificat(s) médical(aux) non rempli(s)`) toast.warn(t('membre.import.warn', {count: cetifNotFill}))
}) })
} }
}; };
@ -368,16 +345,18 @@ function FileInput() {
return ( return (
<div> <div>
<span>Charger l'Excel</span> <span>{t('chargerLexcel')}</span>
<div className="input-group"> <div className="input-group">
<input type="file" className="form-control" id="logo" name="logo" accept=".xls,.xlsx" onChange={handleFileUpload}/> <input type="file" className="form-control" id="logo" name="logo" accept=".xls,.xlsx" onChange={handleFileUpload}/>
</div> </div>
<small>Merci d'utiliser le fichier ci-dessus comme base, ne pas renommer les colonnes ni modifier les n° de licences.</small> <small>{t('chargerLexcel.msg')}</small>
</div> </div>
); );
} }
function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page, setPage, source}) { function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page, setPage, source}) {
const {t} = useTranslation();
const pages = [] const pages = []
for (let i = 1; i <= data.page_count; i++) { for (let i = 1; i <= data.page_count; i++) {
pages.push(<li key={i} className={"page-item " + ((page === i) ? "active" : "")}> pages.push(<li key={i} className={"page-item " + ((page === i) ? "active" : "")}>
@ -387,8 +366,12 @@ function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page
return <> return <>
<div className="mb-4"> <div className="mb-4">
<small>Ligne {((page - 1) * data.page_size) + 1} à { <small>{t("page_info_full", {
(page * data.page_size > data.result_count) ? data.result_count : (page * data.page_size)} (page {page} sur {data.page_count})</small> line: ((page - 1) * data.page_size) + 1,
tt_line: (page * data.page_size > data.result_count) ? data.result_count : (page * data.page_size),
page: page,
tt_page: data.page_count,
})}</small>
<div className="list-group"> <div className="list-group">
{visibleMember.map(member => ( {visibleMember.map(member => (
<MakeRow key={member.id} member={member} navigate={navigate} showLicenceState={showLicenceState} source={source}/>))} <MakeRow key={member.id} member={member} navigate={navigate} showLicenceState={showLicenceState} source={source}/>))}
@ -453,6 +436,7 @@ function MakeRow({member, showLicenceState, navigate, source}) {
} }
function OrderBar({onOrderChange, defaultValues = "", source}) { function OrderBar({onOrderChange, defaultValues = "", source}) {
const {t} = useTranslation();
const [orderCriteria, setOrderCriteria] = useState([...defaultValues.split(",").filter(c => c !== ''), '']); const [orderCriteria, setOrderCriteria] = useState([...defaultValues.split(",").filter(c => c !== ''), '']);
const handleChange = (index, value) => { const handleChange = (index, value) => {
@ -474,20 +458,20 @@ function OrderBar({onOrderChange, defaultValues = "", source}) {
// Liste de toutes les options possibles // Liste de toutes les options possibles
const allOptions = [ const allOptions = [
{value: 'lname n', label: 'Nom ↓', base: 'lname'}, {value: 'lname n', label: t('nom') + ' ↓', base: 'lname'},
{value: 'lname i', label: 'Nom ↑', base: 'lname'}, {value: 'lname i', label: t('nom') + ' ↑', base: 'lname'},
{value: 'fname n', label: 'Prénom ↓', base: 'fname'}, {value: 'fname n', label: t('prenom') + ' ↓', base: 'fname'},
{value: 'fname i', label: 'Prénom ↑', base: 'fname'}, {value: 'fname i', label: t('prenom') + ' ↑', base: 'fname'},
{value: 'categorie n', label: 'Catégorie ↓', base: 'categorie'}, {value: 'categorie n', label: t('catégorie') + ' ↓', base: 'categorie'},
{value: 'categorie i', label: 'Catégorie ↑', base: 'categorie'}, {value: 'categorie i', label: t('catégorie') + ' ↑', base: 'categorie'},
{value: 'licence n', label: 'Licence ↓', base: 'licence'}, {value: 'licence n', label: t('licence') + ' ↓', base: 'licence'},
{value: 'licence i', label: 'Licence ↑', base: 'licence'}, {value: 'licence i', label: t('licence') + ' ↑', base: 'licence'},
]; ];
if (source === "admin") { if (source === "admin") {
allOptions.push( allOptions.push(
{value: 'club.name n', label: 'Club ↓', base: 'club.name'}, {value: 'club.name n', label: t('club', {count: 1}) + ' ↓', base: 'club.name'},
{value: 'club.name i', label: 'Club ↑', base: 'club.name'}, {value: 'club.name i', label: t('club', {count: 1}) + ' ↑', base: 'club.name'},
); );
} }
@ -539,17 +523,18 @@ function FiltreBar({
catFilter, catFilter,
setCatFilter, setCatFilter,
}) { }) {
const {t} = useTranslation();
return <div> return <div>
<div className="mb-3"> <div className="mb-3">
<Checkbox value={showLicenceState} onChange={setShowLicenceState} label="Afficher l'état des licences"/> <Checkbox value={showLicenceState} onChange={setShowLicenceState} label={t('membre.filtre.licence')}/>
</div> </div>
<div className="mb-3"> <div className="mb-3">
<Checkbox value={showArchived} onChange={setShowArchived} name="checkbox2" label="Afficher les combattants inactifs"/> <Checkbox value={showArchived} onChange={setShowArchived} name="checkbox2" label={t('membre.filtre.inactif')}/>
</div> </div>
<div className="mb-3"> <div className="mb-3">
<select className="form-select" value={catFilter} onChange={event => setCatFilter(event.target.value)}> <select className="form-select" value={catFilter} onChange={event => setCatFilter(event.target.value)}>
<option value="">--- toute les catégories ---</option> <option value="">{t('---TouteLesCatégories---')}</option>
{Tools.CatList.map(cat => ( {Tools.CatList.map(cat => (
<option key={cat} value={cat}>{getCatName(cat)}</option> <option key={cat} value={cat}>{getCatName(cat)}</option>
))} ))}
@ -558,21 +543,21 @@ function FiltreBar({
{source !== "club" && <ClubSelectFilter clubFilter={clubFilter} setClubFilter={setClubFilter}/>} {source !== "club" && <ClubSelectFilter clubFilter={clubFilter} setClubFilter={setClubFilter}/>}
<div className="mb-3"> <div className="mb-3">
<select className="form-select" value={stateFilter} onChange={event => setStateFilter(Number(event.target.value))}> <select className="form-select" value={stateFilter} onChange={event => setStateFilter(Number(event.target.value))}>
<option value={4}>Tout les états de licences</option> <option value={4}>{t('membre.filtre.licences.4')}</option>
<option value={0}>Sans demande ni licence validée</option> <option value={0}>{t('membre.filtre.licences.0')}</option>
<option value={1}>Avec demande ou licence validée</option> <option value={1}>{t('membre.filtre.licences.1')}</option>
<option value={2}>Demande en cours</option> <option value={2}>{t('membre.filtre.licences.2')}</option>
<option value={5}>Demande complet</option> <option value={5}>{t('membre.filtre.licences.5')}</option>
<option value={6}>Demande incomplet</option> <option value={6}>{t('membre.filtre.licences.6')}</option>
<option value={3}>Licence validée</option> <option value={3}>{t('membre.filtre.licences.3')}</option>
</select> </select>
</div> </div>
<div className="mb-3"> <div className="mb-3">
<select className="form-select" value={paymentFilter} onChange={event => setPaymentFilter(Number(event.target.value))} <select className="form-select" value={paymentFilter} onChange={event => setPaymentFilter(Number(event.target.value))}
hidden={stateFilter === 0 || stateFilter === 4}> hidden={stateFilter === 0 || stateFilter === 4}>
<option value={2}>Tout les états de paiement</option> <option value={2}>{t('membre.filtre.payement.2')}</option>
<option value={0}>Sans paiement</option> <option value={0}>{t('membre.filtre.payement.0')}</option>
<option value={1}>Avec paiement</option> <option value={1}>{t('membre.filtre.payement.1')}</option>
</select> </select>
</div> </div>
</div> </div>
@ -581,13 +566,14 @@ function FiltreBar({
function ClubSelectFilter({clubFilter, setClubFilter}) { function ClubSelectFilter({clubFilter, setClubFilter}) {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/club/no_detail`, setLoading, 1) const {data, error} = useFetch(`/club/no_detail`, setLoading, 1)
const {t} = useTranslation();
return <> return <>
{data {data
? <div className="mb-3"> ? <div className="mb-3">
<select className="form-select" value={clubFilter} onChange={event => setClubFilter(event.target.value)}> <select className="form-select" value={clubFilter} onChange={event => setClubFilter(event.target.value)}>
<option value="">--- tout les clubs ---</option> <option value="">{t('---ToutLesClubs---')}</option>
<option value="null">--- sans club ---</option> <option value="null">{t('---SansClub---')}</option>
{data.map(club => (<option key={club.id} value={club.name}>{club.name}</option>))} {data.map(club => (<option key={club.id} value={club.name}>{club.name}</option>))}
</select> </select>
</div> </div>

View File

@ -4,7 +4,7 @@ import {AxiosError} from "../components/AxiosError.jsx";
import {ThreeDots} from "react-loader-spinner"; import {ThreeDots} from "react-loader-spinner";
import {useEffect, useRef, useState} from "react"; import {useEffect, useRef, useState} from "react";
import {useLocation, useNavigate} from "react-router-dom"; import {useLocation, useNavigate} from "react-router-dom";
import {apiAxios, errFormater, getCatName} from "../utils/Tools.js"; import {apiAxios, getCatName, getToastMessage} from "../utils/Tools.js";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {SearchBar} from "../components/SearchBar.jsx"; import {SearchBar} from "../components/SearchBar.jsx";
import {ConfirmDialog} from "../components/ConfirmDialog.jsx"; import {ConfirmDialog} from "../components/ConfirmDialog.jsx";
@ -12,8 +12,11 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faCircleInfo, faEuroSign} from "@fortawesome/free-solid-svg-icons"; import {faCircleInfo, faEuroSign} from "@fortawesome/free-solid-svg-icons";
import "./PayAndValidateList.css"; import "./PayAndValidateList.css";
import * as Tools from "../utils/Tools.js"; import * as Tools from "../utils/Tools.js";
import {useTranslation} from "react-i18next";
import {counter} from "@fortawesome/fontawesome-svg-core";
export function PayAndValidateList({source}) { export function PayAndValidateList({source}) {
const {t} = useTranslation();
const {hash} = useLocation(); const {hash} = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
@ -66,16 +69,7 @@ export function PayAndValidateList({source}) {
const fetchLicenceData = () => { const fetchLicenceData = () => {
toast.promise( toast.promise(
apiAxios.get(`/licence/current/${source}`), apiAxios.get(`/licence/current/${source}`), getToastMessage("membre.toast.licences.load"))
{
pending: "Chargement des licences...",
success: "Licences chargées",
error: {
render({data}) {
return errFormater(data, "Impossible de charger les licences")
}
}
})
.then(data => { .then(data => {
setLicenceData(data.data); setLicenceData(data.data);
}); });
@ -93,21 +87,12 @@ export function PayAndValidateList({source}) {
const handleValidation = () => { const handleValidation = () => {
if (selectedMembers.length === 0) { if (selectedMembers.length === 0) {
toast.error("Aucun membre sélectionné"); toast.error(t('aucunMembreSélectionné'));
return; return;
} }
toast.promise( toast.promise(
apiAxios.post(`/licence/validate`, selectedMembers), apiAxios.post(`/licence/validate`, selectedMembers), getToastMessage("toast.licence.bulk.valid")
{
pending: "Validation des licences en cours...",
success: "Licences validées avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de la validation des licences")
}
}
}
).then(() => { ).then(() => {
setSelectedMembers([]); setSelectedMembers([]);
refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}&categorie=${catFilter}`); refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}&categorie=${catFilter}`);
@ -116,21 +101,12 @@ export function PayAndValidateList({source}) {
const handlePayment = () => { const handlePayment = () => {
if (selectedMembers.length === 0) { if (selectedMembers.length === 0) {
toast.error("Aucun membre sélectionné"); toast.error(t('aucunMembreSélectionné'));
return; return;
} }
toast.promise( toast.promise(
apiAxios.post(`/licence/validate-pay`, selectedMembers), apiAxios.post(`/licence/validate-pay`, selectedMembers), getToastMessage("toast.licence.bulk.pay")
{
pending: "Validation des licences en cours...",
success: "Licences validées avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de la validation des licences")
}
}
}
).then(() => { ).then(() => {
setSelectedMembers([]); setSelectedMembers([]);
fetchLicenceData(); fetchLicenceData();
@ -145,16 +121,7 @@ export function PayAndValidateList({source}) {
} }
toast.promise( toast.promise(
apiAxios.post(`/licence/pay`, selectedMembers), apiAxios.post(`/licence/pay`, selectedMembers), getToastMessage("toast.licence.order")
{
pending: "Création de la commande en cours...",
success: "Commande crée avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de le création de la commande")
}
}
}
).then((data) => { ).then((data) => {
window.location.href = data.data; window.location.href = data.data;
}); });
@ -163,7 +130,7 @@ export function PayAndValidateList({source}) {
return <> return <>
<h2>Validation des licences</h2> <h2>Validation des licences</h2>
<button type="button" className="btn btn-link" onClick={() => navigate("../member")}> <button type="button" className="btn btn-link" onClick={() => navigate("../member")}>
&laquo; retour {t('back')}
</button> </button>
<div> <div>
<div className="row"> <div className="row">
@ -179,7 +146,7 @@ export function PayAndValidateList({source}) {
</div> </div>
<div className="col-lg-3"> <div className="col-lg-3">
<div className="card mb-4"> <div className="card mb-4">
<div className="card-header">Filtre</div> <div className="card-header">{t('filtre')}</div>
<div className="card-body"> <div className="card-body">
<FiltreBar data={data} clubFilter={clubFilter} setClubFilter={setClubFilter} source={source} <FiltreBar data={data} clubFilter={clubFilter} setClubFilter={setClubFilter} source={source}
stateFilter={stateFilter} setStateFilter={setStateFilter} paymentFilter={paymentFilter} stateFilter={stateFilter} setStateFilter={setStateFilter} paymentFilter={paymentFilter}
@ -189,38 +156,34 @@ export function PayAndValidateList({source}) {
<div className="mb-4"> <div className="mb-4">
{source === "admin" && <> {source === "admin" && <>
<button className="btn btn-primary" data-bs-toggle="modal" data-bs-target="#confirm-pay">Valider le payement <button className="btn btn-primary" data-bs-toggle="modal"
des {selectedMembers.length} licences sélectionnée data-bs-target="#confirm-pay">{t('validerLePayement', {count: selectedMembers.length})}
</button> </button>
<ConfirmDialog title="Payment des licences" <ConfirmDialog title={t('paymentDesLicences')}
message={"Êtes-vous sûr de vouloir marquer comme payées les " + selectedMembers.length + " licences ?"} message={t('paymentDesLicences.msg', {count: selectedMembers.length})}
onConfirm={handlePayment} id="confirm-pay"/> onConfirm={handlePayment} id="confirm-pay"/>
<button className="btn btn-primary" data-bs-toggle="modal" data-bs-target="#confirm-validation" style={{marginTop: "0.5em"}}>Valider <button className="btn btn-primary" data-bs-toggle="modal" data-bs-target="#confirm-validation"
les {selectedMembers.length} licences sélectionnée style={{marginTop: "0.5em"}}>{t('validerLicence', {count: selectedMembers.length})}
</button> </button>
<ConfirmDialog title="Validation des licences" <ConfirmDialog title={t('validationDesLicences')}
message={"Êtes-vous sûr de vouloir valider les " + selectedMembers.length + " licences ?"} message={t('validerLicence.msg', {count: selectedMembers.length})}
onConfirm={handleValidation} id="confirm-validation"/> onConfirm={handleValidation} id="confirm-validation"/>
</>} </>}
{source === "club" && <> {source === "club" && <>
<span>{selectedMembers.length} licences sélectionnée<br/>Total à régler : {selectedMembers.length * 15}</span> <span>{t("payment.recap", {count: selectedMembers.length, total: selectedMembers.length * 15})}</span>
<HaPay onClick={handlePay}/> <HaPay onClick={handlePay}/>
<div className="card"> <div className="card">
<div className="card-header"> <div className="card-header">
<FontAwesomeIcon icon={faCircleInfo} size="2xl" style={{color: "#74C0FC", marginRight: "0.25em"}}/> <FontAwesomeIcon icon={faCircleInfo} size="2xl" style={{color: "#74C0FC", marginRight: "0.25em"}}/>
A propos de HelloAsso {t('payment.info')}
</div>
<div className="card-body">
Le modèle solidaire de HelloAsso garantit que 100% de votre paiement sera versé à lassociation choisie. Vous
pouvez soutenir laide quils apportent aux associations en laissant une contribution volontaire à HelloAsso au
moment de votre paiement.
</div> </div>
<div
className="card-body">{t('payment.ha.info')}</div>
</div> </div>
</>} </>}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@ -228,6 +191,7 @@ export function PayAndValidateList({source}) {
} }
function HaPay({onClick}) { function HaPay({onClick}) {
const {t} = useTranslation();
return <> return <>
<div className="HaPay" style={{marginTop: "0.5em"}}> <div className="HaPay" style={{marginTop: "0.5em"}}>
<button className="HaPayButton" onClick={onClick}> <button className="HaPayButton" onClick={onClick}>
@ -237,7 +201,7 @@ function HaPay({onClick}) {
className="HaPayButtonLogo" className="HaPayButtonLogo"
/> />
<div className="HaPayButtonLabel"> <div className="HaPayButtonLabel">
<span> Payer avec </span> <span> {t('payment.payerAvec')} </span>
<svg <svg
width="73" width="73"
height="14" height="14"
@ -287,7 +251,7 @@ function HaPay({onClick}) {
d="M3.875 3V4.5H7.625V3C7.625 1.96875 6.78125 1.125 5.75 1.125C4.69531 1.125 3.875 1.96875 3.875 3ZM2.75 4.5V3C2.75 1.35938 4.08594 0 5.75 0C7.39062 0 8.75 1.35938 8.75 3V4.5H9.5C10.3203 4.5 11 5.17969 11 6V10.5C11 11.3438 10.3203 12 9.5 12H2C1.15625 12 0.5 11.3438 0.5 10.5V6C0.5 5.17969 1.15625 4.5 2 4.5H2.75ZM1.625 6V10.5C1.625 10.7109 1.78906 10.875 2 10.875H9.5C9.6875 10.875 9.875 10.7109 9.875 10.5V6C9.875 5.8125 9.6875 5.625 9.5 5.625H2C1.78906 5.625 1.625 5.8125 1.625 6Z" d="M3.875 3V4.5H7.625V3C7.625 1.96875 6.78125 1.125 5.75 1.125C4.69531 1.125 3.875 1.96875 3.875 3ZM2.75 4.5V3C2.75 1.35938 4.08594 0 5.75 0C7.39062 0 8.75 1.35938 8.75 3V4.5H9.5C10.3203 4.5 11 5.17969 11 6V10.5C11 11.3438 10.3203 12 9.5 12H2C1.15625 12 0.5 11.3438 0.5 10.5V6C0.5 5.17969 1.15625 4.5 2 4.5H2.75ZM1.625 6V10.5C1.625 10.7109 1.78906 10.875 2 10.875H9.5C9.6875 10.875 9.875 10.7109 9.875 10.5V6C9.875 5.8125 9.6875 5.625 9.5 5.625H2C1.78906 5.625 1.625 5.8125 1.625 6Z"
/> />
</svg> </svg>
<span>Paiement sécurisé</span> <span>{t('payment.paiementSécurisé')}</span>
<img <img
src="https://helloassodocumentsprod.blob.core.windows.net/public-documents/bouton_payer_avec_helloasso/logo-visa.svg" src="https://helloassodocumentsprod.blob.core.windows.net/public-documents/bouton_payer_avec_helloasso/logo-visa.svg"
alt="Logo Visa" alt="Logo Visa"
@ -311,6 +275,7 @@ function HaPay({onClick}) {
function MakeCentralPanel({data, visibleMember, navigate, page, source, selectedMembers, setSelectedMembers}) { function MakeCentralPanel({data, visibleMember, navigate, page, source, selectedMembers, setSelectedMembers}) {
const lastCheckedRef = useRef(null); const lastCheckedRef = useRef(null);
const {t} = useTranslation();
function handleCheckbox(e, memberId) { function handleCheckbox(e, memberId) {
const isShiftKeyPressed = e.shiftKey; const isShiftKeyPressed = e.shiftKey;
@ -364,8 +329,12 @@ function MakeCentralPanel({data, visibleMember, navigate, page, source, selected
return <> return <>
<div className="mb-4"> <div className="mb-4">
<small>Ligne {((page - 1) * data.page_size) + 1} à { <small>{t("page_info_full", {
(page * data.page_size > data.result_count) ? data.result_count : (page * data.page_size)} (page {page} sur {data.page_count})</small> line: ((page - 1) * data.page_size) + 1,
tt_line: (page * data.page_size > data.result_count) ? data.result_count : (page * data.page_size),
page: page,
tt_page: data.page_count,
})}</small>
<ul className="list-group"> <ul className="list-group">
{visibleMember.map(member => ( {visibleMember.map(member => (
<MakeRow key={member.id} member={member} navigate={navigate} source={source} isChecked={selectedMembers.includes(member.id)} <MakeRow key={member.id} member={member} navigate={navigate} source={source} isChecked={selectedMembers.includes(member.id)}
@ -387,6 +356,8 @@ function MakeCentralPanel({data, visibleMember, navigate, page, source, selected
} }
function MakeRow({member, source, isChecked, onCheckboxClick, onRowClick}) { function MakeRow({member, source, isChecked, onCheckboxClick, onRowClick}) {
const {t} = useTranslation();
const rowContent = <> const rowContent = <>
<div className="row"> <div className="row">
<div className="col-auto"> <div className="col-auto">
@ -402,7 +373,7 @@ function MakeRow({member, source, isChecked, onCheckboxClick, onRowClick}) {
</div> </div>
{source === "club" ? {source === "club" ?
<small>{member.categorie}</small> <small>{member.categorie}</small>
: <small>{member.club?.name || "Sans club"}</small>} : <small>{member.club?.name || t("club", {count: 0})}</small>}
</> </>
if (member.licence != null) { if (member.licence != null) {
@ -429,12 +400,13 @@ function FiltreBar({data, clubFilter, setClubFilter, source, stateFilter, setSta
allClub.push(...data.result.map((e) => e.club?.name)) allClub.push(...data.result.map((e) => e.club?.name))
allClub = allClub.filter((value, index, self) => self.indexOf(value) === index).filter(value => value != null).sort() allClub = allClub.filter((value, index, self) => self.indexOf(value) === index).filter(value => value != null).sort()
}, [data]); }, [data]);
const {t} = useTranslation();
return <div> return <div>
{source !== "club" && <ClubSelectFilter clubFilter={clubFilter} setClubFilter={setClubFilter}/>} {source !== "club" && <ClubSelectFilter clubFilter={clubFilter} setClubFilter={setClubFilter}/>}
<div className="mb-3"> <div className="mb-3">
<select className="form-select" value={catFilter} onChange={event => setCatFilter(event.target.value)}> <select className="form-select" value={catFilter} onChange={event => setCatFilter(event.target.value)}>
<option value="">--- toute les catégories ---</option> <option value="">{t('---TouteLesCatégories---')}</option>
{Tools.CatList.map(cat => ( {Tools.CatList.map(cat => (
<option key={cat} value={cat}>{getCatName(cat)}</option> <option key={cat} value={cat}>{getCatName(cat)}</option>
))} ))}
@ -442,17 +414,17 @@ function FiltreBar({data, clubFilter, setClubFilter, source, stateFilter, setSta
</div> </div>
<div className="mb-3"> <div className="mb-3">
<select className="form-select" value={stateFilter} onChange={event => setStateFilter(Number(event.target.value))}> <select className="form-select" value={stateFilter} onChange={event => setStateFilter(Number(event.target.value))}>
<option value={1}>Avec demande ou licence validée</option> <option value={1}>{t('membre.filtre.licences.1')}</option>
<option value={2}>Demande en cours</option> <option value={2}>{t('membre.filtre.licences.2')}</option>
<option value={5}>Demande complet</option> <option value={5}>{t('membre.filtre.licences.5')}</option>
<option value={6}>Demande incomplet</option> <option value={6}>{t('membre.filtre.licences.6')}</option>
</select> </select>
</div> </div>
{source !== "club" && <div className="mb-3"> {source !== "club" && <div className="mb-3">
<select className="form-select" value={paymentFilter} onChange={event => setPaymentFilter(Number(event.target.value))}> <select className="form-select" value={paymentFilter} onChange={event => setPaymentFilter(Number(event.target.value))}>
<option value={2}>Tout les états de paiement</option> <option value={2}>{t('membre.filtre.payement.2')}</option>
<option value={0}>Sans paiement</option> <option value={0}>{t('membre.filtre.payement.0')}</option>
<option value={1}>Avec paiement</option> <option value={1}>{t('membre.filtre.payement.1')}</option>
</select> </select>
</div>} </div>}
</div> </div>
@ -461,13 +433,14 @@ function FiltreBar({data, clubFilter, setClubFilter, source, stateFilter, setSta
function ClubSelectFilter({clubFilter, setClubFilter}) { function ClubSelectFilter({clubFilter, setClubFilter}) {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/club/no_detail`, setLoading, 1) const {data, error} = useFetch(`/club/no_detail`, setLoading, 1)
const {t} = useTranslation();
return <> return <>
{data {data
? <div className="mb-3"> ? <div className="mb-3">
<select className="form-select" value={clubFilter} onChange={event => setClubFilter(event.target.value)}> <select className="form-select" value={clubFilter} onChange={event => setClubFilter(event.target.value)}>
<option value="">--- tout les clubs ---</option> <option value="">{t('---ToutLesClubs---')}</option>
<option value="null">--- sans club ---</option> <option value="null">{t('---SansClub---')}</option>
{data.map(club => (<option key={club.id} value={club.name}>{club.name}</option>))} {data.map(club => (<option key={club.id} value={club.name}>{club.name}</option>))}
</select> </select>
</div> </div>

View File

@ -11,10 +11,12 @@ import {ClubPage} from "./club/ClubPage.jsx";
import {AffiliationReqList} from "./affiliation/AffiliationReqList.jsx"; import {AffiliationReqList} from "./affiliation/AffiliationReqList.jsx";
import {StatsPage} from "./StatsPage.jsx"; import {StatsPage} from "./StatsPage.jsx";
import {PayAndValidateList} from "../PayAndValidateList.jsx"; import {PayAndValidateList} from "../PayAndValidateList.jsx";
import {useTranslation} from "react-i18next";
export function AdminRoot() { export function AdminRoot() {
const {t} = useTranslation();
return <> return <>
<h1>Espace administration</h1> <h1>{t('espaceAdministration')}</h1>
<LoadingProvider> <LoadingProvider>
<Outlet/> <Outlet/>
</LoadingProvider> </LoadingProvider>

View File

@ -1,11 +1,14 @@
import {Area, AreaChart, CartesianGrid, Cell, Pie, PieChart, ResponsiveContainer, Sector, Tooltip, XAxis, YAxis} from "recharts"; import {Area, AreaChart, CartesianGrid, Cell, Pie, PieChart, ResponsiveContainer, Sector, Tooltip, XAxis, YAxis} from "recharts";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {getSaison} from "../../utils/Tools.js"; import {getSaison} from "../../utils/Tools.js";
import {useTranslation} from "react-i18next";
export default function StatsLazy({data}) { export default function StatsLazy({data}) {
const {t} = useTranslation();
return <div className="container"> return <div className="container">
<div className="row" style={{marginTop: "2em"}}> <div className="row" style={{marginTop: "2em"}}>
<h3>Nombre de licences</h3> <h3>{t('nombreDeLicences')}</h3>
<div className="col-lg-8" style={{margin: "auto"}}> <div className="col-lg-8" style={{margin: "auto"}}>
<div style={{width: '100%', aspectRatio: 1.5}}> <div style={{width: '100%', aspectRatio: 1.5}}>
<NbGraph raw_data={data}/> <NbGraph raw_data={data}/>
@ -13,7 +16,7 @@ export default function StatsLazy({data}) {
</div> </div>
</div> </div>
<div className="row" style={{marginTop: "3em"}}> <div className="row" style={{marginTop: "3em"}}>
<h3>Nombre de licences par catégorie pour {getSaison()}</h3> <h3>{t('nombreDeLicencesParCatégorie', {saison: getSaison()})}</h3>
<div className="col-lg-8" style={{margin: "auto"}}> <div className="col-lg-8" style={{margin: "auto"}}>
<div style={{width: '100%', aspectRatio: 1.5}}> <div style={{width: '100%', aspectRatio: 1.5}}>
<CatGraph raw_data={data}/> <CatGraph raw_data={data}/>
@ -25,6 +28,7 @@ export default function StatsLazy({data}) {
function NbGraph({raw_data}) { function NbGraph({raw_data}) {
const [showData, setShowData] = useState([]) const [showData, setShowData] = useState([])
const {t} = useTranslation();
useEffect(() => { useEffect(() => {
const data = [] const data = []
@ -57,10 +61,10 @@ function NbGraph({raw_data}) {
<XAxis dataKey="name"/> <XAxis dataKey="name"/>
<YAxis/> <YAxis/>
<Tooltip/> <Tooltip/>
<Area type="monotone" dataKey="na" name="Non définie" stackId="1" stroke={colors[0]} fill={colors[0]}/> <Area type="monotone" dataKey="na" name={t('nonDéfinie')} stackId="1" stroke={colors[0]} fill={colors[0]}/>
<Area type="monotone" dataKey="h" name="Homme" stackId="1" stroke={colors[1]} fill={colors[1]}/> <Area type="monotone" dataKey="h" name={t('homme')} stackId="1" stroke={colors[1]} fill={colors[1]}/>
<Area type="monotone" dataKey="f" name="Femme" stackId="1" stroke={colors[2]} fill={colors[2]}/> <Area type="monotone" dataKey="f" name={t('femme')} stackId="1" stroke={colors[2]} fill={colors[2]}/>
<Area type="monotone" dataKey="not_valid" name="Non validée" stackId="2" stroke={colors[3]} fill={colors[3]} strokeDasharray="5 5" <Area type="monotone" dataKey="not_valid" name={t('nonValidée')} stackId="2" stroke={colors[3]} fill={colors[3]} strokeDasharray="5 5"
fillOpacity="30%"/> fillOpacity="30%"/>
</AreaChart> </AreaChart>
</ResponsiveContainer> </ResponsiveContainer>

View File

@ -3,14 +3,16 @@ import {FallingLines} from "react-loader-spinner";
import {useLoadingSwitcher} from "../../hooks/useLoading.jsx"; import {useLoadingSwitcher} from "../../hooks/useLoading.jsx";
import {useFetch} from "../../hooks/useFetch.js"; import {useFetch} from "../../hooks/useFetch.js";
import {AxiosError} from "../../components/AxiosError.jsx"; import {AxiosError} from "../../components/AxiosError.jsx";
import {useTranslation} from "react-i18next";
export function StatsPage() { export function StatsPage() {
const StatsLazy = lazy(() => import('./StatsLazy.jsx')) const StatsLazy = lazy(() => import('./StatsLazy.jsx'))
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/stats`, setLoading, 1) const {data, error} = useFetch(`/stats`, setLoading, 1)
const {t} = useTranslation();
return <div> return <div>
<h2>Statistiques</h2> <h2>{t("stats")}</h2>
<Suspense fallback={<FallingLines/>}> <Suspense fallback={<FallingLines/>}>
{data {data
? <> ? <>

View File

@ -5,9 +5,11 @@ import {useFetch} from "../../../hooks/useFetch.js";
import {ThreeDots} from "react-loader-spinner"; import {ThreeDots} from "react-loader-spinner";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {Checkbox} from "../../../components/MemberCustomFiels.jsx"; import {Checkbox} from "../../../components/MemberCustomFiels.jsx";
import {useTranslation} from "react-i18next";
export function AffiliationReqList() { export function AffiliationReqList() {
const navigate = useNavigate(); const navigate = useNavigate();
const {t} = useTranslation();
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, refresh, error} = useFetch(`/affiliation/request`, setLoading, 1) const {data, refresh, error} = useFetch(`/affiliation/request`, setLoading, 1)
@ -22,9 +24,9 @@ export function AffiliationReqList() {
}); });
return <> return <>
<h2>Demande d'affiliation</h2> <h2>{t("nav.aff_request")}</h2>
<button type="button" className="btn btn-link" onClick={() => navigate("/admin/club")}> <button type="button" className="btn btn-link" onClick={() => navigate("/admin/club")}>
&laquo; retour {t('back')}
</button> </button>
<div> <div>
@ -39,7 +41,7 @@ export function AffiliationReqList() {
</div> </div>
<div className="col-lg-3"> <div className="col-lg-3">
<div className="card mb-4"> <div className="card mb-4">
<div className="card-header">Filtre</div> <div className="card-header">{t('filtre')}</div>
<div className="card-body"> <div className="card-body">
<FiltreBar data={data} saisonFilter={saisonFilter} setSaisonFilter={setSaisonFilter}/> <FiltreBar data={data} saisonFilter={saisonFilter} setSaisonFilter={setSaisonFilter}/>
</div> </div>
@ -51,10 +53,11 @@ export function AffiliationReqList() {
} }
function MakeCentralPanel({data, visibleRequest, navigate}) { function MakeCentralPanel({data, visibleRequest, navigate}) {
const {t} = useTranslation();
return <> return <>
<div className="mb-4"> <div className="mb-4">
<small>{visibleRequest.length} ligne(s) affichée(s) sur {data.length}</small> <small>{t("page_info_ligne", {show: visibleRequest.length, total: data.length})}</small>
<div className="list-group"> <div className="list-group">
{visibleRequest.map(req => (<MakeRow key={req.id} request={req} navigate={navigate}/>))} {visibleRequest.map(req => (<MakeRow key={req.id} request={req} navigate={navigate}/>))}
</div> </div>
@ -75,6 +78,8 @@ function MakeRow({request, navigate}) {
let allSaison = [] let allSaison = []
function FiltreBar({data, saisonFilter, setSaisonFilter}) { function FiltreBar({data, saisonFilter, setSaisonFilter}) {
const {t} = useTranslation();
useEffect(() => { useEffect(() => {
if (!data) if (!data)
return; return;
@ -85,7 +90,7 @@ function FiltreBar({data, saisonFilter, setSaisonFilter}) {
return <div> return <div>
<div className="mb-3"> <div className="mb-3">
<select className="form-select" value={String(saisonFilter)} onChange={event => setSaisonFilter(Number(event.target.value))}> <select className="form-select" value={String(saisonFilter)} onChange={event => setSaisonFilter(Number(event.target.value))}>
<option value="">--- tout les saisons ---</option> <option value="">{t('all_season')}</option>
{allSaison && allSaison.map((value, index) => { {allSaison && allSaison.map((value, index) => {
return <option key={index} value={value}>{value}-{value+1}</option> return <option key={index} value={value}>{value}-{value+1}</option>
}) })

View File

@ -3,25 +3,27 @@ import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx
import {useFetch} from "../../../hooks/useFetch.js"; import {useFetch} from "../../../hooks/useFetch.js";
import {AxiosError} from "../../../components/AxiosError.jsx"; import {AxiosError} from "../../../components/AxiosError.jsx";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {apiAxios, errFormater} from "../../../utils/Tools.js"; import {apiAxios, errFormater, getToastMessage} from "../../../utils/Tools.js";
import {RoleList, TextField} from "../../../components/MemberCustomFiels.jsx"; import {RoleList, TextField} from "../../../components/MemberCustomFiels.jsx";
import {useEffect, useRef, useState} from "react"; import {useEffect, useRef, useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faFilePdf} from "@fortawesome/free-solid-svg-icons"; import {faFilePdf} from "@fortawesome/free-solid-svg-icons";
import {useTranslation} from "react-i18next";
const vite_url = import.meta.env.VITE_URL; const vite_url = import.meta.env.VITE_URL;
export function AffiliationReqPage() { export function AffiliationReqPage() {
const {id} = useParams() const {id} = useParams()
const navigate = useNavigate(); const navigate = useNavigate();
const {t} = useTranslation();
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, refresh, error} = useFetch(`/affiliation/request/${id}`, setLoading, 1) const {data, refresh, error} = useFetch(`/affiliation/request/${id}`, setLoading, 1)
return <> return <>
<h2>Demande d'affiliation</h2> <h2>{t("nav.aff_request")}</h2>
<button type="button" className="btn btn-link" onClick={() => navigate("/admin/affiliation/request")}> <button type="button" className="btn btn-link" onClick={() => navigate("/admin/club")}>
&laquo; retour {t('back')}
</button> </button>
<div> <div>
{data {data
@ -36,19 +38,11 @@ export function AffiliationReqPage() {
function Content({data, refresh}) { function Content({data, refresh}) {
const navigate = useNavigate(); const navigate = useNavigate();
const {t} = useTranslation();
const handleRm = (reason) => { const handleRm = (reason) => {
toast.promise( toast.promise(
apiAxios.delete(`/affiliation/request/${data.id}?reason=${encodeURIComponent(reason)}`), apiAxios.delete(`/affiliation/request/${data.id}?reason=${encodeURIComponent(reason)}`), getToastMessage('aff.toast.del')
{
pending: "Suppression de la demande d'affiliation en cours",
success: "Demande d'affiliation supprimée avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de la suppression de la demande d'affiliation")
}
}
}
).then(_ => { ).then(_ => {
navigate("/admin/affiliation/request") navigate("/admin/affiliation/request")
}) })
@ -83,7 +77,7 @@ function Content({data, refresh}) {
if (mode === '0') { if (mode === '0') {
if (event.target['mode0_licence' + i].value === "") { if (event.target['mode0_licence' + i].value === "") {
toast.error("Veuillez saisir un numéro de licence valide pour le membre " + (i + 1)); toast.error(t('aff.submit.error1', {id: i + 1}));
err++; err++;
continue; continue;
} }
@ -91,7 +85,7 @@ function Content({data, refresh}) {
formData.append(`m${i + 1}_licence`, event.target['licence' + i].value); formData.append(`m${i + 1}_licence`, event.target['licence' + i].value);
} else if (mode === '1') { } else if (mode === '1') {
if (event.target['similar' + i].value === -1) { if (event.target['similar' + i].value === -1) {
toast.error("Veuillez choisir un membre similaire pour le membre " + (i + 1)); toast.error(t('aff.submit.error2', {id: i + 1}));
err++; err++;
continue; continue;
} }
@ -104,7 +98,7 @@ function Content({data, refresh}) {
if (event.target['flexRadioDefault' + i].value === '0') { if (event.target['flexRadioDefault' + i].value === '0') {
if (event.target['new_email' + i].value === "") { if (event.target['new_email' + i].value === "") {
toast.error("Veuillez saisir un email valide pour le membre " + (i + 1)); toast.error(t('aff.submit.error3', {id: i + 1}));
err++; err++;
continue; continue;
} }
@ -112,7 +106,7 @@ function Content({data, refresh}) {
formData.append(`m${i + 1}_email`, event.target['new_email' + i].value); formData.append(`m${i + 1}_email`, event.target['new_email' + i].value);
} else { } else {
if (event.target['old_email' + i].value === "") { if (event.target['old_email' + i].value === "") {
toast.error("Veuillez saisir un email valide pour le membre " + (i + 1)); toast.error(t('aff.submit.error3', {id: i + 1}));
err++; err++;
continue; continue;
} }
@ -126,32 +120,13 @@ function Content({data, refresh}) {
return; return;
if (event.nativeEvent.submitter.value === "save") { if (event.nativeEvent.submitter.value === "save") {
toast.promise( toast.promise(apiAxios.put(`/affiliation/request/save`, formData), getToastMessage('aff.toast.save')
apiAxios.put(`/affiliation/request/save`, formData),
{
pending: "Enregistrement de la demande d'affiliation en cours",
success: "Demande d'affiliation enregistrée avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de l'enregistrement de la demande d'affiliation")
}
}
}
).then(_ => { ).then(_ => {
refresh(`/affiliation/request/${data.id}`) refresh(`/affiliation/request/${data.id}`)
}) })
} else if (event.nativeEvent.submitter.value === "accept") { } else if (event.nativeEvent.submitter.value === "accept") {
toast.promise( toast.promise(
apiAxios.put(`/affiliation/request/apply`, formData), apiAxios.put(`/affiliation/request/apply`, formData), getToastMessage('aff.toast.accept')
{
pending: "Acceptation de l'affiliation en cours",
success: "Affiliation acceptée avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de l'acceptation de l'affiliation")
}
}
}
).then(_ => { ).then(_ => {
navigate("/admin/affiliation/request") navigate("/admin/affiliation/request")
}) })
@ -162,23 +137,23 @@ function Content({data, refresh}) {
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="card mb-4"> <div className="card mb-4">
<input name="id" value={data.id} readOnly hidden/> <input name="id" value={data.id} readOnly hidden/>
<div className="card-header">Demande d'affiliation</div> <div className="card-header">{t("nav.aff_request")}</div>
<div className="card-body text-center"> <div className="card-body text-center">
{data.club && <h5>Ce club a déjà été affilié (affiliation n°{data.club_no_aff})</h5>} {data.club && <h5>{t('aff.info1', {no: data.club_no_aff})}</h5>}
<h4 id="saison">Saison {data.saison}-{data.saison + 1}</h4> <h4 id="saison">{t('saison')} {data.saison}-{data.saison + 1}</h4>
<div className="row mb-3"> <div className="row mb-3">
<div className="input-group"> <div className="input-group">
<span className="input-group-text" id="name">Nom du club</span> <span className="input-group-text" id="name">{t('aff.nomDuClub')}</span>
<input type="text" className="form-control" placeholder="Nom du club" aria-label="name" <input type="text" className="form-control" placeholder={t('aff.nomDuClub')} aria-label="name"
name="name" aria-describedby="name" defaultValue={data.name} required/> name="name" aria-describedby="name" defaultValue={data.name} required/>
</div> </div>
{data.club && <div className="form-text" id="name">Ancien nom: {data.club_name}</div>} {data.club && <div className="form-text" id="name">{t('aff.ancienNom', {name: data.club_name})}</div>}
</div> </div>
<TextField name="state_id" text="SIRET ou RNA" value={data.stateId} disabled={true}/> <TextField name="state_id" text={t('siretOuRna')} value={data.stateId} disabled={true}/>
<TextField name="address" text="Adresse" value={data.address}/> <TextField name="address" text={t('adresse')} value={data.address}/>
<TextField name="contact" text="Contact administratif" value={data.contact}/> <TextField name="contact" text={t('contactAdministratif')} value={data.contact}/>
<img <img
src={`${vite_url}/api/affiliation/request/${data.id}/logo`} src={`${vite_url}/api/affiliation/request/${data.id}/logo`}
@ -186,15 +161,15 @@ function Content({data, refresh}) {
className="img-fluid" style={{object_fit: 'contain', maxHeight: '15em'}}/> className="img-fluid" style={{object_fit: 'contain', maxHeight: '15em'}}/>
<div className="mb-3"> <div className="mb-3">
<div className="input-group"> <div className="input-group">
<label className="input-group-text" htmlFor="logo">Blason</label> <label className="input-group-text" htmlFor="logo">{t('blason')}</label>
<input type="file" className="form-control" id="logo" name="logo" <input type="file" className="form-control" id="logo" name="logo"
accept=".jpg,.jpeg,.gif,.png,.svg"/> accept=".jpg,.jpeg,.gif,.png,.svg"/>
</div> </div>
<div className="form-text" id="logo">Laissez vide pour ne rien changer.</div> <div className="form-text" id="logo">{t('keepEmpty')}</div>
</div> </div>
<div className="mb-3"> <div className="mb-3">
<div className="input-group"> <div className="input-group">
<label className="input-group-text" htmlFor="status">Status</label> <label className="input-group-text" htmlFor="status">{t('status')}</label>
<a href={`${vite_url}/api/affiliation/request/${data.id}/status`} target='_blank'> <a href={`${vite_url}/api/affiliation/request/${data.id}/status`} target='_blank'>
<button className="btn btn-outline-secondary" type="button" id="button-addon1" <button className="btn btn-outline-secondary" type="button" id="button-addon1"
onClick={_ => null}><FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon> onClick={_ => null}><FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon>
@ -202,7 +177,7 @@ function Content({data, refresh}) {
</a> </a>
<input type="file" className="form-control" id="status" name="status" accept=".pdf,.txt"/> <input type="file" className="form-control" id="status" name="status" accept=".pdf,.txt"/>
</div> </div>
<div className="form-text" id="status">Laissez vide pour ne rien changer.</div> <div className="form-text" id="status">{t('keepEmpty')}</div>
</div> </div>
{data.members.map((member, index) => { {data.members.map((member, index) => {
@ -214,10 +189,11 @@ function Content({data, refresh}) {
</div> </div>
<div className="row mb-3"> <div className="row mb-3">
<div className="d-grid gap-2 d-md-flex justify-content-md-end"> <div className="d-grid gap-2 d-md-flex justify-content-md-end">
<button type="submit" value="accept" className="btn btn-success">Accepter</button> <button type="submit" value="accept" className="btn btn-success">{t('button.accepter')}</button>
<button type="submit" value="save" className="btn btn-primary">Enregistrer</button> <button type="submit" value="save" className="btn btn-primary">{t('button.enregistrer')}</button>
<button className="btn btn-danger" value="rm" data-bs-toggle="modal" data-bs-target="#confirm-delete">Refuser</button> <button className="btn btn-danger" value="rm" data-bs-toggle="modal"
<ConfirmReasonDialog title="Refuser la demande" message="Êtes-vous sûr de vouloir refuser cette demande ?" data-bs-target="#confirm-delete">{t('button.refuser')}</button>
<ConfirmReasonDialog title={t('aff.refuserLaDemande')} message={t('aff.refuserLaDemande.detail')}
onConfirm={handleRm}/> onConfirm={handleRm}/>
</div> </div>
</div> </div>
@ -228,26 +204,28 @@ function Content({data, refresh}) {
function ConfirmReasonDialog({onConfirm, id = "confirm-delete"}) { function ConfirmReasonDialog({onConfirm, id = "confirm-delete"}) {
const [reason, setReason] = useState("") const [reason, setReason] = useState("")
const {t} = useTranslation();
return <div className="modal fade" id={id} tabIndex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"> return <div className="modal fade" id={id} tabIndex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<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="myModalLabel">"Refuser la demande"</h4> <h4 className="modal-title" id="myModalLabel">{t('aff.refuserLaDemande')}</h4>
</div> </div>
<div className="modal-body"> <div className="modal-body">
<div className="row mb-3"> <div className="row mb-3">
<div className="col"> <div className="col">
<label htmlFor="reason" className="form-label">Raison du refus</label> <label htmlFor="reason" className="form-label">{t('aff.raisonDuRefus')}</label>
<textarea className="form-control" id="reason" name="reason" rows="3" <textarea className="form-control" id="reason" name="reason" rows="3"
placeholder="Veuillez indiquer la raison du refus" value={reason} placeholder={t('aff.raisonDuRefus.msg')} value={reason}
onChange={e => setReason(e.target.value)}></textarea> onChange={e => setReason(e.target.value)}></textarea>
</div> </div>
</div> </div>
<span>Êtes-vous sûr de vouloir refuser cette demande ?</span> <span>{t('aff.refusConfirm')}</span>
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
<button type="button" className="btn btn-default" data-dismiss="modal" data-bs-dismiss="modal">Annuler</button> <button type="button" className="btn btn-default" data-dismiss="modal" data-bs-dismiss="modal">{t('button.annuler')}</button>
<a className="btn btn-danger btn-ok" data-bs-dismiss="modal" onClick={_ => onConfirm(reason)}>Confirmer</a> <a className="btn btn-danger btn-ok" data-bs-dismiss="modal" onClick={_ => onConfirm(reason)}>{t('button.confirmer')}</a>
</div> </div>
</div> </div>
</div> </div>
@ -259,6 +237,7 @@ function MemberPart({index, member}) {
const [current, setCurrent] = useState(-1) const [current, setCurrent] = useState(-1)
const [email, setEmail] = useState("") const [email, setEmail] = useState("")
const [emailChoice, setEmailChoice] = useState("0") const [emailChoice, setEmailChoice] = useState("0")
const {t} = useTranslation();
useEffect(() => { useEffect(() => {
if (mode !== 1) if (mode !== 1)
@ -271,10 +250,10 @@ function MemberPart({index, member}) {
return <div className="col mb-4"> return <div className="col mb-4">
<div className="card"> <div className="card">
<div className="card-header">Membre n°{index + 1}</div> <div className="card-header">{t('aff.membreNo', {no: index + 1})}</div>
<input name={"mode" + index} value={mode} readOnly hidden/> <input name={"mode" + index} value={mode} readOnly hidden/>
<div className="card-body"> <div className="card-body">
<RoleList name={"role" + index} text="Rôle" value={member.role}/> <RoleList name={"role" + index} text={t('role')} value={member.role}/>
<div className="btn-group row" role="group"> <div className="btn-group row" role="group">
<div className="mb-2"> <div className="mb-2">
@ -282,7 +261,7 @@ function MemberPart({index, member}) {
<div className="card-header"> <div className="card-header">
<input type="radio" className="btn-check" id={"btnradio1" + index} autoComplete="off" <input type="radio" className="btn-check" id={"btnradio1" + index} autoComplete="off"
checked={mode === 0} onChange={() => setMode(0)}/> checked={mode === 0} onChange={() => setMode(0)}/>
<label className="btn btn-outline-primary" htmlFor={"btnradio1" + index}>Par n° de licence</label> <label className="btn btn-outline-primary" htmlFor={"btnradio1" + index}>{t('aff.byNoLicence')}</label>
</div> </div>
<div className="card-body"> <div className="card-body">
@ -298,7 +277,7 @@ function MemberPart({index, member}) {
<div className="card-header"> <div className="card-header">
<input type="radio" className="btn-check" id={"btnradio2" + index} autoComplete="off" <input type="radio" className="btn-check" id={"btnradio2" + index} autoComplete="off"
checked={mode === 1} onChange={() => setMode(1)}/> checked={mode === 1} onChange={() => setMode(1)}/>
<label className="btn btn-outline-primary" htmlFor={"btnradio2" + index}>Par Membre similaire</label> <label className="btn btn-outline-primary" htmlFor={"btnradio2" + index}>{t('aff.byMembreSim')}</label>
</div> </div>
<div className="card-body"> <div className="card-body">
<input name={"similar" + index} value={current} readOnly hidden/> <input name={"similar" + index} value={current} readOnly hidden/>
@ -315,11 +294,11 @@ function MemberPart({index, member}) {
<div className="card-header"> <div className="card-header">
<input type="radio" className="btn-check" id={"btnradio3" + index} autoComplete="off" <input type="radio" className="btn-check" id={"btnradio3" + index} autoComplete="off"
checked={mode === 2} onChange={() => setMode(2)}/> checked={mode === 2} onChange={() => setMode(2)}/>
<label className="btn btn-outline-primary" htmlFor={"btnradio3" + index}>Par Nouveau membre</label> <label className="btn btn-outline-primary" htmlFor={"btnradio3" + index}>{t('aff.byNewMenbre')}</label>
</div> </div>
<div className="card-body"> <div className="card-body">
<TextField name={"lname" + index} text="Nom" value={member.lname} disabled={mode !== 2}/> <TextField name={"lname" + index} text={t('nom')} value={member.lname} disabled={mode !== 2}/>
<TextField name={"fname" + index} text="Prénom" value={member.fname} disabled={mode !== 2}/> <TextField name={"fname" + index} text={t('prenom')} value={member.fname} disabled={mode !== 2}/>
</div> </div>
</div> </div>
</div> </div>
@ -327,13 +306,13 @@ function MemberPart({index, member}) {
<div className="input-group"> <div className="input-group">
<div className="input-group-text"> <div className="input-group-text">
<input className="form-check-input mt-0" type="radio" value="0" aria-label="nouvel email" <input className="form-check-input mt-0" type="radio" value="0" aria-label={t('nouvelEmail')}
name={"flexRadioDefault" + index} checked={emailChoice === '0'} name={"flexRadioDefault" + index} checked={emailChoice === '0'}
onChange={e => setEmailChoice(e.target.value)}/> onChange={e => setEmailChoice(e.target.value)}/>
</div> </div>
<span className="input-group-text">Nouvel email</span> <span className="input-group-text">{t('nouvelEmail')}</span>
<input type="Email" className="form-control" aria-label="nouvel email" defaultValue={member.email} <input type="Email" className="form-control" aria-label={t('nouvelEmail')} defaultValue={member.email}
name={"new_email" + index}/> name={"new_email" + index} autoComplete="false"/>
</div> </div>
<div className="input-group"> <div className="input-group">
<div className="input-group-text"> <div className="input-group-text">
@ -341,9 +320,9 @@ function MemberPart({index, member}) {
name={"flexRadioDefault" + index} disabled={!email} checked={emailChoice === '1'} name={"flexRadioDefault" + index} disabled={!email} checked={emailChoice === '1'}
onChange={e => setEmailChoice(e.target.value)}/> onChange={e => setEmailChoice(e.target.value)}/>
</div> </div>
<span className="input-group-text">Conserver l'ancien email</span> <span className="input-group-text">{t('conserverLancienEmail')}</span>
<input type="Email" className="form-control" aria-label="ancien email" disabled={true} <input type="Email" className="form-control" aria-label="ancien email" disabled={true}
name={"old_email" + index} value={email}/> name={"old_email" + index} value={email} autoComplete="false"/>
</div> </div>
</div> </div>
</div> </div>
@ -355,6 +334,7 @@ function MemberLicence({member, mode, index, setEmail}) {
const [licence, setLicence] = useState(member.licence) const [licence, setLicence] = useState(member.licence)
const {data, refresh} = useFetch(null, setLoading, 1) const {data, refresh} = useFetch(null, setLoading, 1)
const ref = useRef(-1) const ref = useRef(-1)
const {t} = useTranslation();
useEffect(() => { useEffect(() => {
if (licence === -1 || licence.length < 1 || licence === ref.current) if (licence === -1 || licence.length < 1 || licence === ref.current)
@ -379,19 +359,21 @@ function MemberLicence({member, mode, index, setEmail}) {
<input name={"mode0_licence" + index} value={data ? data.licence : ""} readOnly hidden/> <input name={"mode0_licence" + index} value={data ? data.licence : ""} readOnly hidden/>
<div className="row"> <div className="row">
<div className="input-group mb-3"> <div className="input-group mb-3">
<span className="input-group-text" id={name}>Licence</span> <span className="input-group-text" id={name}>{t('licence')}</span>
<input type="text" className="form-control" placeholder="00000" name={name} <input type="text" className="form-control" placeholder="00000" name={name}
value={licence >= 0 ? String(licence) : ""} disabled={mode !== 0} required={mode === 0} value={licence >= 0 ? String(licence) : ""} disabled={mode !== 0} required={mode === 0}
onChange={event => setLicence(event.target.value)}/> onChange={event => setLicence(event.target.value)}/>
</div> </div>
</div> </div>
{data && <span className="form-text">Nom: {data.lname} {data.fname}, Club: {data.club ? data.club.name : "Sans club"}</span>} {data && <span
className="form-text">{t('nom')}: {data.lname} {data.fname}, {t('club', {count: 1})}: {data.club ? data.club.name : t('club', {count: 0})}</span>}
</> </>
} }
function MemberSimilar({member, current, setCurrent, mode, index, setEmail}) { function MemberSimilar({member, current, setCurrent, mode, index, setEmail}) {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data} = useFetch(`/member/find/similar?fname=${encodeURI(member.fname)}&lname=${encodeURI(member.lname)}`, setLoading, 1) const {data} = useFetch(`/member/find/similar?fname=${encodeURI(member.fname)}&lname=${encodeURI(member.lname)}`, setLoading, 1)
const {t} = useTranslation();
useEffect(() => { useEffect(() => {
if (data && current >= 0 && mode === 1) { if (data && current >= 0 && mode === 1) {
@ -410,7 +392,7 @@ function MemberSimilar({member, current, setCurrent, mode, index, setEmail}) {
className={"list-group-item list-group-item-action" + (current === index ? " active" : "")} className={"list-group-item list-group-item-action" + (current === index ? " active" : "")}
onClick={() => setCurrent(index)} disabled={mode !== 1}> onClick={() => setCurrent(index)} disabled={mode !== 1}>
{m.lname} {m.fname}<br/> {m.lname} {m.fname}<br/>
<small>{m.club ? m.club.name : "Sans club"}</small> <small>{m.club ? m.club.name : t('club', {count: 0})}</small>
</button> </button>
})} })}
</div> </div>

View File

@ -4,16 +4,18 @@ import {useEffect, useReducer, useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faEye, faFilePdf, faPen} from "@fortawesome/free-solid-svg-icons"; import {faEye, faFilePdf, faPen} from "@fortawesome/free-solid-svg-icons";
import {AxiosError} from "../../../components/AxiosError.jsx"; import {AxiosError} from "../../../components/AxiosError.jsx";
import {apiAxios, errFormater, getSaison} from "../../../utils/Tools.js"; import {apiAxios, getSaison, getToastMessage} from "../../../utils/Tools.js";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {SimpleReducer} from "../../../utils/SimpleReducer.jsx"; import {SimpleReducer} from "../../../utils/SimpleReducer.jsx";
import {useNavigate} from "react-router-dom"; import {useNavigate} from "react-router-dom";
import {useTranslation} from "react-i18next";
const vite_url = import.meta.env.VITE_URL; const vite_url = import.meta.env.VITE_URL;
export function AffiliationCard({clubData}) { export function AffiliationCard({clubData}) {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/affiliation/${clubData.id}`, setLoading, 1) const {data, error} = useFetch(`/affiliation/${clubData.id}`, setLoading, 1)
const {t} = useTranslation();
const [modalAffiliation, setModal] = useState({id: 0, club: clubData.id}) const [modalAffiliation, setModal] = useState({id: 0, club: clubData.id})
const [affiliations, dispatch] = useReducer(SimpleReducer, []) const [affiliations, dispatch] = useReducer(SimpleReducer, [])
@ -29,10 +31,10 @@ export function AffiliationCard({clubData}) {
return <div className="card mb-4"> return <div className="card mb-4">
<div className="card-header container-fluid"> <div className="card-header container-fluid">
<div className="row"> <div className="row">
<div className="col">Affiliation</div> <div className="col">{t('affiliation')}</div>
<div className="col" style={{textAlign: 'right'}}> <div className="col" style={{textAlign: 'right'}}>
<button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#AffiliationModal" <button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#AffiliationModal"
onClick={_ => setModal({id: 0, club: clubData.id, validate: 1})}>Ajouter onClick={_ => setModal({id: 0, club: clubData.id, validate: 1})}>{t('button.ajouter')}
</button> </button>
</div> </div>
</div> </div>
@ -54,8 +56,8 @@ export function AffiliationCard({clubData}) {
<a href={`${vite_url}/api/club/${clubData.id}/affiliation`} target='#'> <a href={`${vite_url}/api/club/${clubData.id}/affiliation`} target='#'>
<button className="btn btn-primary" type="button" id="button-addon1" style={{marginTop: '1em'}} <button className="btn btn-primary" type="button" id="button-addon1" style={{marginTop: '1em'}}
onClick={e => null}> onClick={() => null}>
Téléchargée l'attestation d'affiliation <FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon> {t('dlAff')} <FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon>
</button> </button>
</a> </a>
</div> </div>
@ -76,16 +78,7 @@ function sendAffiliation(event, dispatch) {
const formData = new FormData(event.target); const formData = new FormData(event.target);
toast.promise( toast.promise(
apiAxios.post(`/affiliation/${formData.get('club')}?saison=${formData.get('saison')}`), apiAxios.post(`/affiliation/${formData.get('club')}?saison=${formData.get('saison')}`), getToastMessage('aff.toast.save2')
{
pending: "Enregistrement de l'affiliation en cours",
success: "Affiliation enregistrée avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de l'enregistrement de l'affiliation")
}
}
}
).then(data => { ).then(data => {
dispatch({type: 'UPDATE_OR_ADD', payload: data.data}) dispatch({type: 'UPDATE_OR_ADD', payload: data.data})
dispatch({type: 'SORT', payload: (a, b) => b.saison - a.saison}) dispatch({type: 'SORT', payload: (a, b) => b.saison - a.saison})
@ -96,16 +89,7 @@ function sendAffiliation(event, dispatch) {
function removeAffiliation(id, dispatch) { function removeAffiliation(id, dispatch) {
if (id <= 0) return if (id <= 0) return
toast.promise( toast.promise(
apiAxios.delete(`/affiliation/${id}`), apiAxios.delete(`/affiliation/${id}`), getToastMessage('aff.toast.del2')
{
pending: "Suppression de l'affiliation en cours",
success: "Affiliation supprimée avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de la suppression de l'affiliation")
}
}
}
).then(_ => { ).then(_ => {
dispatch({type: 'REMOVE', payload: id}) dispatch({type: 'REMOVE', payload: id})
}) })
@ -114,6 +98,7 @@ function removeAffiliation(id, dispatch) {
function ModalContent({affiliation, dispatch}) { function ModalContent({affiliation, dispatch}) {
const navigate = useNavigate(); const navigate = useNavigate();
const [saison, setSaison] = useState(0) const [saison, setSaison] = useState(0)
const {t} = useTranslation();
const setSeason = (event) => { const setSeason = (event) => {
setSaison(Number(event.target.value)) setSaison(Number(event.target.value))
} }
@ -130,14 +115,14 @@ function ModalContent({affiliation, dispatch}) {
<input name="id" value={affiliation.id} readOnly hidden/> <input name="id" value={affiliation.id} readOnly hidden/>
<input name="club" value={affiliation.club} readOnly hidden/> <input name="club" value={affiliation.club} readOnly hidden/>
<div className="modal-header"> <div className="modal-header">
<h1 className="modal-title fs-5" id="AffiliationModalLabel">Edition de l'affiliation</h1> <h1 className="modal-title fs-5" id="AffiliationModalLabel">{t("editionDeL'affiliation")}</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="input-group mb-3 justify-content-md-center"> <div className="input-group mb-3 justify-content-md-center">
{affiliation.id === 0 {affiliation.id === 0
? <input type="number" className="form-control" placeholder="Saison" name="saison" ? <input type="number" className="form-control" placeholder={t('saison')} name="saison"
aria-label="Saison" aria-describedby="basic-addon2" value={saison} onChange={setSeason}/> aria-label="Saison" aria-describedby="basic-addon2" value={saison} onChange={setSeason}/>
: <><span className="input-group-text" id="basic-addon2">{saison}</span> : <><span className="input-group-text" id="basic-addon2">{saison}</span>
<input name="saison" value={saison} readOnly hidden/></>} <input name="saison" value={saison} readOnly hidden/></>}
@ -145,10 +130,10 @@ function ModalContent({affiliation, dispatch}) {
<span className="input-group-text" id="basic-addon2">{saison + 1}</span> <span className="input-group-text" id="basic-addon2">{saison + 1}</span>
</div> </div>
<div className="input-group mb-3 justify-content-md-center"> <div className="input-group mb-3 justify-content-md-center">
<span className="input-group-text" id="basic-addon2">État de la demande</span> <span className="input-group-text" id="basic-addon2">{t('étatDeLaDemande')}</span>
{affiliation.validate ? <span className="input-group-text" id="basic-addon2">Validée</span> : {affiliation.validate ? <span className="input-group-text" id="basic-addon2">{t('validée')}</span> :
<> <>
<span className="input-group-text" id="basic-addon2">En attente</span> <span className="input-group-text" id="basic-addon2">{t('enAttente')}</span>
<button type="button" className="btn btn-primary" <button type="button" className="btn btn-primary"
onClick={() => navigate('/admin/affiliation/request/' + (affiliation.id * -1))} onClick={() => navigate('/admin/affiliation/request/' + (affiliation.id * -1))}
data-bs-dismiss="modal"><FontAwesomeIcon icon={faEye}/></button> data-bs-dismiss="modal"><FontAwesomeIcon icon={faEye}/></button>
@ -156,10 +141,10 @@ function ModalContent({affiliation, dispatch}) {
</div> </div>
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
{affiliation.id === 0 && <button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Enregistrer</button>} {affiliation.id === 0 && <button type="submit" className="btn btn-primary" data-bs-dismiss="modal">{t('button.enregistrer')}</button>}
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button> <button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">{t('button.fermer')}</button>
{affiliation.id <= 0 || <button type="button" className="btn btn-danger" data-bs-dismiss="modal" {affiliation.id <= 0 || <button type="button" className="btn btn-danger" data-bs-dismiss="modal"
onClick={() => removeAffiliation(affiliation.id, dispatch)}>Supprimer</button>} onClick={() => removeAffiliation(affiliation.id, dispatch)}>{t('button.supprimer')}</button>}
</div> </div>
</form> </form>
} }

View File

@ -3,14 +3,16 @@ import {useEffect, useState} from "react";
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {useFetch} from "../../../hooks/useFetch.js"; import {useFetch} from "../../../hooks/useFetch.js";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {apiAxios, errFormater} from "../../../utils/Tools.js"; import {apiAxios, errFormater, getToastMessage} from "../../../utils/Tools.js";
import {AxiosError} from "../../../components/AxiosError.jsx"; import {AxiosError} from "../../../components/AxiosError.jsx";
import {Checkbox} from "../../../components/MemberCustomFiels.jsx"; import {Checkbox} from "../../../components/MemberCustomFiels.jsx";
import {ThreeDots} from "react-loader-spinner"; import {ThreeDots} from "react-loader-spinner";
import {SearchBar} from "../../../components/SearchBar.jsx"; import {SearchBar} from "../../../components/SearchBar.jsx";
import {useTranslation} from "react-i18next";
export function ClubList() { export function ClubList() {
const {hash} = useLocation(); const {hash} = useLocation();
const {t} = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
let page = Number(hash.substring(1)); let page = Number(hash.substring(1));
page = (page > 0) ? page : 1; page = (page > 0) ? page : 1;
@ -40,7 +42,7 @@ export function ClubList() {
country: e.country, country: e.country,
siret: e.siret, siret: e.siret,
no_affiliation: e.no_affiliation, no_affiliation: e.no_affiliation,
affiliation: showAffiliationState ? affiliationData.find(aff => (aff.id >= 0) ? Number(aff.club) === e.id : aff.club === e.state_id) : null affiliation: showAffiliationState ? affiliationData.find(aff => (aff.id >= 0) ? Number(aff.club) === e.id : aff.club === e.state_id) : null
}) })
} }
setClubData(data2); setClubData(data2);
@ -51,16 +53,7 @@ export function ClubList() {
return; return;
toast.promise( toast.promise(
apiAxios.get(`/affiliation/current`), apiAxios.get(`/affiliation/current`), getToastMessage("club.toast.aff"))
{
pending: "Chargement des affiliation...",
success: "Affiliation chargées",
error: {
render({data}) {
return errFormater(data, "Impossible de charger les affiliations")
}
}
})
.then(data => { .then(data => {
setAffiliationData(data.data); setAffiliationData(data.data);
}); });
@ -74,7 +67,7 @@ export function ClubList() {
} }
return <> return <>
<h2>Club </h2> <h2>{t('club', {count: 1})}</h2>
<div> <div>
<div className="row"> <div className="row">
<div className="col-lg-9"> <div className="col-lg-9">
@ -90,13 +83,13 @@ export function ClubList() {
<div className="col-lg-3"> <div className="col-lg-3">
<div className="mb-4"> <div className="mb-4">
<div className="mb-2"> <div className="mb-2">
<button className="btn btn-primary" onClick={() => navigate("../affiliation/request")}>Demande d'affiliation en cours <button className="btn btn-primary" onClick={() => navigate("../affiliation/request")}>{t('demandeDaffiliationEnCours')}
</button> </button>
</div> </div>
<button className="btn btn-primary" onClick={() => navigate("new")}>Ajouter un club</button> <button className="btn btn-primary" onClick={() => navigate("new")}>{t('ajouterUnClub')}</button>
</div> </div>
<div className="card mb-4"> <div className="card mb-4">
<div className="card-header">Filtre</div> <div className="card-header">{t('filtre')}</div>
<div className="card-body"> <div className="card-body">
<FiltreBar showAffiliationState={showAffiliationState} setShowAffiliationState={setShowAffiliationState} data={data} <FiltreBar showAffiliationState={showAffiliationState} setShowAffiliationState={setShowAffiliationState} data={data}
countryFilter={countryFilter} setCountryFilter={setCountryFilter}/> countryFilter={countryFilter} setCountryFilter={setCountryFilter}/>
@ -109,6 +102,8 @@ export function ClubList() {
} }
function MakeCentralPanel({data, visibleclub, navigate, showAffiliationState, page}) { function MakeCentralPanel({data, visibleclub, navigate, showAffiliationState, page}) {
const {t} = useTranslation();
const pages = [] const pages = []
for (let i = 1; i <= data.page_count; i++) { for (let i = 1; i <= data.page_count; i++) {
pages.push(<li key={i} className={"page-item " + ((page === i) ? "active" : "")}> pages.push(<li key={i} className={"page-item " + ((page === i) ? "active" : "")}>
@ -118,8 +113,12 @@ function MakeCentralPanel({data, visibleclub, navigate, showAffiliationState, pa
return <> return <>
<div className="mb-4"> <div className="mb-4">
<small>Ligne {((page - 1) * data.page_size) + 1} à { <small>{t("page_info_full", {
(page * data.page_size > data.result_count) ? data.result_count : (page * data.page_size)} (page {page} sur {data.page_count})</small> line: ((page - 1) * data.page_size) + 1,
tt_line: (page * data.page_size > data.result_count) ? data.result_count : (page * data.page_size),
page: page,
tt_page: data.page_count,
})}</small>
<div className="list-group"> <div className="list-group">
{visibleclub.map(club => (<MakeRow key={club.id} club={club} navigate={navigate} showAffiliationState={showAffiliationState}/>))} {visibleclub.map(club => (<MakeRow key={club.id} club={club} navigate={navigate} showAffiliationState={showAffiliationState}/>))}
</div> </div>
@ -165,6 +164,8 @@ function MakeRow({club, showAffiliationState, navigate}) {
let allCountry = [] let allCountry = []
function FiltreBar({showAffiliationState, setShowAffiliationState, data, countryFilter, setCountryFilter}) { function FiltreBar({showAffiliationState, setShowAffiliationState, data, countryFilter, setCountryFilter}) {
const {t} = useTranslation();
useEffect(() => { useEffect(() => {
if (!data) if (!data)
return; return;
@ -175,11 +176,11 @@ function FiltreBar({showAffiliationState, setShowAffiliationState, data, country
return <div> return <div>
<div className="mb-3"> <div className="mb-3">
<Checkbox value={showAffiliationState} onChange={setShowAffiliationState} <Checkbox value={showAffiliationState} onChange={setShowAffiliationState}
label="Afficher l'état des affiliation"/> label={t('afficherLétatDesAffiliation')}/>
</div> </div>
<div className="mb-3"> <div className="mb-3">
<select className="form-select" value={countryFilter} onChange={event => setCountryFilter(event.target.value)}> <select className="form-select" value={countryFilter} onChange={event => setCountryFilter(event.target.value)}>
<option value="">--- tout les pays ---</option> <option value="">{t('---ToutLesPays---')}</option>
{allCountry && allCountry.map((value, index) => { {allCountry && allCountry.map((value, index) => {
return <option key={index} value={value}>{value}</option> return <option key={index} value={value}>{value}</option>
}) })

View File

@ -2,7 +2,7 @@ import {useNavigate, useParams} from "react-router-dom";
import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {useFetch} from "../../../hooks/useFetch.js"; import {useFetch} from "../../../hooks/useFetch.js";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {apiAxios, errFormater} from "../../../utils/Tools.js"; import {apiAxios, getToastMessage} from "../../../utils/Tools.js";
import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx"; import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx";
import {AxiosError} from "../../../components/AxiosError.jsx"; import {AxiosError} from "../../../components/AxiosError.jsx";
import {AffiliationCard} from "./AffiliationCard.jsx"; import {AffiliationCard} from "./AffiliationCard.jsx";
@ -15,37 +15,30 @@ import {HoraireEditor} from "../../../components/Club/HoraireEditor.jsx";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faFilePdf} from "@fortawesome/free-solid-svg-icons"; import {faFilePdf} from "@fortawesome/free-solid-svg-icons";
import {SmartLogoBackground} from "../../../components/SmartLogoBackground.jsx"; import {SmartLogoBackground} from "../../../components/SmartLogoBackground.jsx";
import {useTranslation} from "react-i18next";
const vite_url = import.meta.env.VITE_URL; const vite_url = import.meta.env.VITE_URL;
export function ClubPage() { export function ClubPage() {
const {id} = useParams() const {id} = useParams()
const navigate = useNavigate(); const navigate = useNavigate();
const {t} = useTranslation();
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, refresh, error} = useFetch(`/club/${id}`, setLoading, 1) const {data, error} = useFetch(`/club/${id}`, setLoading, 1)
const handleRm = () => { const handleRm = () => {
toast.promise( toast.promise(
apiAxios.delete(`/club/${id}`), apiAxios.delete(`/club/${id}`), getToastMessage("club.toast.del")
{
pending: "Suppression du club en cours...",
success: "Club supprimé avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de la suppression du club")
}
},
}
).then(_ => { ).then(_ => {
navigate("/admin/club") navigate("/admin/club")
}) })
} }
return <> return <>
<h2>Page club</h2> <h2>{t('pageClub')}</h2>
<button type="button" className="btn btn-link" onClick={() => navigate("/admin/club")}> <button type="button" className="btn btn-link" onClick={() => navigate("/admin/club")}>
&laquo; retour {t('back')}
</button> </button>
{data {data
? <div> ? <div>
@ -60,11 +53,11 @@ export function ClubPage() {
<LoadingProvider><BureauCard clubData={data}/></LoadingProvider> <LoadingProvider><BureauCard clubData={data}/></LoadingProvider>
<div className="col" style={{textAlign: 'right', marginTop: '1em'}}> <div className="col" style={{textAlign: 'right', marginTop: '1em'}}>
<button className="btn btn-danger btn-sm" data-bs-toggle="modal" <button className="btn btn-danger btn-sm" data-bs-toggle="modal"
data-bs-target="#confirm-delete">Supprimer le club data-bs-target="#confirm-delete">{t('supprimerLeClub')}
</button> </button>
</div> </div>
<ConfirmDialog title="Supprimer le club" <ConfirmDialog title={t('supprimerLeClub')}
message="Êtes-vous sûr de vouloir supprimer ce club ?" message={t('supprimerLeClub.msg')}
onConfirm={handleRm}/> onConfirm={handleRm}/>
</div> </div>
@ -79,24 +72,14 @@ function InformationForm({data}) {
const [switchOn, setSwitchOn] = useState(data.international); const [switchOn, setSwitchOn] = useState(data.international);
const [modal, setModal] = useState({id: -1}) const [modal, setModal] = useState({id: -1})
const locationModalCallback = useRef(null) const locationModalCallback = useRef(null)
const {t} = useTranslation();
const handleSubmit = (event) => { const handleSubmit = (event) => {
event.preventDefault(); event.preventDefault();
const formData = new FormData(event.target); const formData = new FormData(event.target);
toast.promise( toast.promise(apiAxios.put(`/club/${data.id}`, formData), getToastMessage("club.toast.save"))
apiAxios.put(`/club/${data.id}`, formData),
{
pending: "Enregistrement du club en cours",
success: "Club enregistrée avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de l'enregistrement du club")
}
},
}
)
} }
return <> return <>
@ -104,11 +87,11 @@ function InformationForm({data}) {
<div className="card mb-4"> <div className="card mb-4">
<input name="id" value={data.id} readOnly hidden/> <input name="id" value={data.id} readOnly hidden/>
<input name="clubId" value={data.clubId} readOnly hidden/> <input name="clubId" value={data.clubId} readOnly hidden/>
<div className="card-header">Affiliation n°{data.no_affiliation}</div> <div className="card-header">{t('affiliationNo', {no: data.no_affiliation})}</div>
<div className="card-body text-center"> <div className="card-body text-center">
<TextField name="name" text="Nom" value={data.name}/> <TextField name="name" text={t('nom')} value={data.name}/>
<CountryList name="country" text="Pays" value={data.country}/> <CountryList name="country" text={t('pays')} value={data.country}/>
<SmartLogoBackground <SmartLogoBackground
src={`${vite_url}/api/club/${data.clubId}/logo`} src={`${vite_url}/api/club/${data.clubId}/logo`}
@ -116,38 +99,38 @@ function InformationForm({data}) {
imgClassName="img-fluid" style={{object_fit: 'contain', maxHeight: '15em'}}/> imgClassName="img-fluid" style={{object_fit: 'contain', maxHeight: '15em'}}/>
<div className="mb-3"> <div className="mb-3">
<div className="input-group"> <div className="input-group">
<label className="input-group-text" htmlFor="logo">Blason</label> <label className="input-group-text" htmlFor="logo">{t('blason')}</label>
<input type="file" className="form-control" id="logo" name="logo" <input type="file" className="form-control" id="logo" name="logo"
accept=".jpg,.jpeg,.gif,.png,.svg"/> accept=".jpg,.jpeg,.gif,.png,.svg"/>
</div> </div>
<div className="form-text" id="logo">Laissez vide pour ne rien changer.</div> <div className="form-text" id="logo">{t('keepEmpty')}</div>
</div> </div>
<div className="input-group mb-3"> <div className="input-group mb-3">
<div className="input-group-text"> <div className="input-group-text">
<input type="checkbox" className="form-check-input mt-0" name="international" id="international" <input type="checkbox" className="form-check-input mt-0" name="international" id="international"
checked={switchOn} onChange={() => setSwitchOn(!switchOn)}/> checked={switchOn} onChange={() => setSwitchOn(!switchOn)}/>
<label className="input-group-text" htmlFor="international">Club externe</label> <label className="input-group-text" htmlFor="international">{t('clubExterne')}</label>
</div> </div>
</div> </div>
{!switchOn && <> {!switchOn && <>
<TextField name="state_id" text="SIRET ou RNA" value={data.state_id} required={false}/> <TextField name="state_id" text={t('siretOuRna')} value={data.state_id} required={false}/>
<TextField name="contact_intern" text="Contact interne" value={data.contact_intern} required={false} <TextField name="contact_intern" text={t('contactInterne')} value={data.contact_intern} required={false}
placeholder="example@test.com"/> placeholder="example@test.com"/>
<TextField name="address" text="Adresse administrative" value={data.address} required={false} <TextField name="address" text={t('adresseAdministrative')} value={data.address} required={false}
placeholder="Adresse administrative"/> placeholder={t('adresseAdministrative')}/>
<div className="mb-3"> <div className="mb-3">
<div className="input-group"> <div className="input-group">
<label className="input-group-text" htmlFor="status">Statue</label> <label className="input-group-text" htmlFor="status">{t('statue')}</label>
<a href={`${vite_url}/api/club/${data.id}/status`} target='_blank'> <a href={`${vite_url}/api/club/${data.id}/status`} target='_blank'>
<button className="btn btn-outline-secondary" type="button" id="button-addon1" <button className="btn btn-outline-secondary" type="button" id="button-addon1"
onClick={e => null}><FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon> onClick={() => null}><FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon>
</button> </button>
</a> </a>
<input type="file" className="form-control" id="status" name="status" accept=".pdf,.txt"/> <input type="file" className="form-control" id="status" name="status" accept=".pdf,.txt"/>
</div> </div>
<div className="form-text" id="status">Laissez vide pour ne rien changer.</div> <div className="form-text" id="status">{t('keepEmpty')}</div>
</div> </div>
<ContactEditor data={data}/> <ContactEditor data={data}/>
@ -158,7 +141,7 @@ function InformationForm({data}) {
</div> </div>
<div className="row mb-3"> <div className="row mb-3">
<div className="d-grid gap-2 d-md-flex justify-content-md-end"> <div className="d-grid gap-2 d-md-flex justify-content-md-end">
<button type="submit" className="btn btn-primary">Enregistrer</button> <button type="submit" className="btn btn-primary">{t('button.enregistrer')}</button>
</div> </div>
</div> </div>
</div> </div>
@ -173,15 +156,16 @@ export function BureauCard({clubData}) {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/club/desk/${clubData.id}`, setLoading, 1) const {data, error} = useFetch(`/club/desk/${clubData.id}`, setLoading, 1)
const navigate = useNavigate(); const navigate = useNavigate();
const {t} = useTranslation();
return <> return <>
<div className="card mb-4"> <div className="card mb-4">
<div className="card-header">Bureau</div> <div className="card-header">{t('bureau')}</div>
<div className="card-body"> <div className="card-body">
<ul className="list-group"> <ul className="list-group">
{data && data.map((d, index) => { {data && data.map((d, index) => {
return <div key={index} className="list-group-item d-flex justify-content-between align-items-start list-group-item-action" return <div key={index} className="list-group-item d-flex justify-content-between align-items-start list-group-item-action"
onClick={__ => navigate(`/admin/member/${d.id}`)}> onClick={__ => navigate(`/admin/member/${d.id}`)}>
<div className="me-auto"><small>{d.role}</small><br/>{d.lname} {d.fname}</div> <div className="me-auto"><small>{d.role}</small><br/>{d.lname} {d.fname}</div>
</div> </div>
})} })}

View File

@ -2,21 +2,23 @@ import {useNavigate} from "react-router-dom";
import {LoadingProvider} from "../../../hooks/useLoading.jsx"; import {LoadingProvider} from "../../../hooks/useLoading.jsx";
import {useFetch} from "../../../hooks/useFetch.js"; import {useFetch} from "../../../hooks/useFetch.js";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {apiAxios, errFormater} from "../../../utils/Tools.js"; import {apiAxios, getToastMessage} from "../../../utils/Tools.js";
import {CountryList, TextField} from "../../../components/MemberCustomFiels.jsx"; import {CountryList, TextField} from "../../../components/MemberCustomFiels.jsx";
import {useRef, useState} from "react"; import {useRef, useState} from "react";
import {LocationEditor, LocationEditorModal} from "../../../components/Club/LocationEditor.jsx"; import {LocationEditor, LocationEditorModal} from "../../../components/Club/LocationEditor.jsx";
import {ContactEditor} from "../../../components/Club/ContactEditor.jsx"; import {ContactEditor} from "../../../components/Club/ContactEditor.jsx";
import {HoraireEditor} from "../../../components/Club/HoraireEditor.jsx"; import {HoraireEditor} from "../../../components/Club/HoraireEditor.jsx";
import {useTranslation} from "react-i18next";
export function NewClubPage() { export function NewClubPage() {
const navigate = useNavigate() const navigate = useNavigate()
const {t} = useTranslation();
return <> return <>
<h2>Page nouveau club</h2> <h2>{t('pageNouveauClub')}</h2>
<button type="button" className="btn btn-link" onClick={() => navigate("/admin/club")}> <button type="button" className="btn btn-link" onClick={() => navigate("/admin/club")}>
&laquo; retour {t('back')}
</button> </button>
<div> <div>
<div className="row"> <div className="row">
@ -35,6 +37,7 @@ function InformationForm() {
const [modal, setModal] = useState({id: -1}) const [modal, setModal] = useState({id: -1})
const locationModalCallback = useRef(null) const locationModalCallback = useRef(null)
const navigate = useNavigate() const navigate = useNavigate()
const {t} = useTranslation();
const {data} = useFetch(`/club/contact_type`) const {data} = useFetch(`/club/contact_type`)
@ -44,16 +47,7 @@ function InformationForm() {
const formData = new FormData(event.target); const formData = new FormData(event.target);
toast.promise( toast.promise(
apiAxios.put(`/club`, formData), apiAxios.put(`/club`, formData), getToastMessage("club.toast.new")
{
pending: "Création du club en cours",
success: "Club créé avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de la création du club")
}
}
}
).then(data => { ).then(data => {
navigate(`/admin/club/${data.data}`); navigate(`/admin/club/${data.data}`);
}) })
@ -62,15 +56,15 @@ function InformationForm() {
return <> return <>
<form onSubmit={handleSubmit} autoComplete="off"> <form onSubmit={handleSubmit} autoComplete="off">
<div className="card mb-4"> <div className="card mb-4">
<div className="card-header">Nouveau club</div> <div className="card-header">{t('nouveauClub')}</div>
<div className="card-body text-center"> <div className="card-body text-center">
<TextField name="name" text="Nom*"/> <TextField name="name" text={t('nom')+"*"}/>
<CountryList name="country" text="Pays*" value={"FR"}/> <CountryList name="country" text={t('pays')+"*"} value={"FR"}/>
<div className="mb-3"> <div className="mb-3">
<div className="input-group"> <div className="input-group">
<label className="input-group-text" htmlFor="logo">Blason</label> <label className="input-group-text" htmlFor="logo">{t('blason')}</label>
<input type="file" className="form-control" id="logo" name="logo" <input type="file" className="form-control" id="logo" name="logo"
accept=".jpg,.jpeg,.gif,.png,.svg"/> accept=".jpg,.jpeg,.gif,.png,.svg"/>
</div> </div>
@ -80,17 +74,17 @@ function InformationForm() {
<div className="input-group-text"> <div className="input-group-text">
<input type="checkbox" className="form-check-input mt-0" name="international" id="international" <input type="checkbox" className="form-check-input mt-0" name="international" id="international"
checked={switchOn} onChange={() => setSwitchOn(!switchOn)}/> checked={switchOn} onChange={() => setSwitchOn(!switchOn)}/>
<label className="input-group-text" htmlFor="international">Club externe</label> <label className="input-group-text" htmlFor="international">{t('clubExterne')}</label>
</div> </div>
</div> </div>
{!switchOn && <> {!switchOn && <>
<TextField name="state_id" text="SIRET ou RNA" required={false}/> <TextField name="state_id" text={t('siretOuRna')} required={false}/>
<TextField name="contact_intern" text="Contact interne" required={false} placeholder="example@test.com"/> <TextField name="contact_intern" text={t('contactInterne')} required={false} placeholder="example@test.com"/>
<TextField name="address" text="Adresse administrative" required={false} placeholder="Adresse administrative"/> <TextField name="address" text={t('adresseAdministrative')} required={false} placeholder="Adresse administrative"/>
<div className="mb-3"> <div className="mb-3">
<div className="input-group"> <div className="input-group">
<label className="input-group-text" htmlFor="status">Status</label> <label className="input-group-text" htmlFor="status">{t('status')}</label>
<input type="file" className="form-control" id="status" name="status" accept=".pdf,.txt"/> <input type="file" className="form-control" id="status" name="status" accept=".pdf,.txt"/>
</div> </div>
</div> </div>
@ -103,7 +97,7 @@ function InformationForm() {
</div> </div>
<div className="row mb-3"> <div className="row mb-3">
<div className="d-grid gap-2 d-md-flex justify-content-md-end"> <div className="d-grid gap-2 d-md-flex justify-content-md-end">
<button type="submit" className="btn btn-primary">Enregistrer</button> <button type="submit" className="btn btn-primary">{t('button.enregistrer')}</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,53 +1,32 @@
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {apiAxios, errFormater} from "../../../utils/Tools.js"; import {apiAxios, getToastMessage} from "../../../utils/Tools.js";
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {useFetch} from "../../../hooks/useFetch.js"; import {useFetch} from "../../../hooks/useFetch.js";
import {ColoredCircle} from "../../../components/ColoredCircle.jsx"; import {ColoredCircle} from "../../../components/ColoredCircle.jsx";
import {AxiosError} from "../../../components/AxiosError.jsx"; import {AxiosError} from "../../../components/AxiosError.jsx";
import {useTranslation} from "react-i18next";
export function CompteInfo({userData}) { export function CompteInfo({userData}) {
const {t} = useTranslation();
const creatAccount = () => { const creatAccount = () => {
toast.promise( toast.promise(apiAxios.put(`/compte/${userData.id}/init`), getToastMessage("membre.toast.compte"))
apiAxios.put(`/compte/${userData.id}/init`),
{
pending: 'Création du compte en cours',
success: 'Compte créé avec succès 🎉',
error: {
render({data}) {
return errFormater(data, "Échec de la création du compte")
}
}
}
)
} }
const sendId = (event) => { const sendId = (event) => {
event.preventDefault(); event.preventDefault();
toast.promise(apiAxios.put(`/compte/${userData.id}/setUUID/${event.target.uuid?.value}`), getToastMessage("membre.toast.id"))
toast.promise(
apiAxios.put(`/compte/${userData.id}/setUUID/${event.target.uuid?.value}`),
{
pending: "Définition de l'identifient en cours",
success: "Identifient défini avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de la définition de l'identifient")
}
}
}
)
} }
return <div className="card mb-4"> return <div className="card mb-4">
<div className="card-header"> <div className="card-header">
<div className="btn-group dropend"> <div className="btn-group dropend">
<div className="dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"> <div className="dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
Compte {t('compte')}
</div> </div>
<ul className="dropdown-menu"> <ul className="dropdown-menu">
<li> <li>
<button type="button" className="btn btn-primary" data-bs-toggle="modal" <button type="button" className="btn btn-primary" data-bs-toggle="modal"
data-bs-target="#comptIdModal">Définir l'id du compte data-bs-target="#comptIdModal">{t('définirLidDuCompte')}
</button> </button>
</li> </li>
</ul> </ul>
@ -60,12 +39,12 @@ export function CompteInfo({userData}) {
<> <>
<div className="row"> <div className="row">
<div className="input-group mb-3"> <div className="input-group mb-3">
<div>Ce membre ne dispose pas de compte...</div> <div>{t('membre.noAccount')}</div>
</div> </div>
</div> </div>
<div className="row"> <div className="row">
<div className="input-group mb-3"> <div className="input-group mb-3">
<button className="btn btn-primary" onClick={creatAccount}>Initialiser le compte</button> <button className="btn btn-primary" onClick={creatAccount}>{t('membre.initaccount')}</button>
</div> </div>
</div> </div>
</> </>
@ -77,18 +56,18 @@ export function CompteInfo({userData}) {
<div className="modal-content"> <div className="modal-content">
<form onSubmit={sendId}> <form onSubmit={sendId}>
<div className="modal-header"> <div className="modal-header">
<h1 className="modal-title fs-5" id="comptIdModalLabel">Entré l'UUID du compte</h1> <h1 className="modal-title fs-5" id="comptIdModalLabel">{t('membre.initaccount.text1')}</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">
<h5>Attention ne changée l'id d'un membre que si vous êtes sûr de ce que vos faites...</h5> <h5>{t('membre.initaccount.text2')}</h5>
<input type="text" className="form-control" placeholder="uuid" name="uuid" <input type="text" className="form-control" placeholder="uuid" name="uuid"
defaultValue={userData.userId}/> defaultValue={userData.userId}/>
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Appliquer</button> <button type="submit" className="btn btn-primary" data-bs-dismiss="modal">{t('button.appliquer')}</button>
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button> <button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">{t('button.fermer')}</button>
</div> </div>
</form> </form>
</div> </div>
@ -100,23 +79,24 @@ export function CompteInfo({userData}) {
function CompteInfoContent({userData}) { function CompteInfoContent({userData}) {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/compte/${userData.userId}`, setLoading, 1) const {data, error} = useFetch(`/compte/${userData.userId}`, setLoading, 1)
const {t} = useTranslation();
return <> return <>
{data {data
? <> ? <>
<div className="row"> <div className="row">
<div className="input-group mb-3"> <div className="input-group mb-3">
<div>Identifiant: {data.login}</div> <div>{t('membre.identifiant')}: {data.login}</div>
</div> </div>
</div> </div>
<div className="row"> <div className="row">
<div className="input-group mb-3"> <div className="input-group mb-3">
<div>Activer: <ColoredCircle boolean={data.enabled}/></div> <div>{t('activer')}: <ColoredCircle boolean={data.enabled}/></div>
</div> </div>
</div> </div>
<div className="row"> <div className="row">
<div className="input-group mb-3"> <div className="input-group mb-3">
<div>Email vérifié: <ColoredCircle boolean={data.emailVerified}/></div> <div>{t('membre.emailVérifié')}: <ColoredCircle boolean={data.emailVerified}/></div>
</div> </div>
</div> </div>
</> </>

View File

@ -1,9 +1,10 @@
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {apiAxios, errFormater} from "../../../utils/Tools.js"; import {apiAxios, errFormater, getToastMessage} from "../../../utils/Tools.js";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import imageCompression from "browser-image-compression"; import imageCompression from "browser-image-compression";
import {BirthDayField, CountryList, OptionField, RoleList, TextField} from "../../../components/MemberCustomFiels.jsx"; import {BirthDayField, CountryList, OptionField, RoleList, TextField} from "../../../components/MemberCustomFiels.jsx";
import {ClubSelect} from "../../../components/ClubSelect.jsx"; import {ClubSelect} from "../../../components/ClubSelect.jsx";
import {useTranslation} from "react-i18next";
export function addPhoto(event, formData, send) { export function addPhoto(event, formData, send) {
const imageFile = event.target.url_photo.files[0]; const imageFile = event.target.url_photo.files[0];
@ -28,17 +29,19 @@ export function addPhoto(event, formData, send) {
export function InformationForm({data}) { export function InformationForm({data}) {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {t} = useTranslation();
const handleSubmit = (event) => { const handleSubmit = (event) => {
event.preventDefault(); event.preventDefault();
setLoading(1) setLoading(1)
let error = false; let error = false;
if (event.target.country?.value === "NA") { if (event.target.country?.value === "NA") {
toast.error('Veuillez sélectionner un pays valide 😕'); toast.error(t('membre.info.error1'));
error = true; error = true;
} }
if (event.target.club?.value === "Sélectionner...") { if (event.target.club?.value === t('selectionner...')) {
toast.error('Veuillez sélectionner un club valide 😕'); toast.error(t('membre.info.error2'));
error = true; error = true;
} }
@ -61,17 +64,14 @@ export function InformationForm({data}) {
formData.append("grade_arbitrage", event.target.grade_arbitrage?.value); formData.append("grade_arbitrage", event.target.grade_arbitrage?.value);
const send = (formData_) => { const send = (formData_) => {
apiAxios.put(`/member/${data.id}`, formData_, { toast.promise(
headers: { apiAxios.put(`/member/${data.id}`, formData_, {
'Accept': '*/*', headers: {
'Content-Type': 'multipart/form-data', 'Accept': '*/*',
} 'Content-Type': 'multipart/form-data',
}).then(_ => { }
toast.success('Profil mis à jours avec succès 🎉'); }), getToastMessage("membre.toast.save")
}).catch(e => { ).finally(() => {
console.log(e.response)
toast.error(errFormater(e,'Échec de la mise à jours du profil 😕'));
}).finally(() => {
if (setLoading) if (setLoading)
setLoading(0) setLoading(0)
}) })
@ -81,34 +81,33 @@ export function InformationForm({data}) {
return <form onSubmit={handleSubmit} autoComplete="off"> return <form onSubmit={handleSubmit} autoComplete="off">
<div className="card mb-4"> <div className="card mb-4">
<div className="card-header">Information</div> <div className="card-header">{t('information')}</div>
<div className="card-body"> <div className="card-body">
<TextField name="lname" text="Nom" value={data.lname}/> <TextField name="lname" text={t('nom')} value={data.lname}/>
<TextField name="fname" text="Prénom" value={data.fname}/> <TextField name="fname" text={t('prenom')} value={data.fname}/>
<TextField name="email" text="Email" value={data.email} placeholder="name@example.com" <TextField name="email" text={t('email')} value={data.email} placeholder="name@example.com"
type="email" required={false}/> type="email" required={false}/>
<OptionField name="genre" text="Genre" value={data.genre} <OptionField name="genre" text={t('genre')} value={data.genre}
values={{NA: 'N/A', H: 'H', F: 'F'}}/> values={{NA: 'N/A', H: 'H', F: 'F'}}/>
<CountryList name="country" text="Pays" value={data.country}/> <CountryList name="country" text={t('pays')} value={data.country}/>
<BirthDayField inti_date={data.birth_date ? data.birth_date.split('T')[0] : ''} <BirthDayField inti_date={data.birth_date ? data.birth_date.split('T')[0] : ''}
inti_category={data.categorie} required={false}/> inti_category={data.categorie} required={false}/>
<div className="row"> <div className="row">
<ClubSelect defaultValue={data?.club?.id} name="club" na={true}/> <ClubSelect defaultValue={data?.club?.id} name="club" na={true}/>
</div> </div>
<RoleList name="role" text="Rôle" value={data.role}/> <RoleList name="role" text={t('role')} value={data.role}/>
<OptionField name="grade_arbitrage" text="Grade d'arbitrage" value={data.grade_arbitrage} <OptionField name="grade_arbitrage" text={t('gradeDarbitrage')} value={data.grade_arbitrage}
values={{NA: 'N/A', ASSESSEUR: 'Assesseur', ARBITRE: 'Arbitre'}}/> values={{NA: 'N/A', ASSESSEUR: 'Assesseur', ARBITRE: 'Arbitre'}}/>
<div className="row"> <div className="row">
<div className="input-group mb-3"> <div className="input-group mb-3">
<label className="input-group-text" htmlFor="url_photo">Photos <label className="input-group-text" htmlFor="url_photo">{t('photos')} {t('(optionnelle)')}</label>
(optionnelle)</label>
<input type="file" className="form-control" id="url_photo" name="url_photo" <input type="file" className="form-control" id="url_photo" name="url_photo"
accept=".jpg,.jpeg,.gif,.png,.svg"/> accept=".jpg,.jpeg,.gif,.png,.svg"/>
</div> </div>
</div> </div>
<div className="row"> <div className="row">
<div className="d-grid gap-2 d-md-flex justify-content-md-end"> <div className="d-grid gap-2 d-md-flex justify-content-md-end">
<button type="submit" className="btn btn-primary">Enregistrer</button> <button type="submit" className="btn btn-primary">{t('button.enregistrer')}</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -4,8 +4,9 @@ import {useEffect, useReducer, useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faEuroSign, faPen} from "@fortawesome/free-solid-svg-icons"; import {faEuroSign, faPen} from "@fortawesome/free-solid-svg-icons";
import {AxiosError} from "../../../components/AxiosError.jsx"; import {AxiosError} from "../../../components/AxiosError.jsx";
import {apiAxios, errFormater, getSaison} from "../../../utils/Tools.js"; import {apiAxios, getSaison, getToastMessage} from "../../../utils/Tools.js";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {useTranslation} from "react-i18next";
function licenceReducer(licences, action) { function licenceReducer(licences, action) {
switch (action.type) { switch (action.type) {
@ -37,6 +38,7 @@ function licenceReducer(licences, action) {
export function LicenceCard({userData}) { export function LicenceCard({userData}) {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/licence/${userData.id}`, setLoading, 1) const {data, error} = useFetch(`/licence/${userData.id}`, setLoading, 1)
const {t} = useTranslation();
const [modalLicence, setModal] = useState({id: -1, membre: userData.id}) const [modalLicence, setModal] = useState({id: -1, membre: userData.id})
const [licences, dispatch] = useReducer(licenceReducer, []) const [licences, dispatch] = useReducer(licenceReducer, [])
@ -52,10 +54,10 @@ export function LicenceCard({userData}) {
return <div className="card mb-4 mb-md-0"> return <div className="card mb-4 mb-md-0">
<div className="card-header container-fluid"> <div className="card-header container-fluid">
<div className="row"> <div className="row">
<div className="col">Licence</div> <div className="col">{t('licence')}</div>
<div className="col" style={{textAlign: 'right'}}> <div className="col" style={{textAlign: 'right'}}>
<button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#LicenceModal" <button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#LicenceModal"
onClick={_ => setModal({id: -1, membre: userData.id})}>Ajouter onClick={_ => setModal({id: -1, membre: userData.id})}>{t('button.ajouter')}
</button> </button>
</div> </div>
</div> </div>
@ -95,16 +97,7 @@ function sendLicence(event, dispatch) {
formData.set('certificate', `${event.target.certificateBy?.value}¤${event.target.certificateDate?.value}`) formData.set('certificate', `${event.target.certificateBy?.value}¤${event.target.certificateDate?.value}`)
toast.promise( toast.promise(
apiAxios.post(`/licence/${formData.get('membre')}`, formData), apiAxios.post(`/licence/${formData.get('membre')}`, formData), getToastMessage("membre.toast.licence.save")
{
pending: "Enregistrement de la licence en cours",
success: "Licence enregistrée avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de l'enregistrement de la licence")
}
}
}
).then(data => { ).then(data => {
dispatch({type: 'UPDATE_OR_ADD', payload: data.data}) dispatch({type: 'UPDATE_OR_ADD', payload: data.data})
dispatch({type: 'SORT'}) dispatch({type: 'SORT'})
@ -114,16 +107,7 @@ function sendLicence(event, dispatch) {
function removeLicence(id, dispatch) { function removeLicence(id, dispatch) {
toast.promise( toast.promise(
apiAxios.delete(`/licence/${id}`), apiAxios.delete(`/licence/${id}`), getToastMessage("membre.toast.licence.del")
{
pending: "Suppression de la licence en cours",
success: "Licence supprimée avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de la suppression de la licence")
}
}
}
).then(_ => { ).then(_ => {
dispatch({type: 'REMOVE', payload: id}) dispatch({type: 'REMOVE', payload: id})
}) })
@ -136,6 +120,7 @@ function ModalContent({licence, dispatch}) {
const [validate, setValidate] = useState(false) const [validate, setValidate] = useState(false)
const [pay, setPay] = useState(false) const [pay, setPay] = useState(false)
const [isNew, setNew] = useState(true) const [isNew, setNew] = useState(true)
const {t} = useTranslation();
const setSeason = (event) => { const setSeason = (event) => {
setSaison(Number(event.target.value)) setSaison(Number(event.target.value))
} }
@ -179,7 +164,7 @@ function ModalContent({licence, dispatch}) {
<input name="id" value={licence.id} readOnly hidden/> <input name="id" value={licence.id} readOnly hidden/>
<input name="membre" value={licence.membre} readOnly hidden/> <input name="membre" value={licence.membre} readOnly hidden/>
<div className="modal-header"> <div className="modal-header">
<h1 className="modal-title fs-5" id="LicenceModalLabel">Edition de la licence</h1> <h1 className="modal-title fs-5" id="LicenceModalLabel">{t('editionDeLaLicence')}</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>
@ -194,25 +179,25 @@ function ModalContent({licence, dispatch}) {
<span className="input-group-text" id="basic-addon2">{saison + 1}</span> <span className="input-group-text" id="basic-addon2">{saison + 1}</span>
</div> </div>
<span>Certificat médical</span> <span>{t('certificatMédical')}</span>
<div className="input-group mb-3 "> <div className="input-group mb-3 ">
<span className="input-group-text" id="basic-addon2">Fait par</span> <span className="input-group-text" id="basic-addon2">{t('faitPar')}</span>
<input type="text" className="form-control" placeholder="Fait par" name="certificateBy" <input type="text" className="form-control" placeholder={t('faitPar')} name="certificateBy"
aria-label="Fait par" aria-describedby="basic-addon2" value={certificateBy} onChange={handleCertificateByChange}/> aria-label={t('faitPar')} aria-describedby="basic-addon2" value={certificateBy} onChange={handleCertificateByChange}/>
<span className="input-group-text" id="basic-addon2">, le</span> <span className="input-group-text" id="basic-addon2">, {t('le')}</span>
<input type="date" className="form-control" placeholder="jj/mm/aaaa" name="certificateDate" <input type="date" className="form-control" placeholder="jj/mm/aaaa" name="certificateDate"
aria-describedby="basic-addon2" value={certificateDate} onChange={handleCertificateDateChange}/> aria-describedby="basic-addon2" value={certificateDate} onChange={handleCertificateDateChange}/>
</div> </div>
<RadioGroupeOnOff name="pay" text="Paiement de la licence" value={pay} <RadioGroupeOnOff name="pay" text={t('paiementDeLaLicence')} value={pay}
onChange={handlePayChange}/> onChange={handlePayChange}/>
<RadioGroupeOnOff name="validate" text="Validation de la licence" value={validate} <RadioGroupeOnOff name="validate" text={t('validationDeLaLicence')} value={validate}
onChange={handleValidateChange}/> onChange={handleValidateChange}/>
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
<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('button.enregistrer')}</button>
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">Annuler</button> <button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">{t('button.annuler')}</button>
{isNew || <button type="button" className="btn btn-danger" data-bs-dismiss="modal" {isNew || <button type="button" className="btn btn-danger" data-bs-dismiss="modal"
onClick={() => removeLicence(licence.id, dispatch)}>Supprimer</button>} onClick={() => removeLicence(licence.id, dispatch)}>{t('button.supprimer')}</button>}
</div> </div>
</form> </form>
} }

View File

@ -7,42 +7,35 @@ import {PremForm} from "./PremForm.jsx";
import {InformationForm} from "./InformationForm.jsx"; import {InformationForm} from "./InformationForm.jsx";
import {LicenceCard} from "./LicenceCard.jsx"; import {LicenceCard} from "./LicenceCard.jsx";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {apiAxios, errFormater} from "../../../utils/Tools.js"; import {apiAxios, errFormater, getToastMessage} from "../../../utils/Tools.js";
import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx"; import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faFilePdf} from "@fortawesome/free-solid-svg-icons"; import {faFilePdf} from "@fortawesome/free-solid-svg-icons";
import {SelectCard} from "./SelectCard.jsx"; import {SelectCard} from "./SelectCard.jsx";
import {useTranslation} from "react-i18next";
const vite_url = import.meta.env.VITE_URL; const vite_url = import.meta.env.VITE_URL;
export function MemberPage() { export function MemberPage() {
const {id} = useParams() const {id} = useParams()
const navigate = useNavigate(); const navigate = useNavigate();
const {t} = useTranslation();
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/member/${id}`, setLoading, 1) const {data, error} = useFetch(`/member/${id}`, setLoading, 1)
const handleRm = () => { const handleRm = () => {
toast.promise( toast.promise(
apiAxios.delete(`/member/${id}`), apiAxios.delete(`/member/${id}`), getToastMessage("membre.toast.del")
{
pending: "Suppression du compte en cours...",
success: "Compte supprimé avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de la suppression du compte")
}
}
}
).then(_ => { ).then(_ => {
navigate(-1) navigate(-1)
}) })
} }
return <> return <>
<h2>Page membre</h2> <h2>{t('pageMembre')}</h2>
<button type="button" className="btn btn-link" onClick={() => navigate(-1)}> <button type="button" className="btn btn-link" onClick={() => navigate(-1)}>
&laquo; retour {t('back')}
</button> </button>
{data {data
? <div> ? <div>
@ -63,10 +56,10 @@ export function MemberPage() {
</div> </div>
</div> </div>
<div className="col" style={{textAlign: 'right', marginTop: '1em'}}> <div className="col" style={{textAlign: 'right', marginTop: '1em'}}>
<button className="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#confirm-delete">Supprimer le compte <button className="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#confirm-delete">{t('supprimerLeCompte')}
</button> </button>
</div> </div>
<ConfirmDialog title="Supprimer le compte" message="Êtes-vous sûr de vouloir supprimer ce compte ?" <ConfirmDialog title={t('supprimerLeCompte')} message={t('supprimerLeCompte.msg')}
onConfirm={handleRm}/> onConfirm={handleRm}/>
</div> </div>
</div> </div>
@ -77,8 +70,10 @@ export function MemberPage() {
} }
function PhotoCard({data}) { function PhotoCard({data}) {
const {t} = useTranslation();
return <div className="card mb-4"> return <div className="card mb-4">
<div className="card-header">{data.licence ? "Licence n°"+data.licence : "Pas de licence"}</div> <div className="card-header">{data.licence ? t('licenceNo', {no: data.licence}) : t('pasDeLicence')}</div>
<div className="card-body text-center"> <div className="card-body text-center">
<div className="input-group mb-3"> <div className="input-group mb-3">
<img <img
@ -89,7 +84,7 @@ function PhotoCard({data}) {
<a href={`${vite_url}/api/member/${data.id}/licence`} target='#'> <a href={`${vite_url}/api/member/${data.id}/licence`} target='#'>
<button className="btn btn-primary" type="button" id="button-addon1" <button className="btn btn-primary" type="button" id="button-addon1"
onClick={e => null}> onClick={e => null}>
Téléchargée la licence <FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon> {t('téléchargéeLaLicence')} <FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon>
</button> </button>
</a> </a>
</div> </div>

View File

@ -1,18 +1,20 @@
import {useNavigate} from "react-router-dom"; import {useNavigate} from "react-router-dom";
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {apiAxios} from "../../../utils/Tools.js"; import {apiAxios, getToastMessage} from "../../../utils/Tools.js";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {BirthDayField, CountryList, OptionField, RoleList, TextField} from "../../../components/MemberCustomFiels.jsx"; import {BirthDayField, CountryList, OptionField, RoleList, TextField} from "../../../components/MemberCustomFiels.jsx";
import {ClubSelect} from "../../../components/ClubSelect.jsx"; import {ClubSelect} from "../../../components/ClubSelect.jsx";
import {addPhoto} from "./InformationForm.jsx"; import {addPhoto} from "./InformationForm.jsx";
import {useTranslation} from "react-i18next";
export function NewMemberPage() { export function NewMemberPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const {t} = useTranslation();
return <> return <>
<h2>Page membre</h2> <h2>{t('pageMembre')}</h2>
<button type="button" className="btn btn-link" onClick={() => navigate("/admin/member")}> <button type="button" className="btn btn-link" onClick={() => navigate("/admin/member")}>
&laquo; retour {t('back')}
</button> </button>
<div> <div>
<div className="row"> <div className="row">
@ -25,6 +27,7 @@ export function NewMemberPage() {
function Form() { function Form() {
const navigate = useNavigate(); const navigate = useNavigate();
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {t} = useTranslation();
const handleSubmit = (event) => { const handleSubmit = (event) => {
event.preventDefault(); event.preventDefault();
@ -44,54 +47,51 @@ function Form() {
formData.append("grade_arbitrage", event.target.grade_arbitrage?.value); formData.append("grade_arbitrage", event.target.grade_arbitrage?.value);
const send = (formData_) => { const send = (formData_) => {
apiAxios.post(`/member`, formData_, { toast.promise(
headers: { apiAxios.post(`/member`, formData_, {
'Accept': '*/*', headers: {
'Content-Type': 'multipart/form-data', 'Accept': '*/*',
} 'Content-Type': 'multipart/form-data',
}).then(data => { }
toast.success('Profile crée avec succès 🎉'); }), getToastMessage("membre.toast.save")
).then(data => {
navigate(`/admin/member/${data.data}`) navigate(`/admin/member/${data.data}`)
}).catch(e => {
console.log(e.response)
toast.error('Échec de la création du profile 😕 (code: ' + e.response.status + ')');
}).finally(() => { }).finally(() => {
if (setLoading) if (setLoading)
setLoading(0) setLoading(0)
}) })
} }
addPhoto(event, formData, send); addPhoto(event, formData, send);
} }
return <form onSubmit={handleSubmit}> return <form onSubmit={handleSubmit}>
<div className="card mb-4"> <div className="card mb-4">
<div className="card-header">Nouveau membre</div> <div className="card-header">{t('nouveauMembre')}</div>
<div className="card-body"> <div className="card-body">
<TextField name="lname" text="Nom"/> <TextField name="lname" text={t('nom')}/>
<TextField name="fname" text="Prénom"/> <TextField name="fname" text={t('prenom')}/>
<TextField name="email" text="Email" placeholder="name@example.com" <TextField name="email" text={t('email')} placeholder="name@example.com"
type="email" required={false}/> type="email" required={false}/>
<OptionField name="genre" text="Genre" values={{NA: 'N/A', H: 'H', F: 'F'}}/> <OptionField name="genre" text={t('genre')} values={{NA: 'N/A', H: 'H', F: 'F'}}/>
<CountryList name="country" text="Pays" value={"FR"}/> <CountryList name="country" text={t('pays')} value={"FR"}/>
<BirthDayField/> <BirthDayField/>
<div className="row"> <div className="row">
<ClubSelect name="club" na={true}/> <ClubSelect name="club" na={true}/>
</div> </div>
<RoleList name="role" text="Rôle" value={'MEMBRE'}/> <RoleList name="role" text={t('role')} value={'MEMBRE'}/>
<OptionField name="grade_arbitrage" text="Grade d'arbitrage" value={'NA'} <OptionField name="grade_arbitrage" text={t('gradeDarbitrage')} value={'NA'}
values={{NA: 'N/A', ASSESSEUR: 'Assesseur', ARBITRE: 'Arbitre'}}/> values={{NA: 'N/A', ASSESSEUR: 'Assesseur', ARBITRE: 'Arbitre'}}/>
<div className="row"> <div className="row">
<div className="input-group mb-3"> <div className="input-group mb-3">
<label className="input-group-text" htmlFor="url_photo">Photos <label className="input-group-text" htmlFor="url_photo">{t('photos')}
(optionnelle)</label> {t('(optionnelle)')}</label>
<input type="file" className="form-control" id="url_photo" name="url_photo" <input type="file" className="form-control" id="url_photo" name="url_photo"
accept=".jpg,.jpeg,.gif,.png,.svg"/> accept=".jpg,.jpeg,.gif,.png,.svg"/>
</div> </div>
</div> </div>
<div className="row"> <div className="row">
<div className="d-grid gap-2 d-md-flex justify-content-md-end"> <div className="d-grid gap-2 d-md-flex justify-content-md-end">
<button type="submit" className="btn btn-primary">Créer</button> <button type="submit" className="btn btn-primary">{t('button.créer')}</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,12 +1,15 @@
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {apiAxios} from "../../../utils/Tools.js"; import {apiAxios, getToastMessage} from "../../../utils/Tools.js";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {useFetch} from "../../../hooks/useFetch.js"; import {useFetch} from "../../../hooks/useFetch.js";
import {CheckField} from "../../../components/MemberCustomFiels.jsx"; import {CheckField} from "../../../components/MemberCustomFiels.jsx";
import {AxiosError} from "../../../components/AxiosError.jsx"; import {AxiosError} from "../../../components/AxiosError.jsx";
import {useTranslation} from "react-i18next";
export function PremForm({userData}) { export function PremForm({userData}) {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {t} = useTranslation();
const handleSubmitPerm = (event) => { const handleSubmitPerm = (event) => {
event.preventDefault(); event.preventDefault();
setLoading(1) setLoading(1)
@ -17,17 +20,14 @@ export function PremForm({userData}) {
formData.append("create_compet", event.target.create_compet?.checked); formData.append("create_compet", event.target.create_compet?.checked);
formData.append("safca_super_admin", event.target.safca_super_admin?.checked); formData.append("safca_super_admin", event.target.safca_super_admin?.checked);
apiAxios.put(`/compte/${userData.userId}/roles`, formData, { toast.promise(
headers: { apiAxios.put(`/compte/${userData.userId}/roles`, formData, {
'Accept': '*/*', headers: {
'Content-Type': 'form-data', 'Accept': '*/*',
} 'Content-Type': 'form-data',
}).then(_ => { }
toast.success('Permission mise à jours avec succès 🎉'); }), getToastMessage("membre.toast.perm")
}).catch(e => { ).finally(() => {
console.log(e.response)
toast.error('Échec de la mise à jours des permissions 😕 (code: ' + e.response.status + ')');
}).finally(() => {
if (setLoading) if (setLoading)
setLoading(0) setLoading(0)
}) })
@ -35,21 +35,21 @@ export function PremForm({userData}) {
return <form onSubmit={handleSubmitPerm}> return <form onSubmit={handleSubmitPerm}>
<div className="card mb-4"> <div className="card mb-4">
<div className="card-header">Permission</div> <div className="card-header">{t('permission')}</div>
<div className="card-body"> <div className="card-body">
<div className="row g-3"> <div className="row g-3">
{userData.userId {userData.userId
? <PremFormContent userData={userData}/> ? <PremFormContent userData={userData}/>
: <div className="col"> : <div className="col">
<div className="input-group mb-3"> <div className="input-group mb-3">
<div>Ce membre ne dispose pas de compte...</div> <div>{t('membre.noAccount')}</div>
</div> </div>
</div> </div>
} }
</div> </div>
<div className="row"> <div className="row">
<div className="d-grid gap-2 d-md-flex justify-content-md-end"> <div className="d-grid gap-2 d-md-flex justify-content-md-end">
{userData.userId && <button type="submit" className="btn btn-primary">Enregistrer</button>} {userData.userId && <button type="submit" className="btn btn-primary">{t('button.enregistrer')}</button>}
</div> </div>
</div> </div>
</div> </div>
@ -60,15 +60,16 @@ export function PremForm({userData}) {
function PremFormContent({userData}) { function PremFormContent({userData}) {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/compte/${userData.userId}/roles`, setLoading, 1) const {data, error} = useFetch(`/compte/${userData.userId}/roles`, setLoading, 1)
const {t} = useTranslation();
return <> return <>
<div className="col"> <div className="col">
<h5>FFSAF intra</h5> <h5>{t('perm.ffsafIntra')}</h5>
{data {data
? <> ? <>
<CheckField name="federation_admin" text="Administrateur de la fédération" <CheckField name="federation_admin" text={t('perm.administrateurDeLaFédération')}
value={data.includes("federation_admin")}/> value={data.includes("federation_admin")}/>
<CheckField name="create_compet" text="Créer des compétion" <CheckField name="create_compet" text={t('perm.créerDesCompétion')}
value={data.includes("create_compet")}/> value={data.includes("create_compet")}/>
</> </>
: error && <AxiosError error={error}/>} : error && <AxiosError error={error}/>}

View File

@ -4,8 +4,9 @@ import {useEffect, useReducer, useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faEuroSign, faPen} from "@fortawesome/free-solid-svg-icons"; import {faEuroSign, faPen} from "@fortawesome/free-solid-svg-icons";
import {AxiosError} from "../../../components/AxiosError.jsx"; import {AxiosError} from "../../../components/AxiosError.jsx";
import {apiAxios, CatList, errFormater, getCatName, getSaison} from "../../../utils/Tools.js"; import {apiAxios, CatList, errFormater, getCatName, getSaison, getToastMessage} from "../../../utils/Tools.js";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {useTranslation} from "react-i18next";
function selectionReducer(selections, action) { function selectionReducer(selections, action) {
switch (action.type) { switch (action.type) {
@ -37,6 +38,7 @@ function selectionReducer(selections, action) {
export function SelectCard({userData}) { export function SelectCard({userData}) {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/selection/${userData.id}`, setLoading, 1) const {data, error} = useFetch(`/selection/${userData.id}`, setLoading, 1)
const {t} = useTranslation();
const [modalSelection, setModal] = useState({id: -1, membre: userData.id}) const [modalSelection, setModal] = useState({id: -1, membre: userData.id})
const [selections, dispatch] = useReducer(selectionReducer, []) const [selections, dispatch] = useReducer(selectionReducer, [])
@ -52,10 +54,10 @@ export function SelectCard({userData}) {
return <div className="card mb-4 mb-md-0"> return <div className="card mb-4 mb-md-0">
<div className="card-header container-fluid"> <div className="card-header container-fluid">
<div className="row"> <div className="row">
<div className="col">Sélection en équipe de France</div> <div className="col">{t('sélectionEnéquipeDeFrance')}</div>
<div className="col" style={{textAlign: 'right'}}> <div className="col" style={{textAlign: 'right'}}>
<button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#SelectionModal" <button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#SelectionModal"
onClick={() => setModal({id: -1, membre: userData.id, categorie: userData.categorie})}>Ajouter onClick={() => setModal({id: -1, membre: userData.id, categorie: userData.categorie})}>{t('button.ajouter')}
</button> </button>
</div> </div>
</div> </div>
@ -64,7 +66,7 @@ export function SelectCard({userData}) {
<ul className="list-group"> <ul className="list-group">
{selections.map((selection, index) => { {selections.map((selection, index) => {
return <div key={index} className="list-group-item d-flex justify-content-between align-items-start"> return <div key={index} className="list-group-item d-flex justify-content-between align-items-start">
<div className="me-auto">{selection?.saison}-{selection?.saison + 1} en {getCatName(selection?.categorie)}</div> <div className="me-auto">{selection?.saison}-{selection?.saison + 1} {t('en')} {getCatName(selection?.categorie)}</div>
<button className="badge btn btn-primary rounded-pill" data-bs-toggle="modal" <button className="badge btn btn-primary rounded-pill" data-bs-toggle="modal"
data-bs-target="#SelectionModal" onClick={_ => setModal(selection)}> data-bs-target="#SelectionModal" onClick={_ => setModal(selection)}>
<FontAwesomeIcon icon={faPen}/></button> <FontAwesomeIcon icon={faPen}/></button>
@ -98,16 +100,7 @@ function sendSelection(event, dispatch) {
membre: event.target.membre.value, membre: event.target.membre.value,
saison: event.target.saison.value, saison: event.target.saison.value,
categorie: event.target.categorie.value, categorie: event.target.categorie.value,
}), }), getToastMessage("membre.toast.select.save")
{
pending: "Enregistrement de la séléction en cours",
success: "Séléction enregistrée avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de l'enregistrement de la séléction")
}
}
}
).then(data => { ).then(data => {
dispatch({type: 'UPDATE_OR_ADD', payload: data.data}) dispatch({type: 'UPDATE_OR_ADD', payload: data.data})
dispatch({type: 'SORT'}) dispatch({type: 'SORT'})
@ -117,16 +110,7 @@ function sendSelection(event, dispatch) {
function removeSelection(id, dispatch) { function removeSelection(id, dispatch) {
toast.promise( toast.promise(
apiAxios.delete(`/selection/${id}`), apiAxios.delete(`/selection/${id}`), getToastMessage("membre.toast.select.del")
{
pending: "Suppression de la séléction en cours",
success: "Séléction supprimée avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de la suppression de la séléction")
}
}
}
).then(_ => { ).then(_ => {
dispatch({type: 'REMOVE', payload: id}) dispatch({type: 'REMOVE', payload: id})
}) })
@ -136,6 +120,7 @@ function ModalContent({selection, dispatch}) {
const [saison, setSaison] = useState(0) const [saison, setSaison] = useState(0)
const [cat, setCat] = useState("") const [cat, setCat] = useState("")
const [isNew, setNew] = useState(true) const [isNew, setNew] = useState(true)
const {t} = useTranslation();
const setSeason = (event) => { const setSeason = (event) => {
setSaison(Number(event.target.value)) setSaison(Number(event.target.value))
@ -159,15 +144,15 @@ function ModalContent({selection, dispatch}) {
<input name="id" value={selection.id} readOnly hidden/> <input name="id" value={selection.id} readOnly hidden/>
<input name="membre" value={selection.membre} readOnly hidden/> <input name="membre" value={selection.membre} readOnly hidden/>
<div className="modal-header"> <div className="modal-header">
<h1 className="modal-title fs-5" id="SelectionModalLabel">Edition de la séléction</h1> <h1 className="modal-title fs-5" id="SelectionModalLabel">{t('editionDeLaSéléction')}</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="input-group mb-3 justify-content-md-center"> <div className="input-group mb-3 justify-content-md-center">
{isNew {isNew
? <input type="number" className="form-control" placeholder="Saison" name="saison" ? <input type="number" className="form-control" placeholder={t('saison')} name="saison"
aria-label="Saison" aria-describedby="basic-addon2" value={saison} onChange={setSeason}/> aria-label={t('saison')} aria-describedby="basic-addon2" value={saison} onChange={setSeason}/>
: <><span className="input-group-text" id="basic-addon2">{saison}</span> : <><span className="input-group-text" id="basic-addon2">{saison}</span>
<input name="saison" value={saison} readOnly hidden/></>} <input name="saison" value={saison} readOnly hidden/></>}
<span className="input-group-text" id="basic-addon2">-</span> <span className="input-group-text" id="basic-addon2">-</span>
@ -175,9 +160,9 @@ function ModalContent({selection, dispatch}) {
</div> </div>
<div className="input-group mb-3"> <div className="input-group mb-3">
<label className="input-group-text" htmlFor="inputGroupSelect01">Catégorie</label> <label className="input-group-text" htmlFor="inputGroupSelect01">{t('catégorie')}</label>
<select className="form-select" id="inputGroupSelect01" value={cat} onChange={handleCatChange} name="categorie" required> <select className="form-select" id="inputGroupSelect01" value={cat} onChange={handleCatChange} name="categorie" required>
<option>Choisir...</option> <option>{t('choisir...')}</option>
{CatList.map((cat) => { {CatList.map((cat) => {
return (<option key={cat} value={cat}>{getCatName(cat)}</option>) return (<option key={cat} value={cat}>{getCatName(cat)}</option>)
})} })}
@ -186,23 +171,10 @@ function ModalContent({selection, dispatch}) {
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
<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('button.enregistrer')}</button>
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">Annuler</button> <button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">{t('button.annuler')}</button>
{isNew || <button type="button" className="btn btn-danger" data-bs-dismiss="modal" {isNew || <button type="button" className="btn btn-danger" data-bs-dismiss="modal"
onClick={() => removeSelection(selection.id, dispatch)}>Supprimer</button>} onClick={() => removeSelection(selection.id, dispatch)}>{t('button.supprimer')}</button>}
</div> </div>
</form> </form>
} }
function RadioGroupeOnOff({value, onChange, name, text}) {
return <div className="btn-group input-group mb-3 justify-content-md-center" role="group"
aria-label="Basic radio toggle button group">
<span className="input-group-text">{text}</span>
<input type="radio" className="btn-check" id={"btnradio1" + name} autoComplete="off"
value="false" checked={value === false} onChange={onChange}/>
<label className="btn btn-outline-primary" htmlFor={"btnradio1" + name}>Non</label>
<input type="radio" className="btn-check" name={name} id={"btnradio2" + name} autoComplete="off"
value="true" checked={value === true} onChange={onChange}/>
<label className="btn btn-outline-primary" htmlFor={"btnradio2" + name}>Oui</label>
</div>;
}

View File

@ -8,9 +8,12 @@ import {MyClubPage} from "./club/MyClubPage.jsx";
import {PayAndValidateList} from "../PayAndValidateList.jsx"; import {PayAndValidateList} from "../PayAndValidateList.jsx";
import {PaymentError} from "./PaymentError.jsx"; import {PaymentError} from "./PaymentError.jsx";
import {PaymentReturn} from "./PaymentReturn.jsx"; import {PaymentReturn} from "./PaymentReturn.jsx";
import {useTranslation} from "react-i18next";
export function ClubRoot() { export function ClubRoot() {
const {userinfo} = useAuth() const {userinfo} = useAuth()
const {t} = useTranslation();
let club = "" let club = ""
if (userinfo?.groups) { if (userinfo?.groups) {
for (let group of userinfo.groups) { for (let group of userinfo.groups) {
@ -23,7 +26,7 @@ export function ClubRoot() {
return <> return <>
<div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap'}}> <div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap'}}>
<h3 style={{marginLeft: '0.75em'}}>Club: {club}</h3></div> <h3 style={{marginLeft: '0.75em'}}>{t("club", {count: 1})}: {club}</h3></div>
<LoadingProvider> <LoadingProvider>
<Outlet/> <Outlet/>
</LoadingProvider> </LoadingProvider>

View File

@ -1,18 +1,21 @@
import {useSearchParams} from "react-router-dom"; import {useSearchParams} from "react-router-dom";
import {useTranslation} from "react-i18next";
export function PaymentError () { export function PaymentError() {
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const {t} = useTranslation();
const error = searchParams.get("error"); const error = searchParams.get("error");
return <div className="container"> return <div className="container">
<div className="row"> <div className="row">
<div className="col-md-12" style={{textAlign: "center"}}> <div className="col-md-12" style={{textAlign: "center"}}>
<h1>Erreur de paiement😕</h1> <h1>{t('erreurDePaiement')}</h1>
<p>Une erreur est survenue lors du traitement de votre paiement. Veuillez réessayer plus tard.</p> <p>{t('erreurDePaiement.msg')}</p>
<p><strong>Message d'erreur :</strong> {error}</p> <p><strong>{t('erreurDePaiement.detail')}</strong> {error}</p>
<button className="btn btn-primary" onClick={() => navigate("/club/member")} style={{marginTop: "0.5rem"}}>Retour à la liste de membres</button> <button className="btn btn-primary" onClick={() => navigate("/club/member")}
style={{marginTop: "0.5rem"}}>{t('retouràLaListeDeMembres')}</button>
</div> </div>
</div> </div>
</div>; </div>;

View File

@ -1,15 +1,17 @@
import {useNavigate} from "react-router-dom"; import {useNavigate} from "react-router-dom";
import {useTranslation} from "react-i18next";
export function PaymentReturn() { export function PaymentReturn() {
const navigate = useNavigate(); const navigate = useNavigate();
const {t} = useTranslation();
return <div className="container"> return <div className="container">
<div className="row"> <div className="row">
<div className="col-md-12" style={{textAlign: "center"}}> <div className="col-md-12" style={{textAlign: "center"}}>
<h1>🎉Votre paiement a été traité avec succès.🎉</h1> <h1>{t('paymentOk')}</h1>
<p>Merci pour votre paiement. Les licences devraient être activées dans l'heure qui vient, à condition que le certificat médical soit rempli.</p> <p>{t('paymentOk.msg')}</p>
<button className="btn btn-primary" onClick={() => navigate("/club/member")} style={{marginTop: "0.5rem"}}>Retour à la liste de membres</button> <button className="btn btn-primary" onClick={() => navigate("/club/member")} style={{marginTop: "0.5rem"}}>{t('retouràLaListeDeMembres')}</button>
</div> </div>
</div> </div>
</div>; </div>;

View File

@ -5,22 +5,23 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faEye, faFilePdf} from "@fortawesome/free-solid-svg-icons"; import {faEye, faFilePdf} from "@fortawesome/free-solid-svg-icons";
import {AxiosError} from "../../../components/AxiosError.jsx"; import {AxiosError} from "../../../components/AxiosError.jsx";
import {apiAxios} from "../../../utils/Tools.js"; import {apiAxios} from "../../../utils/Tools.js";
import {toast} from "react-toastify";
import {useNavigate} from "react-router-dom"; import {useNavigate} from "react-router-dom";
import {useTranslation} from "react-i18next";
const vite_url = import.meta.env.VITE_URL; const vite_url = import.meta.env.VITE_URL;
export function AffiliationCard({clubData}) { export function AffiliationCard({clubData}) {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/affiliation/${clubData.id}`, setLoading, 1) const {data, error} = useFetch(`/affiliation/${clubData.id}`, setLoading, 1)
const {t} = useTranslation();
const [modalAffiliation, setModal] = useState({id: 0, club: clubData.id}) const [modalAffiliation, setModal] = useState({id: 0, club: clubData.id})
return <div className="card mb-4"> return <div className="card mb-4">
<div className="card-header container-fluid"> <div className="card-header container-fluid">
<div className="row"> <div className="row">
<div className="col">Affiliation</div> <div className="col">{t('affiliation')}</div>
<div className="col" style={{textAlign: 'right'}}> <div className="col" style={{textAlign: 'right'}}>
<button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#RenewModal">Renouveler</button> <button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#RenewModal">{t('renouveler')}</button>
</div> </div>
</div> </div>
</div> </div>
@ -42,7 +43,7 @@ export function AffiliationCard({clubData}) {
<a href={`${vite_url}/api/club/me/affiliation`} target='#'> <a href={`${vite_url}/api/club/me/affiliation`} target='#'>
<button className="btn btn-primary" type="button" id="button-addon1" style={{marginTop: '1em'}} <button className="btn btn-primary" type="button" id="button-addon1" style={{marginTop: '1em'}}
onClick={_ => null}> onClick={_ => null}>
Télécharger lattestation daffiliation <FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon> {t('dlAff')} <FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon>
</button> </button>
</a> </a>
</div> </div>
@ -60,11 +61,12 @@ export function AffiliationCard({clubData}) {
function ModalContent({affiliation}) { function ModalContent({affiliation}) {
const navigate = useNavigate(); const navigate = useNavigate();
const {t} = useTranslation();
return <> return <>
{affiliation && <div> {affiliation && <div>
<div className="modal-header"> <div className="modal-header">
<h1 className="modal-title fs-5" id="AffiliationModalLabel">Etat de l'affiliation</h1> <h1 className="modal-title fs-5" id="AffiliationModalLabel">{t("editionDeL'affiliation")}</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>
@ -75,10 +77,10 @@ function ModalContent({affiliation}) {
<span className="input-group-text" id="basic-addon2">{(affiliation.saison || 0) + 1}</span> <span className="input-group-text" id="basic-addon2">{(affiliation.saison || 0) + 1}</span>
</div> </div>
<div className="input-group mb-3 justify-content-md-center"> <div className="input-group mb-3 justify-content-md-center">
<span className="input-group-text" id="basic-addon2">État de la demande</span> <span className="input-group-text" id="basic-addon2">{t('étatDeLaDemande')}</span>
{affiliation.validate ? <span className="input-group-text" id="basic-addon2">Validée</span> : {affiliation.validate ? <span className="input-group-text" id="basic-addon2">{t('validée')}</span> :
<> <>
<span className="input-group-text" id="basic-addon2">En attente</span> <span className="input-group-text" id="basic-addon2">{t('enAttente')}</span>
<button type="button" className="btn btn-primary" <button type="button" className="btn btn-primary"
onClick={() => navigate('/affiliation#e' + (affiliation.id * -1))} onClick={() => navigate('/affiliation#e' + (affiliation.id * -1))}
data-bs-dismiss="modal"><FontAwesomeIcon icon={faEye}/></button> data-bs-dismiss="modal"><FontAwesomeIcon icon={faEye}/></button>
@ -86,7 +88,7 @@ function ModalContent({affiliation}) {
</div> </div>
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button> <button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">{t('button.fermer')}</button>
</div> </div>
</div> </div>
} }
@ -96,10 +98,11 @@ function ModalContent({affiliation}) {
export function BureauCard({clubData}) { export function BureauCard({clubData}) {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/club/desk/${clubData.id}`, setLoading, 1) const {data, error} = useFetch(`/club/desk/${clubData.id}`, setLoading, 1)
const {t} = useTranslation();
return <> return <>
<div className="card mb-4"> <div className="card mb-4">
<div className="card-header">Bureau</div> <div className="card-header">{t('bureau')}</div>
<div className="card-body"> <div className="card-body">
<ul className="list-group"> <ul className="list-group">
{data && data.map((d, index) => { {data && data.map((d, index) => {
@ -126,6 +129,7 @@ export function BureauCard({clubData}) {
function ModalContent2({clubData, data}) { function ModalContent2({clubData, data}) {
const navigate = useNavigate(); const navigate = useNavigate();
const {t} = useTranslation();
const sendAffiliationRequest = (event) => { const sendAffiliationRequest = (event) => {
event.preventDefault() event.preventDefault()
@ -150,13 +154,12 @@ function ModalContent2({clubData, data}) {
return <form onSubmit={sendAffiliationRequest}> return <form onSubmit={sendAffiliationRequest}>
<div className="modal-header"> <div className="modal-header">
<h1 className="modal-title fs-5" id="AffiliationModalLabel">Renouvellement de l'affiliation</h1> <h1 className="modal-title fs-5" id="AffiliationModalLabel">{t('renouvellementDeLaffiliation')}</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">
<p>Veuillez sélectionner 0 à 3 membres du bureau pour remplir la pré-demande. (Si un membre non-bureau va le devenir l'an prochain, <p>{t('club.aff_renew.msg')}</p>
vous pourrez les renseigner à la prochaine étape)</p>
{data && data.map((d, index) => { {data && data.map((d, index) => {
return <div key={index} className="input-group mb-1"> return <div key={index} className="input-group mb-1">
<div className="input-group-text"> <div className="input-group-text">
@ -170,8 +173,8 @@ function ModalContent2({clubData, data}) {
)} )}
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">Annuler</button> <button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">{t('button.annuler')}</button>
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Suivant</button> <button type="submit" className="btn btn-primary" data-bs-dismiss="modal">{t('button.suivant')}</button>
</div> </div>
</form> </form>
} }

View File

@ -1,7 +1,7 @@
import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {useFetch} from "../../../hooks/useFetch.js"; import {useFetch} from "../../../hooks/useFetch.js";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {apiAxios, errFormater} from "../../../utils/Tools.js"; import {apiAxios, errFormater, getToastMessage} from "../../../utils/Tools.js";
import {AxiosError} from "../../../components/AxiosError.jsx"; import {AxiosError} from "../../../components/AxiosError.jsx";
import {AffiliationCard, BureauCard} from "./AffiliationCard.jsx"; import {AffiliationCard, BureauCard} from "./AffiliationCard.jsx";
import {CountryList, TextField} from "../../../components/MemberCustomFiels.jsx"; import {CountryList, TextField} from "../../../components/MemberCustomFiels.jsx";
@ -13,15 +13,17 @@ import {HoraireEditor} from "../../../components/Club/HoraireEditor.jsx";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faFilePdf} from "@fortawesome/free-solid-svg-icons"; import {faFilePdf} from "@fortawesome/free-solid-svg-icons";
import {SmartLogoBackground} from "../../../components/SmartLogoBackground.jsx"; import {SmartLogoBackground} from "../../../components/SmartLogoBackground.jsx";
import {useTranslation} from "react-i18next";
const vite_url = import.meta.env.VITE_URL; const vite_url = import.meta.env.VITE_URL;
export function MyClubPage() { export function MyClubPage() {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/club/me`, setLoading, 1) const {data, error} = useFetch(`/club/me`, setLoading, 1)
const {t} = useTranslation();
return <> return <>
<h3>Données administratives</h3> <h3>{t('donnéesAdministratives')}</h3>
{data {data
? <div> ? <div>
<div className="row"> <div className="row">
@ -46,37 +48,27 @@ export function MyClubPage() {
function InformationForm({data}) { function InformationForm({data}) {
const [modal, setModal] = useState({id: -1}) const [modal, setModal] = useState({id: -1})
const locationModalCallback = useRef(null) const locationModalCallback = useRef(null)
const {t} = useTranslation();
const handleSubmit = (event) => { const handleSubmit = (event) => {
event.preventDefault(); event.preventDefault();
const formData = new FormData(event.target); const formData = new FormData(event.target);
toast.promise( toast.promise(apiAxios.put(`/club/me`, formData), getToastMessage('club.toast.save'))
apiAxios.put(`/club/me`, formData),
{
pending: "Enregistrement des modifications en cours",
success: "Modifications enregistrées avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de l'enregistrement des modifications")
}
}
}
)
} }
return <> return <>
<form onSubmit={handleSubmit} autoComplete="off"> <form onSubmit={handleSubmit} autoComplete="off">
<div className="card mb-4"> <div className="card mb-4">
<input name="id" value={data.id} readOnly hidden/> <input name="id" value={data.id} readOnly hidden/>
<div className="card-header">Affiliation n°{data.no_affiliation}</div> <div className="card-header">{t('affiliationNo', {no: data.no_affiliation})}</div>
<div className="card-body text-center"> <div className="card-body text-center">
<TextField name="name" text="Nom" value={data.name} disabled={true}/> <TextField name="name" text={t('nom')} value={data.name} disabled={true}/>
<CountryList name="country" text="Pays" value={data.country} disabled={true}/> <CountryList name="country" text={t('pays')} value={data.country} disabled={true}/>
{!data.international && <> {!data.international && <>
<TextField name="state_id" text="SIRET ou RNA" value={data.state_id} disabled={true}/> <TextField name="state_id" text={t('siretOuRna')} value={data.state_id} disabled={true}/>
</>} </>}
<div className="row mb-3"> <div className="row mb-3">
@ -91,17 +83,17 @@ function InformationForm({data}) {
<button className="btn btn-outline-secondary" type="button" id="button-addon1" <button className="btn btn-outline-secondary" type="button" id="button-addon1"
onClick={_ => null}> onClick={_ => null}>
<FontAwesomeIcon icon={faFilePdf} size="5x"></FontAwesomeIcon><br/> <FontAwesomeIcon icon={faFilePdf} size="5x"></FontAwesomeIcon><br/>
Voir les statues {t('voirLesStatues')}
</button> </button>
</a> </a>
</div> </div>
<div className="form-text" id="status">Pour modifier les informations ci-dessus, merci de contacter la FFSAF par mail.</div> <div className="form-text" id="status">{t('club.change.status')}</div>
</div> </div>
{!data.international && <> {!data.international && <>
<TextField name="contact_intern" text="Contact interne" value={data.contact_intern} required={false} <TextField name="contact_intern" text={t('contactInterne')} value={data.contact_intern} required={false}
placeholder="example@test.com"/> placeholder="example@test.com"/>
<TextField name="address" text="Adresse administrative" value={data.address} placeholder="Adresse administrative"/> <TextField name="address" text={t('adresseAdministrative')} value={data.address} placeholder={t('adresseAdministrative')}/>
<ContactEditor data={data}/> <ContactEditor data={data}/>
<LocationEditor data={data} setModal={setModal} sendData={locationModalCallback}/> <LocationEditor data={data} setModal={setModal} sendData={locationModalCallback}/>
@ -111,7 +103,7 @@ function InformationForm({data}) {
</div> </div>
<div className="row mb-3"> <div className="row mb-3">
<div className="d-grid gap-2 d-md-flex justify-content-md-end"> <div className="d-grid gap-2 d-md-flex justify-content-md-end">
<button type="submit" className="btn btn-primary">Enregistrer</button> <button type="submit" className="btn btn-primary">{t('button.enregistrer')}</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,14 +1,14 @@
import {toast} from "react-toastify";
import {apiAxios} from "../../../utils/Tools.js";
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {useFetch} from "../../../hooks/useFetch.js"; import {useFetch} from "../../../hooks/useFetch.js";
import {ColoredCircle} from "../../../components/ColoredCircle.jsx"; import {ColoredCircle} from "../../../components/ColoredCircle.jsx";
import {AxiosError} from "../../../components/AxiosError.jsx"; import {AxiosError} from "../../../components/AxiosError.jsx";
import {useTranslation} from "react-i18next";
export function CompteInfo({userData}) { export function CompteInfo({userData}) {
const {t} = useTranslation();
return <div className="card mb-4"> return <div className="card mb-4">
<div className="card-header">Compte</div> <div className="card-header">{t('compte')}</div>
<div className="card-body text-center"> <div className="card-body text-center">
{userData.userId {userData.userId
? <CompteInfoContent userData={userData}/> ? <CompteInfoContent userData={userData}/>
@ -16,8 +16,8 @@ export function CompteInfo({userData}) {
<> <>
<div className="row"> <div className="row">
<div className="input-group mb-3"> <div className="input-group mb-3">
<div>Ce membre ne dispose pas de compte... <br/> <div>{t('membre.noAccount')}<br/>
Un compte sera créé par la fédération lors de la validation de sa première licence {t('membre.noAccount.clubMsg')}
</div> </div>
</div> </div>
</div> </div>
@ -30,23 +30,24 @@ export function CompteInfo({userData}) {
function CompteInfoContent({userData}) { function CompteInfoContent({userData}) {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/compte/${userData.userId}`, setLoading, 1) const {data, error} = useFetch(`/compte/${userData.userId}`, setLoading, 1)
const {t} = useTranslation();
return <> return <>
{data {data
? <> ? <>
<div className="row"> <div className="row">
<div className="input-group mb-3"> <div className="input-group mb-3">
<div>Identifiant: {data.login}</div> <div>{t('membre.identifiant')}: {data.login}</div>
</div> </div>
</div> </div>
<div className="row"> <div className="row">
<div className="input-group mb-3"> <div className="input-group mb-3">
<div>Activer: <ColoredCircle boolean={data.enabled}/></div> <div>{t('activer')}: <ColoredCircle boolean={data.enabled}/></div>
</div> </div>
</div> </div>
<div className="row"> <div className="row">
<div className="input-group mb-3"> <div className="input-group mb-3">
<div>Email vérifié: <ColoredCircle boolean={data.emailVerified}/></div> <div>{t('membre.emailVérifié')}: <ColoredCircle boolean={data.emailVerified}/></div>
</div> </div>
</div> </div>
</> </>

View File

@ -1,13 +1,16 @@
// noinspection DuplicatedCode // noinspection DuplicatedCode
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {apiAxios} from "../../../utils/Tools.js"; import {apiAxios, getToastMessage} from "../../../utils/Tools.js";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {BirthDayField, CountryList, OptionField, RoleList, TextField} from "../../../components/MemberCustomFiels.jsx"; import {BirthDayField, CountryList, OptionField, RoleList, TextField} from "../../../components/MemberCustomFiels.jsx";
import {addPhoto} from "../../admin/member/InformationForm.jsx"; import {addPhoto} from "../../admin/member/InformationForm.jsx";
import {useTranslation} from "react-i18next";
export function InformationForm({data}) { export function InformationForm({data}) {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {t} = useTranslation();
const handleSubmit = (event) => { const handleSubmit = (event) => {
event.preventDefault(); event.preventDefault();
setLoading(1) setLoading(1)
@ -24,17 +27,14 @@ export function InformationForm({data}) {
formData.append("role", event.target.role?.value); formData.append("role", event.target.role?.value);
const send = (formData_) => { const send = (formData_) => {
apiAxios.put(`/member/club/${data.id}`, formData_, { toast.promise(
headers: { apiAxios.put(`/member/club/${data.id}`, formData_, {
'Accept': '*/*', headers: {
'Content-Type': 'multipart/form-data', 'Accept': '*/*',
} 'Content-Type': 'multipart/form-data',
}).then(_ => { }
toast.success('Profile mis à jours avec succès 🎉'); }), getToastMessage("membre.toast.save")
}).catch(e => { ).finally(() => {
console.log(e.response)
toast.error('Échec de la mise à jours du profile 😕 (code: ' + e.response.status + ': ' + e.response.data + ')');
}).finally(() => {
if (setLoading) if (setLoading)
setLoading(0) setLoading(0)
}) })
@ -44,33 +44,30 @@ export function InformationForm({data}) {
return <form onSubmit={handleSubmit} autoComplete="off"> return <form onSubmit={handleSubmit} autoComplete="off">
<div className="card mb-4"> <div className="card mb-4">
<div className="card-header">Information</div> <div className="card-header">{t('information')}</div>
<div className="card-body"> <div className="card-body">
<TextField name="lname" text="Nom" value={data.lname}/> <TextField name="lname" text={t('nom')} value={data.lname}/>
<TextField name="fname" text="Prénom" value={data.fname}/> <TextField name="fname" text={t('prenom')} value={data.fname}/>
<TextField name="email" text="Email" value={data.email} placeholder="name@example.com" <TextField name="email" text={t('email')} value={data.email} placeholder="name@example.com"
type="email" ttip={<small className="form-text">L'email sert à la création de compte pour se connecter au site et doit être unique. <br/> type="email" ttip={<small className="form-text">{t('membre.info.emailInfo')}</small>}/>
Pour les mineurs, l'email des parents peut être utilisé plusieurs fois grâce à la syntaxe suivante : {'email.parent+<caractères alphanumériques>@exemple.com'}.<br/> <OptionField name="genre" text={t('genre')} value={data.genre}
Exemples : mail.parent+1@exemple.com, mail.parent+titouan@exemple.com, mail.parent+cedrique@exemple.com</small>}/>
<OptionField name="genre" text="Genre" value={data.genre}
values={{NA: 'N/A', H: 'H', F: 'F'}}/> values={{NA: 'N/A', H: 'H', F: 'F'}}/>
<CountryList name="country" text="Pays" value={data.country}/> <CountryList name="country" text={t('pays')} value={data.country}/>
<BirthDayField inti_date={data.birth_date ? data.birth_date.split('T')[0] : ''} <BirthDayField inti_date={data.birth_date ? data.birth_date.split('T')[0] : ''}
inti_category={data.categorie}/> inti_category={data.categorie}/>
<RoleList name="role" text="Rôle" value={data.role}/> <RoleList name="role" text={t('role')} value={data.role}/>
<OptionField name="grade_arbitrage" text="Grade d'arbitrage" value={data.grade_arbitrage} <OptionField name="grade_arbitrage" text={t('gradeDarbitrage')} value={data.grade_arbitrage}
values={{NA: 'N/A', ASSESSEUR: 'Assesseur', ARBITRE: 'Arbitre'}} disabled={true}/> values={{NA: 'N/A', ASSESSEUR: 'Assesseur', ARBITRE: 'Arbitre'}} disabled={true}/>
<div className="row"> <div className="row">
<div className="input-group mb-3"> <div className="input-group mb-3">
<label className="input-group-text" htmlFor="url_photo">Photos <label className="input-group-text" htmlFor="url_photo">{t('photos')} {t('(optionnelle)')}</label>
(optionnelle)</label>
<input type="file" className="form-control" id="url_photo" name="url_photo" <input type="file" className="form-control" id="url_photo" name="url_photo"
accept=".jpg,.jpeg,.gif,.png,.svg"/> accept=".jpg,.jpeg,.gif,.png,.svg"/>
</div> </div>
</div> </div>
<div className="row"> <div className="row">
<div className="d-grid gap-2 d-md-flex justify-content-md-end"> <div className="d-grid gap-2 d-md-flex justify-content-md-end">
<button type="submit" className="btn btn-primary">Enregistrer</button> <button type="submit" className="btn btn-primary">{t('button.enregistrer')}</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -2,11 +2,12 @@ import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {useFetch} from "../../../hooks/useFetch.js"; import {useFetch} from "../../../hooks/useFetch.js";
import {useEffect, useReducer, useState} from "react"; import {useEffect, useReducer, useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faEuroSign, faInfo, faPen} from "@fortawesome/free-solid-svg-icons"; import {faInfo, faPen} from "@fortawesome/free-solid-svg-icons";
import {AxiosError} from "../../../components/AxiosError.jsx"; import {AxiosError} from "../../../components/AxiosError.jsx";
import {apiAxios, errFormater, getSaison} from "../../../utils/Tools.js"; import {apiAxios, getSaison, getToastMessage} from "../../../utils/Tools.js";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {ColoredText} from "../../../components/ColoredCircle.jsx"; import {ColoredText} from "../../../components/ColoredCircle.jsx";
import {useTranslation} from "react-i18next";
function licenceReducer(licences, action) { function licenceReducer(licences, action) {
switch (action.type) { switch (action.type) {
@ -32,6 +33,7 @@ function licenceReducer(licences, action) {
export function LicenceCard({userData}) { export function LicenceCard({userData}) {
const defaultLicence = {id: -1, membre: userData.id, validate: false, saison: getSaison(), certificate: false} const defaultLicence = {id: -1, membre: userData.id, validate: false, saison: getSaison(), certificate: false}
const {t} = useTranslation();
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/licence/${userData.id}`, setLoading, 1) const {data, error} = useFetch(`/licence/${userData.id}`, setLoading, 1)
@ -49,11 +51,11 @@ export function LicenceCard({userData}) {
return <div className="card mb-4 mb-md-0"> return <div className="card mb-4 mb-md-0">
<div className="card-header container-fluid"> <div className="card-header container-fluid">
<div className="row"> <div className="row">
<div className="col">Licence</div> <div className="col">{t('licence')}</div>
<div className="col" style={{textAlign: 'right'}}> <div className="col" style={{textAlign: 'right'}}>
<button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#LicenceModal" <button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#LicenceModal"
onClick={() => setModal(defaultLicence)} onClick={() => setModal(defaultLicence)}
disabled={licences.some(licence => licence.saison === getSaison())}>Demander disabled={licences.some(licence => licence.saison === getSaison())}>{t('demander')}
</button> </button>
</div> </div>
</div> </div>
@ -93,16 +95,7 @@ function sendLicence(event, dispatch) {
formData.set('certificate', `${event.target.certificateBy?.value}¤${event.target.certificateDate?.value}`) formData.set('certificate', `${event.target.certificateBy?.value}¤${event.target.certificateDate?.value}`)
toast.promise( toast.promise(
apiAxios.post(`/licence/club/${formData.get('membre')}`, formData), apiAxios.post(`/licence/club/${formData.get('membre')}`, formData), getToastMessage("membre.toast.licence.ask")
{
pending: "Enregistrement de la demande de licence en cours",
success: "Demande de licence enregistrée avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de la demande de licence")
}
}
}
).then(data => { ).then(data => {
dispatch({type: 'UPDATE_OR_ADD', payload: data.data}) dispatch({type: 'UPDATE_OR_ADD', payload: data.data})
dispatch({type: 'SORT'}) dispatch({type: 'SORT'})
@ -112,16 +105,7 @@ function sendLicence(event, dispatch) {
function removeLicence(id, dispatch) { function removeLicence(id, dispatch) {
toast.promise( toast.promise(
apiAxios.delete(`/licence/club/${id}`), apiAxios.delete(`/licence/club/${id}`), getToastMessage("membre.toast.licence.ask.del")
{
pending: "Suppression de la demande en cours",
success: "Demande supprimée avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de la suppression de la demande de licence")
}
}
}
).then(_ => { ).then(_ => {
dispatch({type: 'REMOVE', payload: id}) dispatch({type: 'REMOVE', payload: id})
}) })
@ -131,6 +115,7 @@ function ModalContent({licence, dispatch}) {
const [certificateBy, setCertificateBy] = useState("") const [certificateBy, setCertificateBy] = useState("")
const [certificateDate, setCertificateDate] = useState("") const [certificateDate, setCertificateDate] = useState("")
const [isNew, setNew] = useState(true) const [isNew, setNew] = useState(true)
const {t} = useTranslation();
const handleCertificateByChange = (event) => { const handleCertificateByChange = (event) => {
setCertificateBy(event.target.value.replaceAll('¤', '')); setCertificateBy(event.target.value.replaceAll('¤', ''));
@ -142,10 +127,10 @@ function ModalContent({licence, dispatch}) {
useEffect(() => { useEffect(() => {
if (licence.id !== -1) { if (licence.id !== -1) {
setNew(false) setNew(false)
if (licence.certificate === null){ if (licence.certificate === null) {
setCertificateBy("") setCertificateBy("")
setCertificateDate("") setCertificateDate("")
}else { } else {
setCertificateBy(licence.certificate.split('¤')[0]) setCertificateBy(licence.certificate.split('¤')[0])
setCertificateDate(licence.certificate.split('¤')[1]) setCertificateDate(licence.certificate.split('¤')[1])
} }
@ -163,36 +148,36 @@ function ModalContent({licence, dispatch}) {
<input name="membre" value={licence.membre} readOnly hidden/> <input name="membre" value={licence.membre} readOnly hidden/>
<div className="modal-header"> <div className="modal-header">
<h1 className="modal-title fs-5" id="LicenceModalLabel"> <h1 className="modal-title fs-5" id="LicenceModalLabel">
{isNew ? "Demande de licence " : "Edition de la demande "} {isNew ? t('demandeDeLicence') : t('editionDeLaDemande')}
(saison {licence.saison}-{licence.saison + 1})</h1> ({t('saison')} {licence.saison}-{licence.saison + 1})</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">
<span>Certificat médical</span> <span>{t('certificatMédical')}</span>
<div className="input-group mb-3 "> <div className="input-group mb-3 ">
<span className="input-group-text" id="basic-addon2">Fait par</span> <span className="input-group-text" id="basic-addon2">{t('faitPar')}</span>
<input type="text" className="form-control" placeholder="Fait par" name="certificateBy" disabled={licence.validate} <input type="text" className="form-control" placeholder={t('faitPar')} name="certificateBy" disabled={licence.validate}
aria-label="Fait par" aria-describedby="basic-addon2" value={certificateBy} onChange={handleCertificateByChange}/> aria-label={t('faitPar')} aria-describedby="basic-addon2" value={certificateBy} onChange={handleCertificateByChange}/>
<span className="input-group-text" id="basic-addon2">, le</span> <span className="input-group-text" id="basic-addon2">, {t('le')}</span>
<input type="date" className="form-control" placeholder="jj/mm/aaaa" name="certificateDate" disabled={licence.validate} <input type="date" className="form-control" placeholder="jj/mm/aaaa" name="certificateDate" disabled={licence.validate}
aria-describedby="basic-addon2" value={certificateDate} onChange={handleCertificateDateChange}/> aria-describedby="basic-addon2" value={certificateDate} onChange={handleCertificateDateChange}/>
</div> </div>
<div className="input-group mb-3 justify-content-md-center"> <div className="input-group mb-3 justify-content-md-center">
<div>Paiement de la licence: <ColoredText boolean={licence.pay}/></div> <div>{t('paiementDeLaLicence')}: <ColoredText boolean={licence.pay}/></div>
</div> </div>
<div className="input-group mb-3 justify-content-md-center"> <div className="input-group mb-3 justify-content-md-center">
<div>Validation de la licence: <ColoredText boolean={licence.validate}/></div> <div>{t('validationDeLaLicence')}: <ColoredText boolean={licence.validate}/></div>
</div> </div>
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
{currentSaison && !licence.validate && {currentSaison && !licence.validate &&
<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('button.enregistrer')}</button>}
<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('button.fermer')}</button>
{currentSaison && !licence.validate && licence.id !== -1 && !licence.pay && {currentSaison && !licence.validate && licence.id !== -1 && !licence.pay &&
<button type="button" className="btn btn-danger" data-bs-dismiss="modal" <button type="button" className="btn btn-danger" data-bs-dismiss="modal"
onClick={() => removeLicence(licence.id, dispatch)}>Annuler</button>} onClick={() => removeLicence(licence.id, dispatch)}>{t('button.annuler')}</button>}
</div> </div>
</form> </form>
} }

View File

@ -6,42 +6,35 @@ import {CompteInfo} from "./CompteInfo.jsx";
import {InformationForm} from "./InformationForm.jsx"; import {InformationForm} from "./InformationForm.jsx";
import {LicenceCard} from "./LicenceCard.jsx"; import {LicenceCard} from "./LicenceCard.jsx";
import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx"; import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx";
import {apiAxios, errFormater} from "../../../utils/Tools.js"; import {apiAxios, errFormater, getToastMessage} from "../../../utils/Tools.js";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faFilePdf} from "@fortawesome/free-solid-svg-icons"; import {faFilePdf} from "@fortawesome/free-solid-svg-icons";
import {SelectCard} from "./SelectCard.jsx"; import {SelectCard} from "./SelectCard.jsx";
import {useTranslation} from "react-i18next";
const vite_url = import.meta.env.VITE_URL; const vite_url = import.meta.env.VITE_URL;
export function MemberPage() { export function MemberPage() {
const {id} = useParams() const {id} = useParams()
const navigate = useNavigate(); const navigate = useNavigate();
const {t} = useTranslation();
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/member/${id}`, setLoading, 1) const {data, error} = useFetch(`/member/${id}`, setLoading, 1)
const handleRm = () => { const handleRm = () => {
toast.promise( toast.promise(
apiAxios.delete(`/member/club/${id}`), apiAxios.delete(`/member/club/${id}`), getToastMessage("membre.toast.del")
{
pending: "Suppression du compte en cours...",
success: "Compte supprimé avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de la suppression du compte")
}
}
}
).then(_ => { ).then(_ => {
navigate(-1) navigate(-1)
}) })
} }
return <> return <>
<h2>Page membre</h2> <h2>{t('pageMembre')}</h2>
<button type="button" className="btn btn-link" onClick={() => navigate(-1)}> <button type="button" className="btn btn-link" onClick={() => navigate(-1)}>
&laquo; retour {t('back')}
</button> </button>
{data {data
? <div> ? <div>
@ -62,10 +55,10 @@ export function MemberPage() {
</div> </div>
{data.licence == null && {data.licence == null &&
<div className="col" style={{textAlign: 'right', marginTop: '1em'}}> <div className="col" style={{textAlign: 'right', marginTop: '1em'}}>
<button className="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#confirm-delete">Supprimer le compte <button className="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#confirm-delete">{t('supprimerLeCompte')}
</button> </button>
</div>} </div>}
<ConfirmDialog title="Supprimer le compte" message="Êtes-vous sûr de vouloir supprimer ce compte ?" <ConfirmDialog title={t('supprimerLeCompte')} message={t('supprimerLeCompte.msg')}
onConfirm={handleRm}/> onConfirm={handleRm}/>
</div> </div>
</div> </div>
@ -76,8 +69,10 @@ export function MemberPage() {
} }
function PhotoCard({data}) { function PhotoCard({data}) {
const {t} = useTranslation();
return <div className="card mb-4"> return <div className="card mb-4">
<div className="card-header">{data.licence ? "Licence n°" + data.licence : "Pas de licence"}</div> <div className="card-header">{data.licence ? t('licenceNo', {no: data.licence}) : t('pasDeLicence')}</div>
<div className="card-body text-center"> <div className="card-body text-center">
<div className="input-group mb-3"> <div className="input-group mb-3">
<img <img
@ -88,7 +83,7 @@ function PhotoCard({data}) {
<a href={`${vite_url}/api/member/${data.id}/licence`} target='#'> <a href={`${vite_url}/api/member/${data.id}/licence`} target='#'>
<button className="btn btn-primary" type="button" id="button-addon1" <button className="btn btn-primary" type="button" id="button-addon1"
onClick={e => null}> onClick={e => null}>
Téléchargée la licence <FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon> {t('téléchargéeLaLicence')} <FontAwesomeIcon icon={faFilePdf}></FontAwesomeIcon>
</button> </button>
</a> </a>
</div> </div>

View File

@ -1,17 +1,19 @@
import {useNavigate} from "react-router-dom"; import {useNavigate} from "react-router-dom";
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {apiAxios} from "../../../utils/Tools.js"; import {apiAxios, getToastMessage} from "../../../utils/Tools.js";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {BirthDayField, CountryList, OptionField, TextField} from "../../../components/MemberCustomFiels.jsx"; import {BirthDayField, CountryList, OptionField, TextField} from "../../../components/MemberCustomFiels.jsx";
import {addPhoto} from "../../admin/member/InformationForm.jsx"; import {addPhoto} from "../../admin/member/InformationForm.jsx";
import {useTranslation} from "react-i18next";
export function NewMemberPage() { export function NewMemberPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const {t} = useTranslation();
return <> return <>
<h2>Page membre</h2> <h2>{t('pageMembre')}</h2>
<button type="button" className="btn btn-link" onClick={() => navigate("/club/member")}> <button type="button" className="btn btn-link" onClick={() => navigate("/club/member")}>
&laquo; retour {t('back')}
</button> </button>
<div> <div>
<div className="row"> <div className="row">
@ -24,6 +26,7 @@ export function NewMemberPage() {
function Form() { function Form() {
const navigate = useNavigate(); const navigate = useNavigate();
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {t} = useTranslation();
const handleSubmit = (event) => { const handleSubmit = (event) => {
event.preventDefault(); event.preventDefault();
@ -40,48 +43,45 @@ function Form() {
formData.append("email", event.target.email?.value); formData.append("email", event.target.email?.value);
const send = (formData_) => { const send = (formData_) => {
apiAxios.post(`/member/club`, formData_, { toast.promise(
headers: { apiAxios.post(`/member/club`, formData_, {
'Accept': '*/*', headers: {
'Content-Type': 'multipart/form-data', 'Accept': '*/*',
} 'Content-Type': 'multipart/form-data',
}).then(data => { }
toast.success('Profile crée avec succès 🎉'); }), getToastMessage("membre.toast.save")
).then(data => {
navigate(`/club/member/${data.data}`) navigate(`/club/member/${data.data}`)
}).catch(e => {
console.log(e.response)
toast.error('Échec de la création du profile 😕 (code: ' + e.response.status + ': ' + e.response.data + ')');
}).finally(() => { }).finally(() => {
if (setLoading) if (setLoading)
setLoading(0) setLoading(0)
}) })
} }
addPhoto(event, formData, send); addPhoto(event, formData, send);
} }
return <form onSubmit={handleSubmit}> return <form onSubmit={handleSubmit}>
<div className="card mb-4"> <div className="card mb-4">
<div className="card-header">Nouveau membre</div> <div className="card-header">{t('nouveauMembre')}</div>
<div className="card-body"> <div className="card-body">
<TextField name="lname" text="Nom"/> <TextField name="lname" text={t('nom')}/>
<TextField name="fname" text="Prénom"/> <TextField name="fname" text={t('prenom')}/>
<TextField name="email" text="Email" placeholder="name@example.com" <TextField name="email" text={t('email')} placeholder="name@example.com"
type="email"/> type="email"/>
<OptionField name="genre" text="Genre" values={{NA: 'N/A', H: 'H', F: 'F'}}/> <OptionField name="genre" text={t('genre')} values={{NA: 'N/A', H: 'H', F: 'F'}}/>
<CountryList name="country" text="Pays" value={"FR"}/> <CountryList name="country" text={t('pays')} value={"FR"}/>
<BirthDayField/> <BirthDayField/>
<div className="row"> <div className="row">
<div className="input-group mb-3"> <div className="input-group mb-3">
<label className="input-group-text" htmlFor="url_photo">Photos <label className="input-group-text" htmlFor="url_photo">{t('photos')}
(optionnelle)</label> {t('(optionnelle)')}</label>
<input type="file" className="form-control" id="url_photo" name="url_photo" <input type="file" className="form-control" id="url_photo" name="url_photo"
accept=".jpg,.jpeg,.gif,.png,.svg"/> accept=".jpg,.jpeg,.gif,.png,.svg"/>
</div> </div>
</div> </div>
<div className="row"> <div className="row">
<div className="d-grid gap-2 d-md-flex justify-content-md-end"> <div className="d-grid gap-2 d-md-flex justify-content-md-end">
<button type="submit" className="btn btn-primary">Créer</button> <button type="submit" className="btn btn-primary">{t('button.créer')}</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -2,22 +2,24 @@ import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {useFetch} from "../../../hooks/useFetch.js"; import {useFetch} from "../../../hooks/useFetch.js";
import {getCatName} from "../../../utils/Tools.js"; import {getCatName} from "../../../utils/Tools.js";
import {AxiosError} from "../../../components/AxiosError.jsx"; import {AxiosError} from "../../../components/AxiosError.jsx";
import {useTranslation} from "react-i18next";
export function SelectCard({userData}) { export function SelectCard({userData}) {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/selection/${userData.id}`, setLoading, 1) const {data, error} = useFetch(`/selection/${userData.id}`, setLoading, 1)
const {t} = useTranslation();
return <div className="card mb-4 mb-md-0"> return <div className="card mb-4 mb-md-0">
<div className="card-header container-fluid"> <div className="card-header container-fluid">
<div className="row"> <div className="row">
<div className="col">Sélection en équipe de France</div> <div className="col">{t('sélectionEnéquipeDeFrance')}</div>
</div> </div>
</div> </div>
<div className="card-body"> <div className="card-body">
<ul className="list-group"> <ul className="list-group">
{data && data.sort((a, b) => b.saison - a.saison).map((selection, index) => { {data && data.sort((a, b) => b.saison - a.saison).map((selection, index) => {
return <div key={index} className="list-group-item d-flex justify-content-between align-items-start"> return <div key={index} className="list-group-item d-flex justify-content-between align-items-start">
<div className="me-auto">{selection?.saison}-{selection?.saison + 1} en {getCatName(selection?.categorie)}</div> <div className="me-auto">{selection?.saison}-{selection?.saison + 1} {t('en')} {getCatName(selection?.categorie)}</div>
</div> </div>
})} })}
{error && <AxiosError error={error}/>} {error && <AxiosError error={error}/>}

View File

@ -1,4 +1,5 @@
import axios from "axios"; import axios from "axios";
import i18n from "../config/i18n.js";
const vite_url = import.meta.env.VITE_URL; const vite_url = import.meta.env.VITE_URL;
@ -78,34 +79,46 @@ export function getSaison(currentDate = new Date()) {
export function getCatName(cat) { export function getCatName(cat) {
switch (cat) { switch (cat) {
case "SUPER_MINI": case "SUPER_MINI":
return "Super Mini"; return i18n.t('cat.superMini');
case "MINI_POUSSIN": case "MINI_POUSSIN":
return "Mini Poussin"; return i18n.t('cat.miniPoussin');
case "POUSSIN": case "POUSSIN":
return "Poussin"; return i18n.t('cat.poussin');
case "BENJAMIN": case "BENJAMIN":
return "Benjamin"; return i18n.t('cat.benjamin');
case "MINIME": case "MINIME":
return "Minime"; return i18n.t('cat.minime');
case "CADET": case "CADET":
return "Cadet"; return i18n.t('cat.cadet');
case "JUNIOR": case "JUNIOR":
return "Junior"; return i18n.t('cat.junior');
case "SENIOR1": case "SENIOR1":
return "Senior 1"; return i18n.t('cat.senior1');
case "SENIOR2": case "SENIOR2":
return "Senior 2"; return i18n.t('cat.senior2');
case "VETERAN1": case "VETERAN1":
return "Vétéran 1"; return i18n.t('cat.vétéran1');
case "VETERAN2": case "VETERAN2":
return "Vétéran 2"; return i18n.t('cat.vétéran2');
case null: case null:
return "Catégorie inconnue"; return i18n.t('cat.catégorieInconnue');
default: default:
return cat; return cat;
} }
} }
export function getToastMessage(msgKey) {
return {
pending: i18n.t(msgKey + '.pending'),
success: i18n.t(msgKey + '.success'),
error: {
render({data}) {
return errFormater(data, i18n.t(msgKey + '.error'))
}
}
}
}
export function win(scores) { export function win(scores) {
let sum = 0 let sum = 0
for (const score of scores) { for (const score of scores) {