plateforme-tfjm2/draw/static/draw.js

743 lines
32 KiB
JavaScript

(async () => {
// check notification permission
// This is useful to alert people that they should do something
await Notification.requestPermission()
})()
const problems_count = JSON.parse(document.getElementById('problems_count').textContent)
const tournaments = JSON.parse(document.getElementById('tournaments_list').textContent)
const sockets = {}
const messages = document.getElementById('messages')
/**
* Request to abort the draw of the given tournament.
* Only volunteers are allowed to do this.
* @param tid The tournament id
*/
function abortDraw(tid) {
sockets[tid].send(JSON.stringify({'type': 'abort'}))
}
/**
* Request to launch a dice between 1 and 100, for the two first steps.
* The parameter `trigram` can be specified (by volunteers) to launch a dice for a specific team.
* @param tid The tournament id
* @param trigram The trigram of the team that a volunteer wants to force the dice launch (default: null)
*/
function drawDice(tid, trigram = null) {
sockets[tid].send(JSON.stringify({'type': 'dice', 'trigram': trigram}))
}
/**
* Request to draw a new problem.
* @param tid The tournament id
*/
function drawProblem(tid) {
sockets[tid].send(JSON.stringify({'type': 'draw_problem'}))
}
/**
* Accept the current proposed problem.
* @param tid The tournament id
*/
function acceptProblem(tid) {
sockets[tid].send(JSON.stringify({'type': 'accept'}))
}
/**
* Reject the current proposed problem.
* @param tid The tournament id
*/
function rejectProblem(tid) {
sockets[tid].send(JSON.stringify({'type': 'reject'}))
}
/**
* Volunteers can export the draw to make it available for notation.
* @param tid The tournament id
*/
function exportDraw(tid) {
sockets[tid].send(JSON.stringify({'type': 'export'}))
}
/**
* Volunteers can make the draw continue for the second round of the final.
* @param tid The tournament id
*/
function continueFinal(tid) {
sockets[tid].send(JSON.stringify({'type': 'continue_final'}))
}
/**
* Display a new notification with the given title and the given body.
* @param title The title of the notification
* @param body The body of the notification
* @param timeout The time (in milliseconds) after that the notification automatically closes. 0 to make indefinite. Default to 5000 ms.
* @return Notification
*/
function showNotification(title, body, timeout = 5000) {
let notif = new Notification(title, {'body': body, 'icon': "/static/tfjm.svg"})
if (timeout)
setTimeout(() => notif.close(), timeout)
return notif
}
document.addEventListener('DOMContentLoaded', () => {
if (document.location.hash) {
// Open the tab of the tournament that is present in the hash
document.querySelectorAll('button[data-bs-toggle="tab"]').forEach(elem => {
if ('#' + elem.innerText.toLowerCase() === document.location.hash.toLowerCase()) {
elem.click()
}
})
}
// When a tab is opened, add the tournament name in the hash
document.querySelectorAll('button[data-bs-toggle="tab"]').forEach(
elem => elem.addEventListener(
'click', () => document.location.hash = '#' + elem.innerText.toLowerCase()))
for (let tournament of tournaments) {
// Open a websocket per tournament
let socket = new WebSocket(
(document.location.protocol === 'https:' ? 'wss' : 'ws') + '://' + window.location.host
+ '/ws/draw/' + tournament.id + '/'
)
sockets[tournament.id] = socket
/**
* Add alert message on the top on the interface.
* @param message The content of the alert.
* @param type The alert type, which is a bootstrap color (success, info, warning, danger,…).
* @param timeout The time (in milliseconds) before the alert is auto-closing. 0 to infinitely, default to 5000 ms.
*/
function addMessage(message, type, timeout = 5000) {
const wrapper = document.createElement('div')
wrapper.innerHTML = [
`<div class="alert alert-${type} alert-dismissible" role="alert">`,
`<div>${message}</div>`,
'<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>',
].join('\n')
messages.append(wrapper)
if (timeout)
setTimeout(() => wrapper.remove(), timeout)
}
/**
* Update the information banner.
* @param info The content to updated
*/
function setInfo(info) {
document.getElementById(`messages-${tournament.id}`).innerHTML = info
}
/**
* Open the draw interface, given the list of teams.
* @param teams The list of teams (represented by their trigrams) that are present on this draw.
*/
function drawStart(teams) {
// Hide the not-started-banner
document.getElementById(`banner-not-started-${tournament.id}`).classList.add('d-none')
// Display the full draw interface
document.getElementById(`draw-content-${tournament.id}`).classList.remove('d-none')
let dicesDiv = document.getElementById(`dices-${tournament.id}`)
for (let team of teams) {
// Add empty dice score badge for each team
let col = document.createElement('div')
col.classList.add('col-md-1')
dicesDiv.append(col)
let diceDiv = document.createElement('div')
diceDiv.id = `dice-${tournament.id}-${team}`
diceDiv.classList.add('badge', 'rounded-pill', 'text-bg-warning')
if (document.getElementById(`abort-${tournament.id}`) !== null) {
// Check if this is a volunteer, who can launch a dice for a specific team
diceDiv.onclick = (e) => drawDice(tournament.id, team)
}
diceDiv.textContent = `${team} 🎲 ??`
col.append(diceDiv)
}
}
/**
* Abort the current draw, and make all invisible, except the not-started-banner.
*/
function drawAbort() {
document.getElementById(`banner-not-started-${tournament.id}`).classList.remove('d-none')
document.getElementById(`draw-content-${tournament.id}`).classList.add('d-none')
document.getElementById(`dices-${tournament.id}`).innerHTML = ""
document.getElementById(`recap-${tournament.id}-round-list`).innerHTML = ""
document.getElementById(`tables-${tournament.id}`).innerHTML = ""
updateDiceVisibility(false)
updateBoxVisibility(false)
updateButtonsVisibility(false)
updateExportVisibility(false)
updateContinueVisibility(false)
}
/**
* This function is triggered after a new dice result. We update the score of the team.
* Can be resetted to empty values if the result is null.
* @param trigram The trigram of the team that launched its dice
* @param result The result of the dice. null if it is a reset.
*/
function updateDiceInfo(trigram, result) {
let elem = document.getElementById(`dice-${tournament.id}-${trigram}`)
if (result === null) {
elem.classList.remove('text-bg-success')
elem.classList.add('text-bg-warning')
elem.innerText = `${trigram} 🎲 ??`
}
else {
elem.classList.remove('text-bg-warning')
elem.classList.add('text-bg-success')
elem.innerText = `${trigram} 🎲 ${result}`
}
}
/**
* Display or hide the dice button.
* @param visible The visibility status
*/
function updateDiceVisibility(visible) {
let div = document.getElementById(`launch-dice-${tournament.id}`)
if (visible)
div.classList.remove('d-none')
else
div.classList.add('d-none')
}
/**
* Display or hide the box button.
* @param visible The visibility status
*/
function updateBoxVisibility(visible) {
let div = document.getElementById(`draw-problem-${tournament.id}`)
if (visible)
div.classList.remove('d-none')
else
div.classList.add('d-none')
}
/**
* Display or hide the accept and reject buttons.
* @param visible The visibility status
*/
function updateButtonsVisibility(visible) {
let div = document.getElementById(`buttons-${tournament.id}`)
if (visible)
div.classList.remove('d-none')
else
div.classList.add('d-none')
}
/**
* Display or hide the export button.
* @param visible The visibility status
*/
function updateExportVisibility(visible) {
let div = document.getElementById(`export-${tournament.id}`)
if (visible)
div.classList.remove('d-none')
else
div.classList.add('d-none')
}
/**
* Display or hide the continuation button.
* @param visible The visibility status
*/
function updateContinueVisibility(visible) {
let div = document.getElementById(`continue-${tournament.id}`)
if (visible)
div.classList.remove('d-none')
else
div.classList.add('d-none')
}
/**
* Set the different pools for the given round, and update the interface.
* @param round The round number, as integer (1 or 2)
* @param poules The list of poules, which are represented with their letters and trigrams,
* [{'letter': 'A', 'teams': ['ABC', 'DEF', 'GHI']}]
*/
function updatePoules(round, poules) {
let roundList = document.getElementById(`recap-${tournament.id}-round-list`)
let poolListId = `recap-${tournament.id}-round-${round}-pool-list`
let poolList = document.getElementById(poolListId)
if (poolList === null) {
// Add a div for the round in the recap div
let div = document.createElement('div')
div.id = `recap-${tournament.id}-round-${round}`
div.classList.add('col-md-6', 'px-3', 'py-3')
div.setAttribute('data-tournament', tournament.id)
let title = document.createElement('strong')
title.textContent = 'Tour ' + round
poolList = document.createElement('ul')
poolList.id = poolListId
poolList.classList.add('list-group', 'list-group-flush')
div.append(title, poolList)
roundList.append(div)
}
let c = 1
for (let poule of poules) {
let teamListId = `recap-${tournament.id}-round-${round}-pool-${poule.letter}-team-list`
let teamList = document.getElementById(teamListId)
if (teamList === null) {
// Add a div for the pool in the recap div
let li = document.createElement('li')
li.id = `recap-${tournament.id}-round-${round}-pool-${poule.letter}`
li.classList.add('list-group-item', 'px-3', 'py-3')
li.setAttribute('data-tournament', tournament.id)
let title = document.createElement('strong')
title.textContent = 'Poule ' + poule.letter + round
teamList = document.createElement('ul')
teamList.id = teamListId
teamList.classList.add('list-group', 'list-group-flush')
li.append(title, teamList)
poolList.append(li)
}
if (poule.teams.length > 0) {
// The pool is initialized
for (let team of poule.teams) {
// Reorder dices
let diceDiv = document.getElementById(`dice-${tournament.id}-${team}`)
diceDiv.parentElement.style.order = c.toString()
c += 1
let teamLiId = `recap-${tournament.id}-round-${round}-team-${team}`
let teamLi = document.getElementById(teamLiId)
if (teamLi === null) {
// Add a line for the team in the recap
teamLi = document.createElement('li')
teamLi.id = teamLiId
teamLi.classList.add('list-group-item')
teamLi.setAttribute('data-tournament', tournament.id)
teamList.append(teamLi)
}
// Add the accepted problem div (empty for now)
let acceptedDivId = `recap-${tournament.id}-round-${round}-team-${team}-accepted`
let acceptedDiv = document.getElementById(acceptedDivId)
if (acceptedDiv === null) {
acceptedDiv = document.createElement('div')
acceptedDiv.id = acceptedDivId
acceptedDiv.classList.add('badge', 'rounded-pill', 'text-bg-warning')
acceptedDiv.textContent = `${team} 📃 ?`
teamLi.append(acceptedDiv)
}
// Add the rejected problems div (empty for now)
let rejectedDivId = `recap-${tournament.id}-round-${round}-team-${team}-rejected`
let rejectedDiv = document.getElementById(rejectedDivId)
if (rejectedDiv === null) {
rejectedDiv = document.createElement('div')
rejectedDiv.id = rejectedDivId
rejectedDiv.classList.add('badge', 'rounded-pill', 'text-bg-danger')
rejectedDiv.textContent = '🗑️'
teamLi.append(rejectedDiv)
}
}
}
// Draw tables
let tablesDiv = document.getElementById(`tables-${tournament.id}`)
let tablesRoundDiv = document.getElementById(`tables-${tournament.id}-round-${round}`)
if (tablesRoundDiv === null) {
// Add the tables div for the current round if necessary
let card = document.createElement('div')
card.classList.add('card', 'col-md-6')
tablesDiv.append(card)
let cardHeader = document.createElement('div')
cardHeader.classList.add('card-header')
cardHeader.innerHTML = `<h2>Tour ${round}</h2>`
card.append(cardHeader)
tablesRoundDiv = document.createElement('div')
tablesRoundDiv.id = `tables-${tournament.id}-round-${round}`
tablesRoundDiv.classList.add('card-body', 'd-flex', 'flex-wrap')
card.append(tablesRoundDiv)
}
for (let poule of poules) {
if (poule.teams.length === 0)
continue
// Display the table for the pool
updatePouleTable(round, poule)
}
}
}
/**
* Update the table for the given round and the given pool, where there will be the chosen problems.
* @param round The round number, as integer (1 or 2)
* @param poule The current pool, which id represented with its letter and trigrams,
* {'letter': 'A', 'teams': ['ABC', 'DEF', 'GHI']}
*/
function updatePouleTable(round, poule) {
let tablesRoundDiv = document.getElementById(`tables-${tournament.id}-round-${round}`)
let pouleTable = document.getElementById(`table-${tournament.id}-${round}-${poule.letter}`)
if (pouleTable === null) {
// Create table
let card = document.createElement('div')
card.classList.add('card', 'w-100', 'my-3', `order-${poule.letter.charCodeAt(0) - 64}`)
tablesRoundDiv.append(card)
let cardHeader = document.createElement('div')
cardHeader.classList.add('card-header')
cardHeader.innerHTML = `<h2>Poule ${poule.letter}${round}</h2>`
card.append(cardHeader)
let cardBody = document.createElement('div')
cardBody.classList.add('card-body')
card.append(cardBody)
pouleTable = document.createElement('table')
pouleTable.id = `table-${tournament.id}-${round}-${poule.letter}`
pouleTable.classList.add('table', 'table-stripped')
cardBody.append(pouleTable)
let thead = document.createElement('thead')
pouleTable.append(thead)
let phaseTr = document.createElement('tr')
thead.append(phaseTr)
let teamTh = document.createElement('th')
teamTh.classList.add('text-center')
teamTh.rowSpan = poule.teams.length === 5 ? 3 : 2
teamTh.textContent = "Équipe"
phaseTr.append(teamTh)
// Add columns
for (let i = 1; i <= (poule.teams.length === 4 ? 4 : 3); ++i) {
let phaseTh = document.createElement('th')
phaseTh.classList.add('text-center')
if (poule.teams.length === 5 && i < 3)
phaseTh.colSpan = 2
phaseTh.textContent = `Phase ${i}`
phaseTr.append(phaseTh)
}
if (poule.teams.length === 5) {
let roomTr = document.createElement('tr')
thead.append(roomTr)
for (let i = 0; i < 5; ++i) {
let roomTh = document.createElement('th')
roomTh.classList.add('text-center')
roomTh.textContent = `Salle ${1 + (i % 2)}`
roomTr.append(roomTh)
}
}
let problemTr = document.createElement('tr')
thead.append(problemTr)
for (let team of poule.teams) {
let problemTh = document.createElement('th')
problemTh.classList.add('text-center')
// Problem is unknown for now
problemTh.innerHTML = `Pb. <span id="table-${tournament.id}-round-${round}-problem-${team}">?</span>`
problemTr.append(problemTh)
}
// Add body
let tbody = document.createElement('tbody')
pouleTable.append(tbody)
for (let i = 0; i < poule.teams.length; ++i) {
let team = poule.teams[i]
let teamTr = document.createElement('tr')
tbody.append(teamTr)
// First create cells, then we will add them in the table
let teamTd = document.createElement('td')
teamTd.classList.add('text-center')
teamTd.innerText = team
teamTr.append(teamTd)
let defenderTd = document.createElement('td')
defenderTd.classList.add('text-center')
defenderTd.innerText = 'Déf'
let opponentTd = document.createElement('td')
opponentTd.classList.add('text-center')
opponentTd.innerText = 'Opp'
let reporterTd = document.createElement('td')
reporterTd.classList.add('text-center')
reporterTd.innerText = 'Rap'
// Put the cells in their right places, according to the pool size and the row number.
if (poule.teams.length === 3) {
switch (i) {
case 0:
teamTr.append(defenderTd, reporterTd, opponentTd)
break
case 1:
teamTr.append(opponentTd, defenderTd, reporterTd)
break
case 2:
teamTr.append(reporterTd, opponentTd, defenderTd)
break
}
}
else if (poule.teams.length === 4) {
let emptyTd = document.createElement('td')
switch (i) {
case 0:
teamTr.append(defenderTd, emptyTd, reporterTd, opponentTd)
break
case 1:
teamTr.append(opponentTd, defenderTd, emptyTd, reporterTd)
break
case 2:
teamTr.append(reporterTd, opponentTd, defenderTd, emptyTd)
break
case 3:
teamTr.append(emptyTd, reporterTd, opponentTd, defenderTd)
break
}
}
else if (poule.teams.length === 5) {
let emptyTd = document.createElement('td')
let emptyTd2 = document.createElement('td')
switch (i) {
case 0:
teamTr.append(defenderTd, emptyTd, opponentTd, reporterTd, emptyTd2)
break
case 1:
teamTr.append(emptyTd, defenderTd, reporterTd, emptyTd2, opponentTd)
break
case 2:
teamTr.append(opponentTd, emptyTd, defenderTd, emptyTd2, reporterTd)
break
case 3:
teamTr.append(reporterTd, opponentTd, emptyTd, defenderTd, emptyTd2)
break
case 4:
teamTr.append(emptyTd, reporterTd, emptyTd2, opponentTd, defenderTd)
break
}
}
}
}
}
/**
* Highligh the team that is currently choosing its problem.
* @param round The current round number, as integer (1 or 2)
* @param pool The current pool letter (A, B, C or D) (null if non-relevant)
* @param team The current team trigram (null if non-relevant)
*/
function updateActiveRecap(round, pool, team) {
// Remove the previous highlights
document.querySelectorAll(`div.text-bg-secondary[data-tournament="${tournament.id}"]`)
.forEach(elem => elem.classList.remove('text-bg-secondary'))
document.querySelectorAll(`li.list-group-item-success[data-tournament="${tournament.id}"]`)
.forEach(elem => elem.classList.remove('list-group-item-success'))
document.querySelectorAll(`li.list-group-item-info[data-tournament="${tournament.id}"]`)
.forEach(elem => elem.classList.remove('list-group-item-info'))
// Highlight current round, if existing
let roundDiv = document.getElementById(`recap-${tournament.id}-round-${round}`)
if (roundDiv !== null)
roundDiv.classList.add('text-bg-secondary')
// Highlight current pool, if existing
let poolLi = document.getElementById(`recap-${tournament.id}-round-${round}-pool-${pool}`)
if (poolLi !== null)
poolLi.classList.add('list-group-item-success')
// Highlight current team, if existing
let teamLi = document.getElementById(`recap-${tournament.id}-round-${round}-team-${team}`)
if (teamLi !== null)
teamLi.classList.add('list-group-item-info')
}
/**
* Update the recap and the table when a team accepts a problem.
* @param round The current round, as integer (1 or 2)
* @param team The current team trigram
* @param problem The accepted problem, as integer
*/
function setProblemAccepted(round, team, problem) {
// Update recap
let recapDiv = document.getElementById(`recap-${tournament.id}-round-${round}-team-${team}-accepted`)
recapDiv.classList.remove('text-bg-warning')
recapDiv.classList.add('text-bg-success')
recapDiv.textContent = `${team} 📃 ${problem}`
// Update table
let tableSpan = document.getElementById(`table-${tournament.id}-round-${round}-problem-${team}`)
tableSpan.textContent = problem
}
/**
* Update the recap when a team rejects a problem.
* @param round The current round, as integer (1 or 2)
* @param team The current team trigram
* @param rejected The full list of rejected problems
*/
function setProblemRejected(round, team, rejected) {
// Update recap
let recapDiv = document.getElementById(`recap-${tournament.id}-round-${round}-team-${team}-rejected`)
recapDiv.textContent = `🗑️ ${rejected.join(', ')}`
if (rejected.length > problems_count - 5) {
// If more than P - 5 problems were rejected, add a penalty of 0.5 of the coefficient of the oral defender
let penaltyDiv = document.getElementById(`recap-${tournament.id}-round-${round}-team-${team}-penalty`)
if (penaltyDiv === null) {
penaltyDiv = document.createElement('div')
penaltyDiv.id = `recap-${tournament.id}-round-${round}-team-${team}-penalty`
penaltyDiv.classList.add('badge', 'rounded-pill', 'text-bg-info')
recapDiv.parentNode.append(penaltyDiv)
}
penaltyDiv.textContent = `${0.5 * (rejected.length - (problems_count - 5))}`
}
}
/**
* For a 5-teams pool, we may reorder the pool if two teams select the same problem.
* Then, we redraw the table and set the accepted problems.
* @param round The current round, as integer (1 or 2)
* @param poule The pool represented by its letter
* @param teams The teams list represented by their trigrams, ["ABC", "DEF", "GHI", "JKL", "MNO"]
* @param problems The accepted problems in the same order than the teams, [1, 1, 2, 2, 3]
*/
function reorderPoule(round, poule, teams, problems) {
// Redraw the pool table
let table = document.getElementById(`table-${tournament.id}-${round}-${poule}`)
table.parentElement.parentElement.remove()
updatePouleTable(round, {'letter': poule, 'teams': teams})
// Put the problems in the table
for (let i = 0; i < teams.length; ++i) {
let team = teams[i]
let problem = problems[i]
setProblemAccepted(round, team, problem)
}
}
// Listen on websockets and process messages from the server
socket.addEventListener('message', e => {
// Parse received data as JSON
const data = JSON.parse(e.data)
switch (data.type) {
case 'alert':
// Add alert message
addMessage(data.message, data.alert_type)
break
case 'notification':
// Add notification
showNotification(data.title, data.body)
break
case 'set_info':
// Update information banner
setInfo(data.information)
break
case 'draw_start':
// Start the draw and update the interface
drawStart(data.trigrams)
break
case 'abort':
// Abort the current draw
drawAbort()
break
case 'dice':
// Update the interface after a dice launch
updateDiceInfo(data.team, data.result)
break
case 'dice_visibility':
// Update the dice button visibility
updateDiceVisibility(data.visible)
break
case 'box_visibility':
// Update the box button visibility
updateBoxVisibility(data.visible)
break
case 'buttons_visibility':
// Update the accept/reject buttons visibility
updateButtonsVisibility(data.visible)
break
case 'export_visibility':
// Update the export button visibility
updateExportVisibility(data.visible)
break
case 'continue_visibility':
// Update the continue button visibility for the final tournament
updateContinueVisibility(data.visible)
break
case 'set_poules':
// Set teams order and pools and update the interface
updatePoules(data.round, data.poules)
break
case 'set_active':
// Highlight the team that is selecting a problem
updateActiveRecap(data.round, data.poule, data.team)
break
case 'set_problem':
// Mark a problem as accepted and update the interface
setProblemAccepted(data.round, data.team, data.problem)
break
case 'reject_problem':
// Mark a problem as rejected and update the interface
setProblemRejected(data.round, data.team, data.rejected)
break
case 'reorder_poule':
// Reorder a pool and redraw the associated table
reorderPoule(data.round, data.poule, data.teams, data.problems)
break
}
})
// Manage errors
socket.addEventListener('close', e => {
console.error('Chat socket closed unexpectedly')
})
// When the socket is opened, set the language in order to receive alerts in the good language
socket.addEventListener('open', e => {
socket.send(JSON.stringify({
'type': 'set_language',
'language': document.getElementsByName('language')[0].value,
}))
})
// Manage the start form
let format_form = document.getElementById('format-form-' + tournament.id)
if (format_form !== null) {
format_form.addEventListener('submit', function (e) {
e.preventDefault()
socket.send(JSON.stringify({
'type': 'start_draw',
'fmt': document.getElementById('format-' + tournament.id).value
}))
})
}
}
})