Pairables list in progress

This commit is contained in:
Claude Brisson
2023-12-21 10:37:16 +01:00
parent fdcdd9c1a9
commit ea9e298330
11 changed files with 166 additions and 36 deletions

View File

@@ -16,6 +16,7 @@ object PairingHandler: PairgothApiHandler {
override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? {
val tournament = getTournament(request)
val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
if (round > tournament.lastRound() + 1) badRequest("invalid round: previous round has not been played")
val playing = tournament.games(round).values.flatMap {
listOf(it.black, it.white)
}.toSet()

View File

@@ -6,25 +6,37 @@ import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.internal.EMPTY_REQUEST
import org.slf4j.LoggerFactory
class ApiTool {
companion object {
const val JSON = "application/json"
val apiRoot = System.getProperty("pairgoth.api.external.url")?.let { "${it.removeSuffix("/")}/" }
?: throw Error("no configured API url")
val logger = LoggerFactory.getLogger("api")
}
private val client = OkHttpClient()
private fun prepare(url: String) = Request.Builder().url("$apiRoot$url").header("Accept", JSON)
private fun Json.toRequestBody() = toString().toRequestBody(JSON.toMediaType())
private fun Request.Builder.process(): Json {
client.newCall(build()).execute().use { response ->
if (response.isSuccessful) {
when (response.body?.contentType()?.subtype) {
null -> throw Error("null body or content type")
"json" -> return Json.parse(response.body!!.string()) ?: throw Error("could not parse json")
else -> throw Error("unhandled content type: ${response.body!!.contentType()}")
try {
return client.newCall(build()).execute().use { response ->
if (response.isSuccessful) {
when (response.body?.contentType()?.subtype) {
null -> throw Error("null body or content type")
"json" -> Json.parse(response.body!!.string()) ?: throw Error("could not parse json")
else -> throw Error("unhandled content type: ${response.body!!.contentType()}")
}
} else {
when (response.body?.contentType()?.subtype) {
"json" -> Json.parse(response.body!!.string()) ?: throw Error("could not parse error json")
else -> throw Error("${response.code} ${response.message}")
}
}
} else throw Error("api call failed: ${response.code} ${response.message}")
}
} catch (e: Throwable) {
logger.error("api call failed", e)
return Json.Object("error" to e.message)
}
}

View File

@@ -0,0 +1,11 @@
package org.jeudego.pairgoth.view
import com.republicate.kson.Json
/**
* Generic utilities
*/
class PairgothTool {
public fun toMap(array: Json.Array) = array.map { ser -> ser as Json.Object }.associateBy { it.getLong("id")!! }
}

View File

@@ -164,7 +164,7 @@
max-width: 40vw;
border: solid 2px darkgray;
border-radius: 5px;
padding-top: 1em;
padding: 1em 0.5em;
&:before {
position: absolute;
content: attr(title);
@@ -175,6 +175,21 @@
font-size: smaller;
font-weight: bold;
}
.listitem {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
gap: 1em;
cursor: pointer;
user-select: none;
&:hover {
background-color: rgba(50, 50, 50, .2);
}
&.selected {
background-color: rgba(100,200,255,200);
cursor: grab;
}
}
}
#pairing-buttons {
display: flex;

View File

@@ -4,13 +4,14 @@
<toolbox scope="application">
<tool key="translate" class="org.jeudego.pairgoth.view.TranslationTool"/>
<tool key="strings" class="org.apache.commons.lang3.StringUtils"/>
<tool key="utils" class="org.jeudego.pairgoth.view.PairgothTool"/>
<!--
<tool key="number" format="#0.00"/>
<tool key="date" locale="fr_FR" format="yyyy-MM-dd"/>
<tool key="inflector" class="org.atteo.evo.inflector.English"/>
-->
</toolbox>
Exception
<toolbox scope="session">
<!--
<tool key="oauth" class="org.jeudego.egc2024.tool.OAuthTool"/>

View File

@@ -33,6 +33,12 @@ function success() {
$('#error').addClass('hidden');
}
function showError(message) {
console.error(message);
$('#error')[0].innerText = message;
$('#error').removeClass('hidden');
}
function error(response) {
const contentType = response.headers.get("content-type");
let promise =
@@ -40,10 +46,8 @@ function error(response) {
? response.json().then(json => json.error || "unknown error")
: Promise.resolve(response.statusText);
promise.then(message => {
message = message.replaceAll(/([a-z])([A-Z])/g,"$1 $2").toLowerCase()
console.error(message);
$('#error')[0].innerText = message;
$('#error').removeClass('hidden');
message = message.replaceAll(/([a-z])([A-Z])/g,"$1 $2").toLowerCase();
showError(message);
});
}

View File

@@ -66,17 +66,41 @@ Node.prototype.offset = function() {
NodeList.prototype.offset = function() {
this.item(0).offset();
}
Element.prototype.attr = function (key) {
return this.attributes[key].value;
Element.prototype.attr = function (key, value) {
if (typeof(value) === 'undefined') {
return this.attributes[key].value;
} else {
this.setAttribute(key, value);
return this;
}
}
NodeList.prototype.attr = function(key) {
this.item(0).attr(key);
NodeList.prototype.attr = function(key, value) {
if (typeof(value) === 'undefined') {
return this.item(0).attr(key);
} else {
this.forEach(elem => {
elem.attr(key, value);
});
return this;
}
}
Element.prototype.data = function (key) {
return this.attributes[`data-${key}`].value
Element.prototype.data = function (key, value) {
if (typeof(value) === 'undefined') {
return this.attributes[`data-${key}`].value
} else {
this.setAttribute(`data-${key}`, value);
return this;
}
}
NodeList.prototype.data = function(key) {
this.item(0).data(key);
NodeList.prototype.data = function(key, value) {
if (typeof(value) === 'undefined') {
this.item(0).data(key);
} else {
this.forEach(elem => {
elem.data(key, value);
})
return this;
}
}
NodeList.prototype.show = function() {
this.item(0).show();
@@ -147,3 +171,25 @@ NodeList.prototype.focus = function() {
let first = this.item(0);
if (first) first.focus();
}
Element.prototype.index = function(selector) {
let i = 0;
let child = this;
while ((child = child.previousSibling) != null) {
if (typeof(selector) === 'undefined' || child.nodeType === Node.ELEMENT_NODE && child.matches(selector)) {
++i;
}
}
return i;
}
NodeList.prototype.filter = function(selector) {
let result = [];
this.forEach(elem => {
if (elem.nodeType === Node.ELEMENT_NODE && elem.matches(selector)) {
result.push(elem);
}
});
return Reflect.construct(Array, result, NodeList);
}

View File

@@ -1,2 +1,23 @@
let focused = undefined;
onLoad(()=>{
$('.listitem').on('click', e => {
if (e.shiftKey && typeof(focused) !== 'undefined') {
let from = focused.index('.listitem');
let to = e.target.closest('.listitem').index('.listitem');
if (from > to) {
let tmp = from;
from = to;
to = tmp;
}
let parent = e.target.closest('.multi-select');
let children = parent.childNodes.filter('.listitem');
for (let j = from; j <= to; ++j) {
children.item(j).addClass('selected');
children.item(j).attr('draggable', true);
}
} else {
focused = e.target.closest('.listitem').toggleClass('selected').attr('draggable', true);
}
});
});

View File

@@ -1,14 +1,26 @@
#set($paired = $api.get("tour/${params.id}/part"))
<div class="tab-content" id="pairing">
<div id="pairing-content">
<div id="pairing-round">
Pairings for round
<button class="ui floating choose-round prev-round button">&laquo;</button>
<span class="active-round">1</span>
<span class="active-round">$round</span>
<button class="ui floating choose-round next-round button">&raquo;</button>
</div>
<div id="pairing-lists">
<div id="pairables" class="multi-select" title="pairable players">
#set($pairables = $api.get("tour/${params.id}/pair/$round"))
#if($pairables.isObject() && $pairables.error)
<script type="text/javascript">
onLoad(() => {
showError("$pairables.error")
});
</script>
#else
#foreach($p in $pairables)
#set($part = $pmap[$p])
<div class="listitem pairable"><span>$part.name $part.firstname</span><span>$part.country #rank($part.rank)</span></div>
#end
#end
</div>
<div id="pairing-buttons">
<button id="pair" class="ui blue right labeled icon floating button">

View File

@@ -2,6 +2,7 @@
<div id="reg-view">
<div id="players-list" class="roundbox">
#set($parts = $api.get("tour/${params.id}/part"))
#set($pmap = $utils.toMap($parts))
<table id="players" class="ui celled selectable striped table">
<thead>
<th>name</th>

View File

@@ -20,6 +20,15 @@
</div>
#end
#end
## set up some global context templating variables
#if($tour)
#set($round = $math.toInteger($!params.round))
#if(!$round)
#set($round = 1)
#else
#set($round = $math.min($math.max($round, 1), $tour.rounds))
#end
#end
<div class="section">
<h1 class="centered title">#if($tour)$tour.name#{else}New Tournament#end</h1>
#if($tour)
@@ -65,7 +74,8 @@
#if($tour)
const tour_id = ${tour.id};
const tour_rounds = ${tour.rounds};
let activeRound = 1;
let activeRound = ${round};
// $params
#end
#set($datepickerLocale = $translate.datepickerLocale($request.lang, $request.loc))
const datepickerLocale = '$datepickerLocale';
@@ -106,12 +116,6 @@
}
if (window.location.hash) {
let step = window.location.hash.substring(1);
let suffix = /^(.)+-(\d+)$/.exec(step);
if (suffix) {
step = suffix[1];
activeRound = parseInt(suffix[2]);
$('.active-round').forEach(e => e.innerHTML = `${activeRound}`);
}
chooseStep(step);
}
@@ -148,14 +152,16 @@
$('.next-round').addClass('disabled');
}
$('.prev-round').on('click', e => {
let base = window.location.href.replace(/-\d+$/g, '');
window.location.href = `#${base}-${activeRound - 1}`;
window.location.reload();
let round = activeRound - 1;
if (round > 0) {
window.location.search = `id=${tour_id}&round=${round}`
}
});
$('.next-round').on('click', e => {
let base = window.location.href.replace(/-\d+$/g, '');
window.location.href = `#${base}-${activeRound + 1}`;
window.location.reload();
let round = activeRound + 1;
if (round <= tour_rounds) {
window.location.search = `id=${tour_id}&round=${round}`
}
});
});
// ]]#