From b1b35c7e175cfc11128d311f146697f7c97f1180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Snorre=20Magnus=20Dav=C3=B8en?= Date: Sun, 4 Aug 2024 19:21:08 +0200 Subject: [PATCH] fix: Sort functionality and admin dashboard sort order (#29) Some various fixes to make sort functionality more flexible. Sort is now handled by placing a 'sort' attribute on the element that should be sorted, e.g. the player list ul element on the scoreboard page. The sort attribute value is the attribute value on each child element used to sort, e.g. for the player list each li element contains an attribute score to use for sorting. Finally a sortFn attribute can be specified to say which function to use to compare sort values. In the case of stringly score numbers we need to parse the numbers before comparing as regular string comparisons won't work and attribute values have to be string. Various other small fixes to the logic to clean up the chart. HyperScript is used to update the player row li elements with new score-attribute values to avoid having to replace the entire row. The only other option would be handling this client side, or using something like the morphdom plugin to htmx to allow merging the server rendered (SSE) html data with the browser html data. Otherwise client side logic can break. --- bun.lockb | Bin 70829 -> 74979 bytes package.json | 11 ++- src/client/client.ts | 126 ++++++++++++++++------------ src/game/state.ts | 2 +- src/index.tsx | 7 +- src/layouts/main.tsx | 1 + src/pages/admin.tsx | 5 +- src/pages/scoreboard/scoreboard.tsx | 39 +++++---- 8 files changed, 115 insertions(+), 76 deletions(-) diff --git a/bun.lockb b/bun.lockb index 11c77d1d5af6655715e80f1c28fb11fea4655d68..d2cb4ba2e0ac667542b12f673954d0104e31a1ae 100755 GIT binary patch delta 15158 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 delta 12630 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 diff --git a/package.json b/package.json index 61e36d7..c565ed2 100644 --- a/package.json +++ b/package.json @@ -10,14 +10,14 @@ "build:tailwind": "tailwindcss -i ./src/tailwind.css -o ./public/tailwind.css", "build:server": "bun build --target=bun ./src/index.tsx --outfile=dist/index.js", "build:client": "bun build --target=bun ./src/client/client.ts --outdir ./public", - "build": "bun run --target=bun build:server && npm run build:tailwind", + "build": "bun run --target=bun build:server && bun run --target=bun build:tailwind && bun run --target=bun build:client", "format:check": "biome format ./src", "format": "biome format ./src --write", "lint": "biome lint ./src", "check": "biome check --write ./src", "typecheck": "tsc --noEmit", "benchmark": "bun run --target=bun tooling/benchmark.ts", - "simulate": "bun run --target=bun tooling/simulate.ts" + "simulate": "bun run build && bun run --target=bun tooling/simulate.ts" }, "dependencies": { "@elysiajs/static": "^1.0.3", @@ -29,7 +29,10 @@ "chartjs-adapter-date-fns": "^3.0.0", "elysia": "^1.0.24", "html-escaper": "^3.0.3", - "htmx.org": "^1.9.12", + "htmx-ext-response-targets": "^2.0.0", + "htmx-ext-sse": "^2.2.1", + "htmx.org": "2.0.1", + "hyperscript.org": "^0.9.12", "lit": "^3.1.4" }, "devDependencies": { @@ -38,7 +41,7 @@ "@total-typescript/shoehorn": "^0.1.2", "@types/bun": "^1.1.5", "@types/html-escaper": "^3.0.2", - "bun-types": "latest", + "bun-types": "1.1.21", "concurrently": "^8.2.2", "daisyui": "^4.12.8", "mitata": "^0.1.11", diff --git a/src/client/client.ts b/src/client/client.ts index 3d48ab9..7aea94f 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -5,49 +5,70 @@ import { type Point, registerables, } from "chart.js"; -import * as htmx from "htmx.org"; import "chartjs-adapter-date-fns"; import { CmcCounter } from "./counter"; import { startConfetti } from "./confetti"; -// First some type overrides -declare global { - interface Window { - htmx: typeof htmx; - } -} - // Then we register some chart plugins and web components Chart.register(...registerables); customElements.define("cmc-counter", CmcCounter); -// Then define some functions to sort the scoreboard and render the chart -function sortScoreboard() { - const list = document.getElementById("scoreboard-list"); - if (!list) { - return; - } +/** + * Define a type for holding a map of sort functions. + * Each function accepts two strings and returns a number. + * The key is the name of the function. + */ +type SortFunctions = { + [key: string]: (a: string, b: string) => number; +}; + +/** + * A map of sort function implementations. + * Allows us to sort elements on different criteria. + * For example, we can sort elements by score or by localeCompare + * if values are nicknames. If values are stringly numbers we + * can sort them by the numeric value. + */ +const sortFunctions: SortFunctions = { + score: (a, b) => Number.parseInt(b) - Number.parseInt(a), + localeCompare: (a, b) => a.localeCompare(b), +}; + +/** + * Sorts all elements with the attribute "sort". + * The value of the attribute specifies the value in each child + * to use for sorting. The optional attribute "sortFn" specifies + * the name of the function to use for sorting. + * + * Fallbacks to "localeCompare" if the function is not found. + * We simply remove the elements in the list and re-append them + * in the sorted order. + * + * Works with auto-animate to animate the sorting. + */ +function sortSortable() { + // First find all elements with attribute "sort" + const sortableElements = Array.from(document.querySelectorAll("[sort]")); + + // For each sortable element we sort the children by the attribute "sortkey" + for (const sortableElement of sortableElements) { + const sortAttr = sortableElement.getAttribute("sort") ?? ""; + const fnName = sortableElement.getAttribute("sortFn") ?? ""; + // biome-ignore lint/complexity/useLiteralKeys: + const sortFn = sortFunctions[fnName] ?? sortFunctions["localeCompare"]; + const items = Array.from(sortableElement.children); + + items.sort((a, b) => { + const sortValueA = a.getAttribute(sortAttr) ?? ""; + const sortValueB = b.getAttribute(sortAttr) ?? ""; + return sortFn(sortValueA, sortValueB); + }); - const items = Array.from(list.children); - - items.sort((a, b) => { - const scoreA = Number.parseInt( - a.querySelector("span")?.innerText ?? "0", - 10, - ); - const scoreB = Number.parseInt( - b.querySelector("span")?.innerText ?? "0", - 10, - ); - - const randomNumberBetweenMinus1And1 = Math.random() * 2 - 1; - return scoreB - (scoreA + randomNumberBetweenMinus1And1); // Sort descending order (highest score first) - }); - - // Clear the list and re-append sorted items - list.innerHTML = ""; - for (const item of items) { - list.appendChild(item); + // Clear the list and re-append sorted items + sortableElement.innerHTML = ""; + for (const item of items) { + sortableElement.appendChild(item); + } } } @@ -61,12 +82,12 @@ let chart: Chart<"line">; function cleanChart() { const now = new Date().getTime(); + const fiveMinutesAgo = now - 5 * 60 * 1000; for (const dataset of chart.data.datasets) { dataset.data = dataset.data.filter((point) => { const x = (point as Point).x; - console.log("X", now - x < 5 * 60 * 1000); - return now - x < 5 * 60 * 1000; + return x >= fiveMinutesAgo; }); } chart.update(); @@ -146,11 +167,12 @@ function renderChart(datasets: ChartConfiguration<"line">["data"]["datasets"]) { window.renderChart = renderChart; -htmx.onLoad(() => { +function htmxOnLoad() { // Find all elements with the auto-animate class and animate them for (const element of Array.from( document.querySelectorAll(".auto-animate"), )) { + console.info("Auto animating", element); autoAnimate(element as HTMLElement); } @@ -161,26 +183,18 @@ htmx.onLoad(() => { if (confettiCanvas) { startConfetti(confettiCanvas); } -}); +} -// On SSE messages do DOM-manipulation where necessary -htmx.on("htmx:sseMessage", (evt) => { - // If player score changes sort the scoreboard - if ( - evt instanceof CustomEvent && - evt.detail.type.startsWith("player-score-") - ) { - // Sort the scoreboard when a player's score changes - sortScoreboard(); - } +function htmxSSEMessage(evt: Event) { + // On SSE messages do DOM-manipulation where necessary + sortSortable(); // If the event is a player-score-chart event, update the chart with the new score data if ( evt instanceof CustomEvent && - evt.detail.type.startsWith("player-score-chart-") + evt.detail.type.startsWith("player-score-chart") ) { - const nick = evt.detail.type.replace("player-score-chart-", ""); - const data = JSON.parse(evt.detail.data); + const { nick, ...data } = JSON.parse(evt.detail.data); const dataset = chart.data.datasets.find((d) => d.label === nick); if (dataset) { @@ -205,4 +219,12 @@ htmx.on("htmx:sseMessage", (evt) => { chart.update(); } } -}); +} + +window.onload = () => { + console.info("Running client.ts window.onload setup function"); + document.body.addEventListener("htmx:load", htmxOnLoad); + document.body.addEventListener("htmx:sseMessage", htmxSSEMessage); + + htmxOnLoad(); +}; diff --git a/src/game/state.ts b/src/game/state.ts index e28f272..81c0cf5 100644 --- a/src/game/state.ts +++ b/src/game/state.ts @@ -138,7 +138,7 @@ const newWorker = (player: Player) => { const player = state.players.find((p) => p.uuid === uuid); if (player) { player.log.unshift(log); - player.score += log.score; + player.score = log.score; } // We need to notify all relevant listeners about the new log // Multiple listeners can be listening for the same player diff --git a/src/index.tsx b/src/index.tsx index 22501ac..7188bc9 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -24,10 +24,11 @@ const app = new Elysia({ Bun.file("node_modules/htmx.org/dist/htmx.min.js"), ) .get("/public/response-targets.js", () => - Bun.file("node_modules/htmx.org/dist/ext/response-targets.js"), + Bun.file("node_modules/htmx-ext-response-targets/response-targets.js"), ) - .get("/public/sse.js", () => - Bun.file("node_modules/htmx.org/dist/ext/sse.js"), + .get("/public/sse.js", () => Bun.file("node_modules/htmx-ext-sse/sse.js")) + .get("/public/hyperscript.js", () => + Bun.file("node_modules/hyperscript.org/dist/_hyperscript.min.js"), ) .use(adminPlugin) .use(homePlugin) diff --git a/src/layouts/main.tsx b/src/layouts/main.tsx index 1f9fca5..5339d83 100644 --- a/src/layouts/main.tsx +++ b/src/layouts/main.tsx @@ -13,6 +13,7 @@ export const HTMLLayout = ({ page, header, children }: LayoutProps) => {