moth

Monarch Of The Hill game server
git clone https://git.woozle.org/neale/moth.git

moth / theme
Neale Pickett  ·  2024-04-17

index.mjs

  1/**
  2 * Functionality for index.html (Login / Puzzles list)
  3 */
  4import * as moth from "./moth.mjs"
  5import * as common from "./common.mjs"
  6
  7class App {
  8    constructor(basePath=".") {        
  9        this.config = {}
 10        
 11        this.server = new moth.Server(basePath)
 12
 13        for (let form of document.querySelectorAll("form.login")) {
 14            form.addEventListener("submit", event => this.handleLoginSubmit(event))
 15        }
 16        for (let e of document.querySelectorAll(".logout")) {
 17            e.addEventListener("click", () => this.Logout())
 18        }
 19
 20        common.StateUpdateChannel.addEventListener("message", () => {
 21            // Give mothd time to catch up
 22            setTimeout(() => this.UpdateState(), 1/2 * common.Second)
 23        })
 24
 25        setInterval(() => this.UpdateState(), common.Minute/3)
 26        setInterval(() => this.UpdateConfig(), common.Minute* 5)
 27
 28        this.UpdateConfig()
 29        .finally(() => this.UpdateState())
 30    }
 31
 32    handleLoginSubmit(event) {
 33        event.preventDefault()
 34        let f = new FormData(event.target)
 35        this.Login(f.get("id"), f.get("name"))
 36    }
 37    
 38    /**
 39     * Attempt to log in to the server.
 40     * 
 41     * @param {string} teamID 
 42     * @param {string} teamName 
 43     */
 44    async Login(teamID, teamName) {
 45        try {
 46            await this.server.Login(teamID, teamName)
 47            common.Toast(`Logged in (team id = ${teamID})`)
 48            this.UpdateState()
 49        }
 50        catch (error) {
 51            common.Toast(error)
 52        }
 53    }
 54
 55    /**
 56     * Log out of the server by clearing the saved Team ID.
 57     */
 58    async Logout() {
 59        try {
 60            this.server.Reset()
 61            common.Toast("Logged out")
 62            this.UpdateState()
 63        }
 64        catch (error) {
 65            common.Toast(error)
 66        }
 67    }
 68
 69    /**
 70     * Update app configuration.
 71     *
 72     * Configuration can be updated less frequently than state, to reduce server
 73     * load, since configuration should (hopefully) change less frequently.
 74     */
 75    async UpdateConfig() {
 76        this.config = await common.Config()
 77
 78        for (let e of document.querySelectorAll(".messages")) {
 79            e.innerHTML = this.config.Messages || ""
 80        }
 81    }
 82
 83    /**
 84     * Update the entire page.
 85     *
 86     * Fetch a new state, and rebuild all dynamic elements on this bage based on
 87     * what's returned. If we're in development mode and not logged in, auto
 88     * login too.
 89     */
 90    async UpdateState() {
 91        this.state = await this.server.GetState()
 92
 93        // Update elements with data-track-solved
 94        for (let e of document.querySelectorAll("[data-track-solved]")) {
 95            // Only hide if data-track-solved is different than config.PuzzleList.TrackSolved
 96            let tracking = this.config.PuzzleList?.TrackSolved || false
 97            let displayIf = common.StringTruthy(e.dataset.trackSolved)
 98            e.classList.toggle("hidden", tracking != displayIf)
 99        }
100
101        for (let e of document.querySelectorAll(".login")) {
102            this.renderLogin(e, !this.server.LoggedIn())
103        }
104        for (let e of document.querySelectorAll(".puzzles")) {
105            this.renderPuzzles(e, this.server.LoggedIn())
106        }
107
108        if (this.state.DevelopmentMode() && !this.server.LoggedIn()) {
109            let teamID = Math.floor(Math.random() * 1000000).toString(16)
110            common.Toast("Automatically logging in to devel server")
111            console.info(`Logging in with generated Team ID: ${teamID}`)
112            return this.Login(teamID, `Team ${teamID}`)
113        }
114    }
115
116    /**
117     * Render a login box.
118     * 
119     * Just toggles visibility, there's nothing dynamic in a login box.
120     */
121    renderLogin(element, visible) {
122        element.classList.toggle("hidden", !visible)
123    }
124
125    /**
126     * Render a puzzles box.
127     *
128     * Displays the list of open puzzles, and adds mothball download links
129     * if the server is in development mode.
130     */
131    renderPuzzles(element, visible) {
132        element.classList.toggle("hidden", !visible)
133        while (element.firstChild) element.firstChild.remove()
134        for (let cat of this.state.Categories()) {
135            let pdiv = element.appendChild(document.createElement("div"))
136            pdiv.classList.add("category")
137            
138            let h = pdiv.appendChild(document.createElement("h2"))
139            h.textContent = cat
140            
141            // Extras if we're running a devel server
142            if (this.state.DevelopmentMode()) {
143                let a = h.appendChild(document.createElement('a'))
144                a.classList.add("mothball")
145                a.textContent = "⬇️"
146                a.href = this.server.URL(`mothballer/${cat}.mb`)
147                a.title = "Download a compiled puzzle for this category"
148            }
149            
150            // List out puzzles in this category
151            let l = pdiv.appendChild(document.createElement("ul"))
152            for (let puzzle of this.state.Puzzles(cat)) {
153                let i = l.appendChild(document.createElement("li"))
154
155                let url = new URL("puzzle.html", common.BaseURL)
156                url.hash = `${puzzle.Category}:${puzzle.Points}`
157                let a = i.appendChild(document.createElement("a"))
158                a.textContent = puzzle.Points
159                a.href = url
160                a.target = "_blank"
161
162                if (this.config.PuzzleList?.TrackSolved) {
163                    a.classList.toggle("solved", this.state.IsSolved(puzzle))
164                }
165                if (this.config.PuzzleList?.Titles) {
166                    this.loadTitle(puzzle, i)
167                }
168            }
169
170            if (!this.state.ContainsUnsolved(cat)) {
171                l.appendChild(document.createElement("li")).textContent = "✿"
172            }
173            
174            element.appendChild(pdiv)        
175        }
176    }
177
178    /**
179     * Asynchronously loads a puzzle, in order to populate the title.
180     * 
181     * Calling this for every open puzzle will generate a lot of load on the server.
182     * If we decide we want this for a multi-participant server,
183     * we should implement some sort of cache.
184     * 
185     * @param {Puzzle} puzzle 
186     * @param {Element} element 
187     */
188    async loadTitle(puzzle, element) {
189        await puzzle.Populate()
190        let title = puzzle.Extra.title
191        if (!title) {
192            return
193        }
194        element.classList.add("entitled")
195        for (let a of element.querySelectorAll("a")) {
196            a.textContent += `: ${title}`
197        }
198    }
199}
200
201function init() {
202    window.app = new App()
203}
204
205common.WhenDOMLoaded(init)