diff options
author | Tor Andersson <tor@ccxvii.net> | 2024-07-23 13:48:12 +0200 |
---|---|---|
committer | Tor Andersson <tor@ccxvii.net> | 2024-07-23 14:01:19 +0200 |
commit | 34487c1b1920c6a52108a7ed7b390d4b08728ff9 (patch) | |
tree | 112890b6ca8a60868c25e6b0ccca80e6d2ee8800 /public | |
parent | c52ca4b709a8752d870a9123b46cfd3764c608a7 (diff) | |
download | server-34487c1b1920c6a52108a7ed7b390d4b08728ff9.tar.gz |
New join page!
* Remove explicit (and redundant) role=Observer parameter.
* Remove old /play/:id redirects.
* Show "private" badge on game boxes.
* Forbid leave/kick in public games.
* Allow "rewind" by owner in public games.
Diffstat (limited to 'public')
-rw-r--r-- | public/join.js | 603 | ||||
-rw-r--r-- | public/style.css | 3 |
2 files changed, 369 insertions, 237 deletions
diff --git a/public/join.js b/public/join.js index f3279ad..f852b95 100644 --- a/public/join.js +++ b/public/join.js @@ -1,66 +1,232 @@ "use strict" -let start_status = game.status +/* global game, roles, players, blacklist, user_id */ + +const pace_text = [ + "", + "Live!", + "Fast \u2013 many moves per day", + "Slow \u2013 one move per day", +] + +let start_status = 0 let evtsrc = null -let timer = 0 let invite_role = null +function is_game_ready() { + if (game.player_count !== players.length) + return false + for (let p of players) + if (p.is_invite) + return false + return true +} + +function is_blacklist(p) { + return blacklist && blacklist.includes(p.user_id) +} + +function has_already_joined() { + for (let p of players) + if (p.user_id === user_id) + return true + return false +} + +function has_other_players() { + for (let p of players) + if (p.user_id !== user_id) + return true + return false +} + +function may_join() { + if (game.is_match || game.status > 1) + return false + if (has_already_joined()) { + if (user_id !== game.owner_id) + return false + if (has_other_players()) + return false + } + return true +} + +function may_part() { + if (game.is_match || game.status > 1) + return false + if (game.status > 0) { + if (!game.is_private) + return false + } + return true +} + +function may_kick() { + if (game.owner_id !== user_id) + return false + return may_part() +} + +function may_start() { + if (game.owner_id !== user_id || game.is_match || game.status !== 0) + return false + if (!is_game_ready()) + return false + return true +} + +function may_delete() { + if (game.owner_id !== user_id || game.is_match || game.status >= 2) + return false + if (game.status > 0 && game.user_count > 0) + return false + return true +} + +function may_rewind() { + if (game.owner_id !== user_id || game.is_match || game.status !== 1) + return false + if (!game.is_private) + return false + return true +} + +function option_to_english(k) { + if (k === true || k === 1) + return "yes" + if (k === false) + return "no" + if (typeof k === "string") + return k.replace(/_/g, " ").replace(/^\w/, (c) => c.toUpperCase()) + return k +} + +function format_options(options) { + if (options === "{}") + return "" + return Object.entries(JSON.parse(options)) + .map(([ k, v ]) => { + if (k === "players") + return v + " Player" + if (v === true || v === 1) + return option_to_english(k) + return option_to_english(k) + "=" + option_to_english(v) + }) + .join(", ") +} + +function format_time_left(time) { + if (time <= 0) + return "no time left" + if (time <= 2 / 24) + return Math.floor(time * 24 * 60) + " minutes left" + if (time <= 2) + return Math.floor(time * 24) + " hours left" + return Math.floor(time) + " days left" +} + +function epoch_from_julianday(x) { + return (x - 2440587.5) * 86400000 +} + +function julianday_from_epoch(x) { + return x / 86400000 + 2440587.5 +} + +function human_date(date) { + if (typeof date === "string") + date = julianday_from_epoch(Date.parse(date + "Z")) + if (typeof date !== "number") + return "never" + var days = julianday_from_epoch(Date.now()) - date + var seconds = days * 86400 + if (days < 1) { + if (seconds < 60) return "now" + if (seconds < 120) return "1 minute ago" + if (seconds < 3600) return Math.floor(seconds / 60) + " minutes ago" + if (seconds < 7200) return "1 hour ago" + if (seconds < 86400) return Math.floor(seconds / 3600) + " hours ago" + } + if (days < 2) return "yesterday" + if (days < 14) return Math.floor(days) + " days ago" + if (days < 31) return Math.floor(days / 7) + " weeks ago" + return new Date(epoch_from_julianday(date)).toISOString().substring(0,10) +} + function confirm_delete() { - let warning = `Are you sure you want to DELETE this game?` + let warning = "Are you sure you want to DELETE this game?" + if (window.confirm(warning)) + post("/api/delete/" + game.game_id) +} + +function confirm_rewind() { + let warning = "Are you sure you want to REWIND this game to the last move?\n\nMake sure you have the consent of all the players." if (window.confirm(warning)) - window.location.href = "/delete/" + game.game_id + post("/api/rewind/" + game.game_id) } -function post(url) { - fetch(url, { method: "POST" }) - .then(r => r.text()) - .then(t => window.error.textContent = (t === "SUCCESS") ? "" : t) - .catch(e => window.error.textContent = e) +async function post(url) { + window.error.textContent = "" + let res = await fetch(url, { method: "POST" }) + if (!res.ok) { + window.error.textContent = res.status + " " + res.statusText + return + } + let text = await res.text() + if (text !== "SUCCESS") { + window.error.textContent = text + return + } start_event_source() } function start() { - post(`/start/${game.game_id}`) + post(`/api/start/${game.game_id}`) } function join(role) { - post(`/join/${game.game_id}/${encodeURIComponent(role)}`) + post(`/api/join/${game.game_id}/${encodeURIComponent(role)}`) +} + +function accept(role) { + post(`/api/accept/${game.game_id}/${encodeURIComponent(role)}`) +} + +function decline(role) { + post(`/api/part/${game.game_id}/${encodeURIComponent(role)}`) } function part(role) { let warning = "Are you sure you want to LEAVE this game?" if (game.status === 0 || window.confirm(warning)) - post(`/part/${game.game_id}/${encodeURIComponent(role)}`) + post(`/api/part/${game.game_id}/${encodeURIComponent(role)}`) } function kick(role) { let player = players.find(p => p.role === role) let warning = `Are you sure you want to KICK player ${player.name} (${role}) from this game?` if (game.status === 0 || window.confirm(warning)) - post(`/part/${game.game_id}/${encodeURIComponent(role)}`) + post(`/api/part/${game.game_id}/${encodeURIComponent(role)}`) } -function accept(role) { - post(`/accept/${game.game_id}/${encodeURIComponent(role)}`) +function invite(role) { + invite_role = role + document.getElementById("invite").showModal() +} + +function hide_invite() { + document.getElementById("invite").close() } function send_invite() { let invite_user = document.getElementById("invite_user").value if (invite_user) { - post(`/invite/${game.game_id}/${encodeURIComponent(invite_role)}/${encodeURIComponent(invite_user)}`) document.getElementById("invite").close() + post(`/api/invite/${game.game_id}/${encodeURIComponent(invite_role)}/${encodeURIComponent(invite_user)}`) } } -function show_invite(role) { - invite_role = role - document.getElementById("invite").showModal() -} - -function hide_invite() { - document.getElementById("invite").close() -} - let blink_title = document.title let blink_timer = 0 @@ -86,261 +252,225 @@ function stop_blinker() { window.addEventListener("focus", stop_blinker) function start_event_source() { + if (!game) + return if (!evtsrc || evtsrc.readyState === 2) { console.log("STARTING EVENT SOURCE") evtsrc = new EventSource("/join-events/" + game.game_id) - evtsrc.addEventListener("players", function (evt) { - console.log("PLAYERS:", evt.data) - players = JSON.parse(evt.data) - update() - }) - evtsrc.addEventListener("ready", function (evt) { - console.log("READY:", evt.data) - ready = JSON.parse(evt.data) - update() + evtsrc.addEventListener("hello", function (evt) { + console.log("HELLO", evt.data) + window.disconnected.textContent = "" }) - evtsrc.addEventListener("game", function (evt) { - console.log("GAME:", evt.data) - game = JSON.parse(evt.data) - if (game.status > 1) { - console.log("CLOSED EVENT SOURCE") - clearInterval(timer) - evtsrc.close() - } + evtsrc.addEventListener("updated", function (evt) { + console.log("UPDATED", evt.data) + let data = JSON.parse(evt.data) + game = data.game + roles = data.roles + players = data.players update() }) evtsrc.addEventListener("deleted", function (evt) { - console.log("DELETED") - window.location.href = '/' + game.title_id + console.log("DELETED", evt.data) + game = null + roles = null + players = null + update() + evtsrc.close() }) - evtsrc.onerror = function (err) { - window.message.innerHTML = "Disconnected from server..." + evtsrc.onerror = function (evt) { + console.log("ERROR", evt) + window.disconnected.textContent = "Disconnected from server..." } - window.addEventListener('beforeunload', function (evt) { + window.addEventListener('beforeunload', function (_evt) { evtsrc.close() }) } } -function is_friend(p) { - return whitelist && whitelist.includes(p.user_id) +function user_link(user_name) { + return `<a class="black" href="/user/${user_name}">${user_name}</a>` } -function is_enemy(p) { - return blacklist && blacklist.includes(p.user_id) +function player_link(player) { + let link = user_link(player.name) + if (player.is_invite) + link = "<i>" + link + "</i> ?" + if (player.user_id === user_id) + link = "\xbb " + link + return link } -function user_link(player) { - return `<a class="black" href="/user/${player.name}">${player.name}</a>` +function play_link(parent, player) { + let e = document.createElement("a") + e.setAttribute("href", `/${game.title_id}/play.html?game=${game.game_id}&role=${encodeURIComponent(player.role)}`) + e.textContent = "Play" + parent.appendChild(e) } -function play_link(player) { - return `\xbb <a href="/${game.title_id}/play.html?game=${game.game_id}&role=${encodeURIComponent(player.role)}">${player.name}</a>` +function action_link(parent, text, action, arg) { + let e = document.createElement("a") + e.setAttribute("href", `javascript:${action.name}('${arg}')`) + e.textContent = text + parent.appendChild(e) } -function action_link(player, action, color, text) { - return `<a class="${color}" href="javascript:${action}('${player.role}')">${text}</a>` +function create_element(parent, tag, classList) { + let e = document.createElement(tag) + if (classList) + e.classList = classList + parent.appendChild(e) + return e } -function update() { - update_common() - if (user_id) - update_login() - else - update_no_login() +function create_button(text, action) { + let e = create_element(window.game_actions, "button") + e.textContent = text + e.onclick = action +} + +function create_game_list_item(parent, key, val) { + if (val) { + let tr = create_element(parent, "tr") + let e_key = create_element(tr, "td") + let e_val = create_element(tr, "td") + e_key.innerHTML = key + e_val.innerHTML = val + } } -function update_common() { +function create_game_list() { + let table = create_element(window.game_info, "table") + let list = create_element(table, "tbody") + if (game.scenario !== "Standard") - document.querySelector("h1").textContent = "#" + game.game_id + " - " + game.title_name + " - " + game.scenario + create_game_list_item(list, "Scenario", game.scenario) + create_game_list_item(list, "Options", format_options(game.options)) + create_game_list_item(list, "Pace", pace_text[game.pace]) + create_game_list_item(list, "Notice", game.notice) + + if (game.owner_id) + create_game_list_item(list, "Created", human_date(game.ctime) + " by " + user_link(game.owner_name)) else - document.querySelector("h1").textContent = "#" + game.game_id + " - " + game.title_name + create_game_list_item(list, "Created", human_date(game.ctime)) - let message = window.message - if (game.status === 0) { - if (ready) - message.innerHTML = "Waiting to start..." - else - message.innerHTML = "Waiting for players to join..." - } else if (game.status === 1) { - message.innerHTML = `<a href="/${game.title_id}/play.html?game=${game.game_id}">Observe</a>` - } else if (game.status === 2) { - message.innerHTML = `<a href="/${game.title_id}/play.html?game=${game.game_id}">Review</a>` - } else if (game.status === 3) { - message.innerHTML = "Archived" + + create_game_list_item(list, "Moves", game.moves) + + if (game.status === 1) { + create_game_list_item(list, "Last move", human_date(game.mtime)) } -} -function update_no_login() { - for (let i = 0; i < roles.length; ++i) { - let role = roles[i] - let role_id = "role_" + role.replace(/ /g, "_") - if (game.is_random && game.status === 0) - role = "Random " + (i+1) - document.getElementById(role_id + "_name").textContent = role - let player = players.find(p => p.role === role) - let element = document.getElementById(role_id) - - if (game.is_match) { - if (player) { - if (game.status === 1) - element.classList.toggle("is_active", player.is_active) - element.innerHTML = user_link(player) - } else { - element.innerHTML = `<i>Empty</i>` - } - continue - } + if (game.status > 1) { + create_game_list_item(list, "Finished", human_date(game.mtime)) + create_game_list_item(list, "Result", game.result) + } +} - if (player) { - element.classList.remove("is_invite") - switch (game.status) { - case 3: - element.innerHTML = player.name - break - case 2: - element.innerHTML = user_link(player) - break - case 1: - if (player.is_invite) { - element.classList.add("is_invite") - element.innerHTML = user_link(player) + " ?" - } else { - element.classList.toggle("is_active", player.is_active) - element.innerHTML = user_link(player) - } - break - case 0: +function create_player_box(role, player) { + let box = create_element(window.game_players, "table") + let thead = create_element(box, "thead") + let tbody = create_element(box, "tbody") + let tr_role = create_element(thead, "tr") + let tr_player = create_element(tbody, "tr", "p") + let tr_actions = create_element(tbody, "tr", "a") + + let td_role_name = create_element(tr_role, "td") + let td_role_time = create_element(tr_role, "td", "r") + let td_player_name = create_element(tr_player, "td") + let td_player_seen = create_element(tr_player, "td", "r") + let td_actions = create_element(tr_actions, "td", "a r") + td_actions.setAttribute("colspan", 2) + td_actions.textContent = "\u200b" + + td_role_name.textContent = role + + if (player) { + if (player.is_active && game.status === 1) + box.classList = "active" + if (player.is_invite) + box.classList = "invite" + + if (game.status > 0 && (game.pace > 0 || player.time_left < 3)) + td_role_time.textContent = format_time_left(player.time_left) + + td_player_name.innerHTML = player_link(player) + if (player.user_id !== user_id && game.status <= 1) + td_player_seen.innerHTML = "<i>seen " + human_date(player.atime) + "</i>" + + if (user_id) { + if (is_blacklist(player)) + td_player_name.classList.add("blacklist") + + if (player.user_id === user_id) { if (player.is_invite) { - element.classList.add("is_invite") - element.innerHTML = user_link(player) + " ?" + action_link(td_actions, "Decline", decline, role) + action_link(td_actions, "Accept", accept, role) } else { - element.innerHTML = user_link(player) + if (may_part()) + action_link(td_actions, "Leave", part, role) + if (game.status === 1 || game.status === 2) + play_link(td_actions, player) } - break + } else { + if (may_kick()) + action_link(td_actions, "Kick", kick, role) + } + } + } else { + td_player_name.innerHTML = "<i>Empty</i>" + + if (user_id) { + if (!game.is_match) { + if (game.owner_id === user_id) + action_link(td_actions, "Invite", invite, role) + if (may_join()) + action_link(td_actions, "Join", join, role) } - } else { - element.classList.remove("is_invite") - element.innerHTML = `<i>Empty</i>` } } } -function update_login() { +function update() { + window.error.textContent = "" + window.game_info.replaceChildren() + window.game_players.replaceChildren() + window.game_actions.replaceChildren() + + if (!game) { + window.game_enter.textContent = "Game deleted!" + stop_blinker() + return + } + + if (game.status === 0) { + if (user_id) + window.game_enter.textContent = "Waiting for players to join." + else + window.game_enter.innerHTML = `<a href="/login">Login</a> or <a href="/signup">sign up</a> to join.` + } else if (game.status === 1) + window.game_enter.innerHTML = `<a href="/${game.title_id}/play.html?game=${game.game_id}">Watch</a>` + else if (game.status === 2) + window.game_enter.innerHTML = `<a href="/${game.title_id}/play.html?game=${game.game_id}">Review</a>` + else + window.game_enter.innerHTML = "Archived." + + create_game_list() + for (let i = 0; i < roles.length; ++i) { let role = roles[i] - let role_id = "role_" + role.replace(/ /g, "_") if (game.is_random && game.status === 0) role = "Random " + (i+1) - document.getElementById(role_id + "_name").textContent = role - let player = players.find(p => p.role === role) - let element = document.getElementById(role_id) - - if (game.is_match) { - if (player) { - if (game.status === 1) - element.classList.toggle("is_active", player.is_active) - if (player.user_id === user_id && (game.status === 1 || game.status === 2)) - element.innerHTML = play_link(player) - else - element.innerHTML = user_link(player) - } else { - element.innerHTML = `<i>Empty</i>` - } - continue - } - - if (player) { - element.classList.remove("is_invite") - switch (game.status) { - case 3: - element.innerHTML = player.name - break - case 2: - if (player.user_id === user_id) - element.innerHTML = play_link(player) - else - element.innerHTML = user_link(player) - break - case 1: - if (player.is_invite) { - element.classList.add("is_invite") - if (player.user_id === user_id) - element.innerHTML = player.name + " ?" + - action_link(player, "part", "red", "\u274c") + - action_link(player, "accept", "green", "\u2714") - else if (game.owner_id === user_id) - element.innerHTML = user_link(player) + " ?" + action_link(player, "kick", "red", "\u274c") - else - element.innerHTML = user_link(player) + " ?" - } else { - element.classList.toggle("is_active", player.is_active) - if (player.user_id === user_id) - element.innerHTML = play_link(player) + action_link(player, "part", "red", "\u274c") - else if (game.owner_id === user_id) - element.innerHTML = user_link(player) + action_link(player, "kick", "red", "\u274c") - else - element.innerHTML = user_link(player) - } - break - case 0: - if (player.is_invite) { - element.classList.add("is_invite") - if (player.user_id === user_id) - element.innerHTML = player.name + " ?" + - action_link(player, "part", "red", "\u274c") + - action_link(player, "accept", "green", "\u2714") - else if (game.owner_id === user_id) - element.innerHTML = user_link(player) + " ?" + action_link(player, "kick", "red", "\u274c") - else - element.innerHTML = user_link(player) + " ?" - } else { - if (player.user_id === user_id) - element.innerHTML = player.name + action_link(player, "part", "red", "\u274c") - else if (game.owner_id === user_id) - element.innerHTML = user_link(player) + action_link(player, "kick", "red", "\u274c") - else - element.innerHTML = user_link(player) - } - break - } - element.classList.toggle("friend", is_friend(player)) - element.classList.toggle("enemy", is_enemy(player)) - if (is_enemy(player)) - element.title = "You have blacklisted this user!" - else - element.title = "" - } else { - element.classList.remove("is_invite") - switch (game.status) { - case 2: - element.innerHTML = `<i>Empty</i>` - break - case 1: - case 0: - if (limit) - element.innerHTML = `<i>Empty</i>` - else if (game.owner_id === user_id) - element.innerHTML = `\xbb <a class="join" href="javascript:join('${role}')">Join</a><a class="green" href="javascript:show_invite('${role}')">\u{2795}</a>` - else - element.innerHTML = `\xbb <a class="join" href="javascript:join('${role}')">Join</a>` - break - } - element.classList.remove("friend") - element.classList.remove("enemy") - element.title = "" - } + create_player_box(role, players.find(p => p.role === role)) } - if (game.owner_id === user_id) { - window.start_button.disabled = !ready - window.start_button.classList = (game.status === 0) ? "" : "hide" - window.delete_button.classList = (game.status === 0 || game.user_count <= 1) ? "" : "hide" - if (game.status === 0 && ready) - start_blinker("READY TO START") - else - stop_blinker() - } else { + if (user_id) { + if (may_rewind()) + create_button("Rewind", confirm_rewind) + if (may_delete()) + create_button("Delete", confirm_delete) + if (may_start()) + create_button("Start", start) + if (start_status === 0 && game.status === 1) start_blinker("STARTED") else @@ -350,8 +480,9 @@ function update_login() { window.onload = function () { update() - if (user_id && game.status <= 1 && !game.is_match) { + if (user_id && game.status <= 1) { start_event_source() - timer = setInterval(start_event_source, 15000) + setInterval(start_event_source, 15000) + setInterval(update, 60000) } } diff --git a/public/style.css b/public/style.css index caa2feb..7928f86 100644 --- a/public/style.css +++ b/public/style.css @@ -21,7 +21,6 @@ html { --color-table-active: hsla(55, 100%, 50%, 0.15); --color-table-invite: hsla(120, 100%, 50%, 0.15); - --color-table-danger: hsla(350, 100%, 50%, 0.15); } /* light gray */ @@ -141,6 +140,8 @@ h1 { margin: 16px 0 16px -1px; font-size: 24px; } h2 { margin: 16px 0 16px -1px; font-size: 20px; } h3 { margin: 16px 0 8px -1px; font-size: 16px; } +h1 .icon { font-weight: normal; } + html { overflow-y: scroll; } |