Pairables list in progress
This commit is contained in:
@@ -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()
|
||||
|
@@ -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 ->
|
||||
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" -> return Json.parse(response.body!!.string()) ?: throw Error("could not parse json")
|
||||
"json" -> Json.parse(response.body!!.string()) ?: throw Error("could not parse json")
|
||||
else -> throw Error("unhandled content type: ${response.body!!.contentType()}")
|
||||
}
|
||||
} else throw Error("api call failed: ${response.code} ${response.message}")
|
||||
} 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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
logger.error("api call failed", e)
|
||||
return Json.Object("error" to e.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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")!! }
|
||||
}
|
@@ -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;
|
||||
|
@@ -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"/>
|
||||
|
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -66,17 +66,41 @@ Node.prototype.offset = function() {
|
||||
NodeList.prototype.offset = function() {
|
||||
this.item(0).offset();
|
||||
}
|
||||
Element.prototype.attr = function (key) {
|
||||
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) {
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@@ -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">«</button>
|
||||
<span class="active-round">1</span>
|
||||
<span class="active-round">$round</span>
|
||||
<button class="ui floating choose-round next-round button">»</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">
|
||||
|
@@ -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>
|
||||
|
@@ -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}`
|
||||
}
|
||||
});
|
||||
});
|
||||
// ]]#
|
||||
|
Reference in New Issue
Block a user