Merge branch 'opengotha' into 'pairing2'

Opengotha

See merge request tournois/pairgoth!1
This commit is contained in:
Claude BRISSON
2023-09-29 11:33:51 +00:00
47 changed files with 427 additions and 28 deletions

View File

@@ -103,6 +103,7 @@
</httpConnector>
<systemProperties>
<pairgoth.api.url>http://localhost:8085/api/</pairgoth.api.url>
<pairgoth.env>dev</pairgoth.env>
</systemProperties>
<webApp>
<resourceBases>${project.basedir}/src/main/webapp,${project.build.directory}/generated-resources/</resourceBases>

View File

@@ -7,12 +7,14 @@ import org.apache.velocity.runtime.parser.node.ASTText
import org.apache.velocity.runtime.parser.node.SimpleNode
import org.jeudego.pairgoth.web.WebappManager
import org.slf4j.LoggerFactory
import java.io.File
import java.io.PrintWriter
import java.io.StringWriter
import java.nio.charset.StandardCharsets
import java.nio.file.Path
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentSkipListSet
import java.util.regex.Pattern
import kotlin.io.path.readLines
import kotlin.io.path.useDirectoryEntries
@@ -79,7 +81,7 @@ class Translator private constructor(private val iso: String) {
if (groupStart > start) output.print(text.substring(start, groupStart))
val capture = matcher.group(group)
var token: String = StringEscapeUtils.unescapeHtml4(capture)
if (StringUtils.containsOnly(token, "\r\n\t -;:.\"/<>\u00A00123456789€!")) output.print(capture) else {
if (StringUtils.containsOnly(token, "\r\n\t -;:.'\"/<>\u00A00123456789€[]!")) output.print(capture) else {
token = normalize(token)
token = translate(token)
output.print(StringEscapeUtils.escapeHtml4(token))
@@ -104,17 +106,15 @@ class Translator private constructor(private val iso: String) {
map[0] = (ignoring != null)
while (pos < text.length) {
if (ignoring == null) {
val nextIgnore = ignoredTags.map { tag ->
Pair(tag, text.indexOf("<$tag(?:>\\s)"))
}.filter {
it.second != -1
}.sortedBy {
it.second
}.firstOrNull()
val nextIgnore = ignoredTags.mapNotNull { tag ->
Regex("<($tag)(?:>|\\s)").find(text)
}.minByOrNull {
it.range.first
}
if (nextIgnore == null) pos = text.length
else {
ignoring = nextIgnore.first
pos += nextIgnore.first.length + 2
ignoring = nextIgnore.groupValues[1]
pos += ignoring.length + 2
}
} else {
val closingTag = text.indexOf("</$ignoring>")
@@ -132,9 +132,12 @@ class Translator private constructor(private val iso: String) {
get() = textAccessor[this] as String
set(value: String) { textAccessor[this] = value }
private val saveMissingTranslations = System.getProperty("pairgoth.env") == "dev"
private val missingTranslations: MutableSet<String> = ConcurrentSkipListSet()
private fun reportMissingTranslation(enText: String) {
logger.warn("missing translation towards {}: {}", iso, enText)
// CB TODO - create file
if (saveMissingTranslations) missingTranslations.add(enText)
}
companion object {
@@ -165,5 +168,18 @@ class Translator private constructor(private val iso: String) {
val providedLanguages = setOf("en", "fr")
const val defaultLanguage = "en"
internal fun notifyExiting() {
translators.values.filter {
it.saveMissingTranslations && it.missingTranslations.isNotEmpty()
}.forEach {
val missing = File("${it.iso}.missing")
logger.info("Saving missing translations for ${it.iso} to ${missing.canonicalPath}")
missing.printWriter().use { out ->
out.println(it.missingTranslations.map { "${it}\t" }.joinToString("\n"))
}
}
}
}
}

View File

@@ -2,6 +2,7 @@ package org.jeudego.pairgoth.web
import com.republicate.mailer.SmtpLoop
import org.apache.commons.lang3.tuple.Pair
import org.jeudego.pairgoth.util.Translator
import org.slf4j.LoggerFactory
import java.io.IOException
import java.lang.IllegalAccessError
@@ -51,7 +52,7 @@ class WebappManager : ServletContextListener, ServletContextAttributeListener, H
/* ServletContextListener interface */
override fun contextInitialized(sce: ServletContextEvent) {
context = sce.servletContext
logger.info("---------- Starting Pairgoth Web Client ----------")
logger.info("---------- Starting $WEBAPP_NAME ----------")
context.setAttribute("manager", this)
webappRoot = context.getRealPath("/")
try {
@@ -78,11 +79,15 @@ class WebappManager : ServletContextListener, ServletContextAttributeListener, H
}
override fun contextDestroyed(sce: ServletContextEvent) {
logger.info("---------- Stopping Web Application ----------")
logger.info("---------- Stopping $WEBAPP_NAME ----------")
Translator.notifyExiting()
val context = sce.servletContext
for (service in webServices.keys) stopService(service, true)
// ??? DriverManager.deregisterDriver(com.mysql.cj.jdbc.Driver ...);
logger.info("---------- Stopped $WEBAPP_NAME ----------")
}
/* ServletContextAttributeListener interface */
@@ -95,6 +100,7 @@ class WebappManager : ServletContextListener, ServletContextAttributeListener, H
override fun sessionDestroyed(se: HttpSessionEvent) {}
companion object {
const val WEBAPP_NAME = "Pairgoth Web Client"
const val PAIRGOTH_PROPERTIES_PREFIX = "pairgoth."
lateinit var webappRoot: String
lateinit var context: ServletContext

View File

@@ -1,10 +1,10 @@
@import "/lib/fomantic-ui-2.8.7/semantic.css" layer(semantic);
@import "/lib/fomantic-ui-2.9.2/semantic.min.css" layer(semantic);
@layer pairgoth {
/* general styles */
body {
font-size: clamp(12px, .8rem + .25vw, 20px);
font-size: clamp(14px, 1rem + 1vw, 24px);
width: 100vw;
height: 100vh;
margin: 0;
@@ -30,6 +30,7 @@
height: 3em;
width: 100%;
position: relative;
align-items: flex-start;
justify-content: space-between;
#logo {
height: 100%;
@@ -41,13 +42,15 @@
}
#center {
max-width: clamp(800px, 80vw, 100vw);
flex: 1;
overflow: auto;
#inner {
max-width: clamp(800px, 80vw, 100vw);
display: flex;
flex-flow: column nowrap;
align-items: center;
margin-left: auto;
margin-right: auto;
}
}
@@ -63,13 +66,53 @@
padding: 0.5em;
}
/* buttons */
button {
font-size: inherit;
/* modal and dimmer */
#dimmer {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: black;
opacity: 0;
transition: opacity 0.5s;
z-index: 1000;
pointer-events: none;
}
button.floating {
body.dimmed #dimmer {
display: block;
opacity: 0.85;
pointer-events: all;
}
.ui.modal {
display: block;
opacity: 0;
transition: opacity 0.5s;
pointer-events: none;
}
.ui.modal .header {
font-size: 1em;
align-items: baseline;
justify-content: space-around;
flex-wrap: wrap;
}
.active.ui.modal {
font-size: 1em;
opacity: 1;
pointer-events: all;
}
/* buttons */
.button {
font-size: 1em;
}
.button.floating {
box-shadow: 0px 8px 8px -5px rgba(0,0,0,35%);
transition: all 0.3s;
&.white {
@@ -88,13 +131,16 @@
}
/* languages */
#lang {
position: relative;
cursor: pointer;
transform: scale(1.2);
#lang-list {
position: absolute;
display: none;
top:100%;
left: -200%;
right: 1em;
flex-flow: column nowrap;
padding: 0.5em;
gap: 0.5em;
@@ -106,6 +152,7 @@
}
a {
display: inline-block;
white-space: nowrap;
text-align: center;
i {
vertical-align: middle;
@@ -114,4 +161,24 @@
}
}
/* UI fixes */
.ui.form, .ui.segment, .ui.form .field > label { font-size: 1em; }
span > input[type="radio"] { vertical-align: text-top; }
span > input[type="text"] { vertical-align: baseline; width: initial; }
span > input.date { vertical-align: baseline; width: 8em; }
.step, .step .title { font-size: 1em; }
.step:before { font-size: 1em; }
.step.description { font-size: 0.8em; }
.step:first-child { padding-left: 1em; }
.step:last-child { padding-right: 1em; }
.step .description { display: none; }
input[type="number"] {
padding: 0.2em 0.1em 0.2em 1em;
vertical-align: baseline;
width: 3.5em;
}
}

View File

@@ -8,6 +8,7 @@
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="stylesheet" href="/lib/fork-awesome-1.2.0/fork-awesome.min.css">
<link rel="stylesheet" href="/css/main.css">
<script type="text/javascript" src="/js/domhelper.js"></script>
</head>
<body class="vert flex">
<div id="header" class="horz flex">
@@ -36,9 +37,9 @@
<div id="version">pairgoth v0.1</div>
<div id="contact"><a href="mailto:pairgoth@jeudego.org">contact</a></div>
</div>
<div id="dimmer"></div>
<script type="text/javascript" src="/js/store2-2.14.2.min.js"></script>
<script type="text/javascript" src="/js/tablesort-5.4.0.min.js"></script>
<script type="text/javascript" src="/js/domhelper.js"></script>
<script type="text/javascript" src="/js/formproxy.js"></script>
<script type="text/javascript" src="/js/api.js"></script>
<script type="text/javascript" src="/js/main.js"></script>

View File

@@ -1,2 +1,28 @@
New tournament Nouveau tournoi
Open Ouvrir
Cancel Annuler
Infos Infos
Next Suivant
Pairing Appariement
Type Type
(pairgo / rengo) (pairgo / rengo)
(teams of individual players) (équipes de joueurs individuels)
MacMahon MacMahon
Number of rounds Nombre de rondes
Partner teams tournament of Tournoi d'équipes partenaires de
Standard tournament of individual players Tournoi standard de joueurs individuels
Swiss Suisse
Teams tournament of Tournoi par équipes de
Tournament dates Dates du tournoi
Tournament name Nom du tournoi
Tournament pairing Appariement du tournoi
Tournament short name Nom abbrégé du tournoi
Tournament type Type de tournoi
end date date de fin
from du
players joueurs
rounds rondes
short_name nom_tournoi
start date date de début
to au

View File

@@ -12,3 +12,61 @@
</button>
</div>
#end
##
## New Tournament dialog
##
<div id="new-tournament" class="ui fullscreen modal">
<i class="close icon"></i>
<div class="horz flex header">
<span>New tournament</span>
<div class="ui ordered unstackable steps">
<div class="active step">
<div class="content">
<div class="title">Infos</div>
<div class="description">name, place and date</div>
</div>
</div>
<div class="step">
<div class="content">
<div class="title">Type</div>
<div class="description">teams or players, rounds</div>
</div>
</div>
<div class="step">
<div class="content">
<div class="title">Pairing</div>
<div class="description">pairing system</div>
</div>
</div>
</div>
</div>
<div class="scrolling content">
#translate('tournament-form.inc.html')
</div>
<div class="actions">
<button class="ui cancel black floating button">Cancel</button>
<button class="ui next green right labeled icon floating button">
<i class="checkmark icon"></i>
Next
</button>
</div></div>
<script type="text/javascript">
const lang = '${request.lang}';
// #[[
onLoad(() => {
$('#new').on('click', e => {
$('#new-tournament').modal(true);
});
new DateRangePicker($('#date-range')[0], {
language: lang
});
$('#new-tournament .tab.segment:first-child').addClass('active');
});
// ]]#
</script>
<!-- date range picker -->
<script type="text/javascript" src="/lib/datepicker-1.3.3/datepicker-full.min.js"></script>
<script type="text/javascript" src="/lib/datepicker-1.3.3/locales/${request.lang}.js"></script>
<link rel="stylesheet" href="/lib/datepicker-1.3.3/datepicker.min.css">

View File

@@ -1,3 +1,5 @@
// This small library is meant to be a lightweight replacement of jQuery basic functions.
window.$ = document.querySelectorAll.bind(document);
Node.prototype.on = window.on = function (eventNames, fn) {
let events = eventNames.split(' ')
@@ -5,31 +7,51 @@ Node.prototype.on = window.on = function (eventNames, fn) {
let name = events[i];
this.addEventListener(name, fn);
}
return this;
};
NodeList.prototype.__proto__ = Array.prototype;
NodeList.prototype.on = NodeList.prototype.addEventListener = function (eventNames, fn) {
this.forEach(function (elem, i) {
elem.on(eventNames, fn);
});
return this;
}
NodeList.prototype.addClass = function(className) {
this.forEach(function (elem, i) {
elem.classList.add(className);
});
return this;
}
Element.prototype.addClass = function(className) {
this.classList.add(className);
return this;
}
NodeList.prototype.removeClass = function(className) {
this.forEach(function (elem, i) {
elem.classList.remove(className);
});
return this;
}
Element.prototype.removeClass = function(className) {
this.classList.remove(className);
return this;
}
NodeList.prototype.toggleClass = function(className) {
this.forEach(function (elem, i) {
elem.classList.toggle(className);
});
return this;
}
Element.prototype.toggleClass = function(className) {
this.classList.toggle(className);
return this;
}
NodeList.prototype.hasClass = function(className) {
return this.item(0).classList.contains(className);
}
Element.prototype.toggleClass = function(className) {
this.classList.contains(className);
}
Node.prototype.offset = function() {
let _x = 0;
let _y = 0;
@@ -42,21 +64,34 @@ Node.prototype.offset = function() {
return { top: _y, left: _x };
}
NodeList.prototype.offset = function() {
this.item(0).offset() // CB TODO review
this.item(0).offset();
}
Element.prototype.attr = function (key) {
return this.attributes[key].value
return this.attributes[key].value;
}
NodeList.prototype.attr = function(key) {
this.item(0).attr(key) // CB TODO review
this.item(0).attr(key);
}
Element.prototype.data = function (key) {
return this.attributes[`data-${key}`].value
}
NodeList.prototype.data = function(key) {
this.item(0).data(key) // CB TODO review
this.item(0).data(key);
}
NodeList.prototype.show = function(key) {
this.item(0).show(key);
return this;
}
Element.prototype.show = function (key) {
this.style.display = 'block';
}
NodeList.prototype.hide = function(key) {
this.item(0).hide(key);
return this;
}
Element.prototype.hide = function (key) {
this.style.display = 'none';
}
let initFunctions = [];
function onLoad(fct) {

View File

@@ -83,3 +83,39 @@ function exportCSV(filename, content) {
link.click();
document.body.removeChild(link);
}
/* modals */
NodeList.prototype.modal = function(show) {
this.item(0).modal(show);
return this;
}
Element.prototype.modal = function(show) {
if (show) {
document.body.addClass('dimmed');
this.addClass('active');
}
else {
this.removeClass('active');
document.body.removeClass('dimmed');
}
return this;
}
onLoad(() => {
/*
document.on('click', e => {
if (!e.target.closest('.modal')) $('.modal').hide();
})
*/
$('i.close.icon').on('click', e => {
let modal = e.target.closest('.modal');
if (modal) modal.modal(false);
});
$('.modal .actions .cancel').on('click', e => {
e.target.closest('.modal').modal(false);
});
$('#dimmer').on('click', e => $('.modal').modal(false));
});

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.datepicker{width:-moz-min-content;width:min-content}.datepicker:not(.active){display:none}.datepicker-dropdown{padding-top:4px;position:absolute;z-index:20}.datepicker-dropdown.datepicker-orient-top{padding-bottom:4px;padding-top:0}.datepicker-picker{background-color:#fff;border-radius:4px;display:flex;flex-direction:column}.datepicker-dropdown .datepicker-picker{box-shadow:0 2px 3px hsla(0,0%,4%,.1),0 0 0 1px hsla(0,0%,4%,.1)}.datepicker-main{flex:auto;padding:2px}.datepicker-footer{background-color:#f5f5f5;box-shadow:inset 0 1px 1px hsla(0,0%,4%,.1)}.datepicker-title{background-color:#f5f5f5;box-shadow:inset 0 -1px 1px hsla(0,0%,4%,.1);font-weight:700;padding:.375rem .75rem;text-align:center}.datepicker-controls{display:flex}.datepicker-header .datepicker-controls{padding:2px 2px 0}.datepicker-controls .button{align-items:center;background-color:#fff;border:1px solid #dcdcdc;border-radius:4px;box-shadow:none;color:#363636;cursor:pointer;display:inline-flex;font-size:1rem;height:2.25em;justify-content:center;line-height:1.5;margin:0;padding:calc(.375em - 1px) .75em;position:relative;text-align:center;vertical-align:top;white-space:nowrap}.datepicker-controls .button:active,.datepicker-controls .button:focus{outline:none}.datepicker-controls .button:hover{border-color:#b8b8b8;color:#363636}.datepicker-controls .button:focus{border-color:#3273dc;color:#363636}.datepicker-controls .button:focus:not(:active){box-shadow:0 0 0 .125em rgba(50,115,220,.25)}.datepicker-controls .button:active{border-color:#474747;color:#363636}.datepicker-controls .button[disabled]{cursor:not-allowed}.datepicker-header .datepicker-controls .button{border-color:transparent;font-weight:700}.datepicker-header .datepicker-controls .button:hover{background-color:#f9f9f9}.datepicker-header .datepicker-controls .button:active{background-color:#f2f2f2}.datepicker-footer .datepicker-controls .button{border-radius:2px;flex:auto;font-size:.75rem;margin:calc(.375rem - 1px) .375rem}.datepicker-controls .view-switch{flex:auto}.datepicker-controls .next-button,.datepicker-controls .prev-button{flex:0 0 14.2857142857%;padding-left:.375rem;padding-right:.375rem}.datepicker-controls .next-button.disabled,.datepicker-controls .prev-button.disabled{visibility:hidden}.datepicker-grid,.datepicker-view{display:flex}.datepicker-view{align-items:stretch;width:15.75rem}.datepicker-grid{flex:auto;flex-wrap:wrap}.datepicker .days{display:flex;flex:auto;flex-direction:column}.datepicker .days-of-week{display:flex}.datepicker .week-numbers{display:flex;flex:0 0 9.6774193548%;flex-direction:column}.datepicker .weeks{align-items:stretch;display:flex;flex:auto;flex-direction:column}.datepicker span{-webkit-touch-callout:none;align-items:center;border-radius:4px;cursor:default;display:flex;justify-content:center;-webkit-user-select:none;-moz-user-select:none;user-select:none}.datepicker .dow{font-size:.875rem;font-weight:700;height:1.5rem}.datepicker .week{color:#b8b8b8;flex:auto;font-size:.75rem}.datepicker .days .dow,.datepicker-cell{flex-basis:14.2857142857%}.datepicker-cell{height:2.25rem}.datepicker-cell:not(.day){flex-basis:25%;height:4.5rem}.datepicker-cell:not(.disabled):hover{background-color:#f9f9f9;cursor:pointer}.datepicker-cell.focused:not(.selected){background-color:#e9e9e9}.datepicker-cell.selected,.datepicker-cell.selected:hover{background-color:#3273dc;color:#fff;font-weight:600}.datepicker-cell.disabled{color:#dcdcdc}.datepicker-cell.next:not(.disabled),.datepicker-cell.prev:not(.disabled){color:#7a7a7a}.datepicker-cell.next.selected,.datepicker-cell.prev.selected{color:#e6e6e6}.datepicker-cell.highlighted:not(.selected):not(.range):not(.today){background-color:#f5f5f5;border-radius:0}.datepicker-cell.highlighted:not(.selected):not(.range):not(.today):not(.disabled):hover{background-color:#efefef}.datepicker-cell.highlighted:not(.selected):not(.range):not(.today).focused{background-color:#e9e9e9}.datepicker-cell.today:not(.selected){background-color:#00d1b2}.datepicker-cell.today:not(.selected):not(.disabled){color:#fff}.datepicker-cell.today.focused:not(.selected){background-color:#00ccad}.datepicker-cell.range-end:not(.selected),.datepicker-cell.range-start:not(.selected){background-color:#b8b8b8;color:#fff}.datepicker-cell.range-end.focused:not(.selected),.datepicker-cell.range-start.focused:not(.selected){background-color:#b3b3b3}.datepicker-cell.range-start:not(.range-end){border-radius:4px 0 0 4px}.datepicker-cell.range-end:not(.range-start){border-radius:0 4px 4px 0}.datepicker-cell.range{background-color:#dcdcdc;border-radius:0}.datepicker-cell.range:not(.disabled):not(.focused):not(.today):hover{background-color:#d7d7d7}.datepicker-cell.range.disabled{color:#c6c6c6}.datepicker-cell.range.focused{background-color:#d1d1d1}.datepicker-input.in-edit{border-color:#276bda}.datepicker-input.in-edit:active,.datepicker-input.in-edit:focus{box-shadow:0 0 .25em .25em rgba(39,107,218,.2)}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
New locales for the datepicker can be downloaded from [the datepicker repository](https://github.com/mymth/vanillajs-datepicker/tree/master/dist/js/locales)

View File

@@ -0,0 +1,18 @@
/**
* British English translation for bootstrap-datepicker
* Xavier Dutreilh <xavier@dutreilh.com>
*/
(function () {
Datepicker.locales['en-GB'] = {
days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"],
months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
today: "Today",
monthsTitle: "Months",
clear: "Clear",
weekStart: 1,
format: "dd/mm/yyyy"
};
}());

View File

@@ -0,0 +1,18 @@
/**
* French translation for bootstrap-datepicker
* Nico Mollet <nico.mollet@gmail.com>
*/
(function () {
Datepicker.locales.fr = {
days: ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"],
daysShort: ["dim.", "lun.", "mar.", "mer.", "jeu.", "ven.", "sam."],
daysMin: ["d", "l", "ma", "me", "j", "v", "s"],
months: ["janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août", "septembre", "octobre", "novembre", "décembre"],
monthsShort: ["janv.", "févr.", "mars", "avril", "mai", "juin", "juil.", "août", "sept.", "oct.", "nov.", "déc."],
today: "Aujourd'hui",
monthsTitle: "Mois",
clear: "Effacer",
weekStart: 1,
format: "dd/mm/yyyy"
};
}());

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -0,0 +1,84 @@
<form class="tournament-form" class="ui form">
<div class="ui infos tab segment">
<div class="field">
<label>Tournament name</label>
<input type="text" name="name" required placeholder="Tournament name"/>
</div>
<div class="field">
<label>Tournament short name</label>
<input type="text" name="shortname" required placeholder="short_name"/>
</div>
<div class="field">
<label>Tournament dates</label>
<span class="date-range">from <input type="text" name="start" required class="date" placeholder="start date"/> to <input type="text" name="end" required class="date" placeholder="end date"/></span>
</div>
<div class="field">
<label>Number of rounds</label>
<span><input type="number" name="rounds" required min="1"/> rounds</span>
</div>
</div>
<div class="ui type tab segment">
<div class="grouped unstackable fields">
<label>Tournament type</label>
<div class="field">
<div class="ui radio">
<label>
<input type="radio" name="type" value="standard" required/>
Standard tournament of individual players
</label>
</div>
</div>
<div class="field">
<div class="ui radio">
<label>
<input type="radio" name="type" value="partners" required/>
<span>Partner teams tournament of <input type="number" min="2" name="partners"/> players</span> (pairgo / rengo)
</label>
</div>
</div>
<div class="field">
<div class="ui radio">
<label>
<input type="radio" name="type" value="teams" required/>
<span>Teams tournament of <input type="number" min="2" name="partners"/> players</span> (teams of individual players)
</label>
</div>
</div>
</div>
</div>
<div class="ui pairing tab segment">
<div class="grouped unstackable fields">
<label>Tournament pairing</label>
<div class="field">
<div class="ui radio">
<label>
<input type="radio" name="pairing" value="macmahon" required/>
MacMahon
</label>
</div>
</div>
<div class="field">
<div class="ui radio">
<label>
<input type="radio" name="pairing" value="swiss" required/>
Swiss
</label>
</div>
</div>
</div>
</div>
</form>
<script type="text/javascript">
onLoad(() => {
$('#tournament-form').on('input', e => {
})
});
</script>
CB TODO :
s'il y a duplication du formulaire (pour nouveau/édition), il faut changer les ids en class, gérer les pbs d'init du date-range, etc.
sinon, il faut paramétrer la dialog (mais les steps 1/2/3 deviennent des tabs, etc.)
Donc dans tous les cas il y a qqc à faire !