2023-09-13 18:52:52 -06:00
|
|
|
/**
|
|
|
|
* Functionality for index.html (Login / Puzzles list)
|
|
|
|
*/
|
|
|
|
import * as moth from "./moth.mjs"
|
|
|
|
import * as common from "./common.mjs"
|
|
|
|
|
|
|
|
class App {
|
2023-09-14 17:42:02 -06:00
|
|
|
constructor(basePath=".") {
|
|
|
|
this.config = {}
|
|
|
|
|
2023-09-13 18:52:52 -06:00
|
|
|
this.server = new moth.Server(basePath)
|
|
|
|
|
|
|
|
for (let form of document.querySelectorAll("form.login")) {
|
|
|
|
form.addEventListener("submit", event => this.handleLoginSubmit(event))
|
|
|
|
}
|
|
|
|
for (let e of document.querySelectorAll(".logout")) {
|
|
|
|
e.addEventListener("click", () => this.Logout())
|
|
|
|
}
|
|
|
|
|
2023-09-14 17:42:02 -06:00
|
|
|
setInterval(() => this.UpdateState(), common.Minute/3)
|
|
|
|
setInterval(() => this.UpdateConfig(), common.Minute* 5)
|
|
|
|
|
|
|
|
this.UpdateConfig()
|
|
|
|
.finally(() => this.UpdateState())
|
2023-09-13 18:52:52 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
handleLoginSubmit(event) {
|
|
|
|
event.preventDefault()
|
2023-09-28 18:16:18 -06:00
|
|
|
let f = new FormData(event.target)
|
|
|
|
this.Login(f.get("id"), f.get("name"))
|
2023-09-13 18:52:52 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Attempt to log in to the server.
|
|
|
|
*
|
2023-09-14 19:08:44 -06:00
|
|
|
* @param {string} teamID
|
|
|
|
* @param {string} teamName
|
2023-09-13 18:52:52 -06:00
|
|
|
*/
|
2023-09-14 17:42:02 -06:00
|
|
|
async Login(teamID, teamName) {
|
2023-09-13 18:52:52 -06:00
|
|
|
try {
|
2023-09-14 17:42:02 -06:00
|
|
|
await this.server.Login(teamID, teamName)
|
|
|
|
common.Toast(`Logged in (team id = ${teamID})`)
|
|
|
|
this.UpdateState()
|
2023-09-13 18:52:52 -06:00
|
|
|
}
|
|
|
|
catch (error) {
|
|
|
|
common.Toast(error)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Log out of the server by clearing the saved Team ID.
|
|
|
|
*/
|
|
|
|
async Logout() {
|
|
|
|
try {
|
|
|
|
this.server.Reset()
|
|
|
|
common.Toast("Logged out")
|
2023-09-14 17:42:02 -06:00
|
|
|
this.UpdateState()
|
2023-09-13 18:52:52 -06:00
|
|
|
}
|
|
|
|
catch (error) {
|
|
|
|
common.Toast(error)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-14 17:42:02 -06:00
|
|
|
/**
|
|
|
|
* Update app configuration.
|
|
|
|
*
|
|
|
|
* Configuration can be updated less frequently than state, to reduce server
|
|
|
|
* load, since configuration should (hopefully) change less frequently.
|
|
|
|
*/
|
|
|
|
async UpdateConfig() {
|
2023-09-15 16:09:08 -06:00
|
|
|
this.config = await common.Config()
|
2023-09-19 16:48:24 -06:00
|
|
|
|
|
|
|
for (let e of document.querySelectorAll(".messages")) {
|
|
|
|
e.innerHTML = this.config.Messages || ""
|
|
|
|
}
|
2023-09-14 17:42:02 -06:00
|
|
|
}
|
|
|
|
|
2023-09-13 18:52:52 -06:00
|
|
|
/**
|
|
|
|
* Update the entire page.
|
|
|
|
*
|
|
|
|
* Fetch a new state, and rebuild all dynamic elements on this bage based on
|
|
|
|
* what's returned. If we're in development mode and not logged in, auto
|
|
|
|
* login too.
|
|
|
|
*/
|
2023-09-14 17:42:02 -06:00
|
|
|
async UpdateState() {
|
2023-09-13 18:52:52 -06:00
|
|
|
this.state = await this.server.GetState()
|
|
|
|
|
2023-09-14 17:42:02 -06:00
|
|
|
// Update elements with data-track-solved
|
|
|
|
for (let e of document.querySelectorAll("[data-track-solved]")) {
|
|
|
|
// Only display if data-track-solved is the same as config.trackSolved
|
|
|
|
e.classList.toggle("hidden", common.Truthy(e.dataset.trackSolved) != this.config.TrackSolved)
|
|
|
|
}
|
|
|
|
|
2023-09-13 18:52:52 -06:00
|
|
|
for (let e of document.querySelectorAll(".login")) {
|
|
|
|
this.renderLogin(e, !this.server.LoggedIn())
|
|
|
|
}
|
|
|
|
for (let e of document.querySelectorAll(".puzzles")) {
|
|
|
|
this.renderPuzzles(e, this.server.LoggedIn())
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.state.DevelopmentMode() && !this.server.LoggedIn()) {
|
2023-09-15 16:13:09 -06:00
|
|
|
let teamID = Math.floor(Math.random() * 1000000).toString(16)
|
2023-09-13 18:52:52 -06:00
|
|
|
common.Toast("Automatically logging in to devel server")
|
2023-09-15 16:13:09 -06:00
|
|
|
console.info(`Logging in with generated Team ID: ${teamID}`)
|
|
|
|
return this.Login(teamID, `Team ${teamID}`)
|
2023-09-13 18:52:52 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Render a login box.
|
|
|
|
*
|
2023-09-14 19:08:44 -06:00
|
|
|
* Just toggles visibility, there's nothing dynamic in a login box.
|
2023-09-13 18:52:52 -06:00
|
|
|
*/
|
|
|
|
renderLogin(element, visible) {
|
|
|
|
element.classList.toggle("hidden", !visible)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Render a puzzles box.
|
|
|
|
*
|
2023-09-14 19:08:44 -06:00
|
|
|
* Displays the list of open puzzles, and adds mothball download links
|
2023-09-13 18:52:52 -06:00
|
|
|
* if the server is in development mode.
|
|
|
|
*/
|
|
|
|
renderPuzzles(element, visible) {
|
|
|
|
element.classList.toggle("hidden", !visible)
|
|
|
|
while (element.firstChild) element.firstChild.remove()
|
|
|
|
for (let cat of this.state.Categories()) {
|
|
|
|
let pdiv = element.appendChild(document.createElement("div"))
|
|
|
|
pdiv.classList.add("category")
|
|
|
|
|
|
|
|
let h = pdiv.appendChild(document.createElement("h2"))
|
|
|
|
h.textContent = cat
|
|
|
|
|
|
|
|
// Extras if we're running a devel server
|
|
|
|
if (this.state.DevelopmentMode()) {
|
|
|
|
let a = h.appendChild(document.createElement('a'))
|
|
|
|
a.classList.add("mothball")
|
2023-09-19 16:48:24 -06:00
|
|
|
a.textContent = "⬇️"
|
2023-09-13 18:52:52 -06:00
|
|
|
a.href = this.server.URL(`mothballer/${cat}.mb`)
|
|
|
|
a.title = "Download a compiled puzzle for this category"
|
|
|
|
}
|
|
|
|
|
|
|
|
// List out puzzles in this category
|
|
|
|
let l = pdiv.appendChild(document.createElement("ul"))
|
|
|
|
for (let puzzle of this.state.Puzzles(cat)) {
|
|
|
|
let i = l.appendChild(document.createElement("li"))
|
|
|
|
|
2023-09-15 16:09:08 -06:00
|
|
|
let url = new URL("puzzle.html", common.BaseURL)
|
2023-09-13 18:52:52 -06:00
|
|
|
url.hash = `${puzzle.Category}:${puzzle.Points}`
|
|
|
|
let a = i.appendChild(document.createElement("a"))
|
|
|
|
a.textContent = puzzle.Points
|
|
|
|
a.href = url
|
|
|
|
a.target = "_blank"
|
2023-09-14 17:42:02 -06:00
|
|
|
|
|
|
|
if (this.config.TrackSolved) {
|
|
|
|
a.classList.toggle("solved", this.state.IsSolved(puzzle))
|
|
|
|
}
|
2024-01-08 18:14:28 -07:00
|
|
|
if (this.config.Titles) {
|
|
|
|
this.loadTitle(puzzle, i)
|
|
|
|
}
|
2023-09-13 18:52:52 -06:00
|
|
|
}
|
|
|
|
|
2023-09-14 17:42:02 -06:00
|
|
|
if (!this.state.ContainsUnsolved(cat)) {
|
2023-09-13 18:52:52 -06:00
|
|
|
l.appendChild(document.createElement("li")).textContent = "✿"
|
|
|
|
}
|
|
|
|
|
|
|
|
element.appendChild(pdiv)
|
|
|
|
}
|
|
|
|
}
|
2024-01-08 18:14:28 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Asynchronously loads a puzzle, in order to populate the title.
|
|
|
|
*
|
|
|
|
* Calling this for every open puzzle will generate a lot of load on the server.
|
|
|
|
* If we decide we want this for a multi-participant server,
|
|
|
|
* we should implement some sort of cache.
|
|
|
|
*
|
|
|
|
* @param {Puzzle} puzzle
|
|
|
|
* @param {Element} element
|
|
|
|
*/
|
|
|
|
async loadTitle(puzzle, element) {
|
|
|
|
await puzzle.Populate()
|
|
|
|
let title = puzzle.Extra.title
|
|
|
|
if (!title) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
element.classList.add("entitled")
|
|
|
|
for (let a of element.querySelectorAll("a")) {
|
|
|
|
a.textContent += `: ${title}`
|
|
|
|
}
|
|
|
|
}
|
2023-09-13 18:52:52 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
function init() {
|
2023-09-29 15:37:18 -06:00
|
|
|
window.app = new App()
|
2023-09-13 18:52:52 -06:00
|
|
|
}
|
|
|
|
|
2023-09-14 19:08:44 -06:00
|
|
|
common.WhenDOMLoaded(init)
|