From e1a32ecd0143cad5acb8cbb1a2d964a4462da6e0 Mon Sep 17 00:00:00 2001 From: Jakob Gegeniger Date: Sun, 10 May 2026 00:10:09 +0200 Subject: [PATCH] step: Working MQTT websocket connection, docker deploy script --- deploy.sh | 3 + dockerfile | 13 ++ package-lock.json | 522 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 3 + src/main.ts | 214 ++++++++++++++++--- src/style.css | 158 +++++++++++++- 6 files changed, 877 insertions(+), 36 deletions(-) create mode 100644 deploy.sh create mode 100644 dockerfile diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..6a2a4a3 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,3 @@ +docker build -t kingArthurApp:latest . +docker save kingArthurApp:latest | gzip > kingArthurApp.tar.gz +scp kingArthurApp.tar.gz youruser@your_server_ip:~/ \ No newline at end of file diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..30eed49 --- /dev/null +++ b/dockerfile @@ -0,0 +1,13 @@ +# Build stage +FROM node:20-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +RUN npm run build + +# Serve stage +FROM nginx:alpine +COPY --from=builder /app/dist /usr/share/nginx/html +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e48c85d..57a91c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,11 +7,23 @@ "": { "name": "king-arthur", "version": "0.0.0", + "dependencies": { + "mqtt": "^5.15.1" + }, "devDependencies": { "typescript": "~6.0.2", "vite": "^8.0.10" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -368,6 +380,171 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/node": { + "version": "25.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz", + "integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/readable-stream": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.23.tgz", + "integrity": "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bl": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz", + "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/broker-factory": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/broker-factory/-/broker-factory-3.1.14.tgz", + "integrity": "sha512-L45k5HMbPIrMid0nTOZ/UPXG/c0aRuQKVrSDFIb1zOkvfiyHgYmIjc3cSiN1KwQIvRDOtKE0tfb3I9EZ3CmpQQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1", + "worker-factory": "^7.0.49" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/commist": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz", + "integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -378,6 +555,37 @@ "node": ">=8" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-unique-numbers": { + "version": "9.0.27", + "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-9.0.27.tgz", + "integrity": "sha512-nDA9ADeINN8SA2u2wCtU+siWFTTDqQR37XvgPIDDmboWQeExz7X0mImxuaN+kJddliIqy2FpVRmnvRZ+j8i1/A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.2.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -411,6 +619,57 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "license": "MIT" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/js-sdsl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", + "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -684,6 +943,70 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mqtt": { + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.15.1.tgz", + "integrity": "sha512-V1WnkGuJh3ec9QXzy5Iylw8OOBK+Xu1WhxcQ9mMpLThG+/JZIMV1PgLNRgIiqXhZnvnVLsuyxHl5A/3bHHbcAA==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.21", + "@types/ws": "^8.18.1", + "commist": "^3.2.0", + "concat-stream": "^2.0.0", + "debug": "^4.4.1", + "help-me": "^5.0.0", + "lru-cache": "^10.4.3", + "minimist": "^1.2.8", + "mqtt-packet": "^9.0.2", + "number-allocator": "^1.0.14", + "readable-stream": "^4.7.0", + "rfdc": "^1.4.1", + "socks": "^2.8.6", + "split2": "^4.2.0", + "worker-timers": "^8.0.23", + "ws": "^8.18.3" + }, + "bin": { + "mqtt": "build/bin/mqtt.js", + "mqtt_pub": "build/bin/pub.js", + "mqtt_sub": "build/bin/sub.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/mqtt-packet": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz", + "integrity": "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==", + "license": "MIT", + "dependencies": { + "bl": "^6.0.8", + "debug": "^4.3.4", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.12", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", @@ -703,6 +1026,16 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/number-allocator": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz", + "integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.1", + "js-sdsl": "4.3.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -752,6 +1085,43 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, "node_modules/rolldown": { "version": "1.0.0-rc.18", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.18.tgz", @@ -786,6 +1156,50 @@ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.18" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -796,6 +1210,24 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -817,9 +1249,13 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true + "license": "0BSD" + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" }, "node_modules/typescript": { "version": "6.0.3", @@ -835,6 +1271,18 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/vite": { "version": "8.0.11", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.11.tgz", @@ -912,6 +1360,74 @@ "optional": true } } + }, + "node_modules/worker-factory": { + "version": "7.0.49", + "resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.49.tgz", + "integrity": "sha512-lW7tpgy6aUv2dFsQhv1yv+XFzdkCf/leoKRTGMPVK5/die6RrUjqgJHJf556qO+ZfytNG6wPXc17E8zzsOLUDw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1" + } + }, + "node_modules/worker-timers": { + "version": "8.0.31", + "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-8.0.31.tgz", + "integrity": "sha512-ngkq5S6JuZyztom8tDgBzorLo9byhBMko/sXfgiUD945AuzKGg1GCgDMCC3NaYkicLpGKXutONM36wEX8UbBCA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "tslib": "^2.8.1", + "worker-timers-broker": "^8.0.16", + "worker-timers-worker": "^9.0.14" + } + }, + "node_modules/worker-timers-broker": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-8.0.16.tgz", + "integrity": "sha512-JyP3AvUGyPGbBGW7XiUewm2+0pN/aYo1QpVf5kdXAfkDZcN3p7NbWrG6XnyDEpDIvfHk/+LCnOW/NsuiU9riYA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "broker-factory": "^3.1.14", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1", + "worker-timers-worker": "^9.0.14" + } + }, + "node_modules/worker-timers-worker": { + "version": "9.0.14", + "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-9.0.14.tgz", + "integrity": "sha512-/qF06C60sXmSLfUl7WglvrDIbspmPOM8UrG63Dnn4bi2x4/DfqHS/+dxF5B+MdHnYO5tVuZYLHdAodrKdabTIg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "tslib": "^2.8.1", + "worker-factory": "^7.0.49" + } + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 910d1b9..3ae9182 100644 --- a/package.json +++ b/package.json @@ -11,5 +11,8 @@ "devDependencies": { "typescript": "~6.0.2", "vite": "^8.0.10" + }, + "dependencies": { + "mqtt": "^5.15.1" } } diff --git a/src/main.ts b/src/main.ts index 73382a0..dae180c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,9 +1,19 @@ +import mqtt from 'mqtt' import './style.css' -type Page = 'players' | 'start' | 'info' +type Page = 'home' | 'players' | 'start' | 'text' +type Route = 'home' | 'maintenance' | 'players' | 'start' | 'text' const storageKey = 'players' -let players: string[] = loadPlayers() +const mqttBrokerHost = '91.99.29.8' +const mqttBrokerPort = 9001 +const mqttBrokerPath = '/mqtt' +const mqttUseTls = false +const mqttTopic = 'king-arthur/page/startup' +const mqttMessage = 'start-hit' + +const initialPlayers = loadPlayers() +let players: string[] = [] let currentName = '' function loadPlayers(): string[] { @@ -26,40 +36,125 @@ function savePlayers(): void { localStorage.setItem(storageKey, JSON.stringify(players)) } -function getPageFromHash(): Page { - const hash = window.location.hash.replace('#', '') - if (hash === 'start' || hash === 'info' || hash === 'players') { - return hash - } - return 'players' +function publishStartHit(): void { + const protocol = mqttUseTls ? 'wss' : 'ws' + const brokerUrl = `${protocol}://${mqttBrokerHost}:${mqttBrokerPort}${mqttBrokerPath}` + const client = mqtt.connect(brokerUrl) + + client.on('connect', () => { + client.publish(mqttTopic, mqttMessage, { qos: 1 }, () => { + client.end() + }) + }) + + client.on('error', (error) => { + console.error('MQTT publish failed:', error) + client.end(true) + }) +} + +function getRouteFromPath(): Route { + const path = window.location.pathname + + if (path === '/') return 'home' + if (path === '/maintenance') return 'maintenance' + if (path === '/players') return 'players' + if (path === '/start') return 'start' + if (path === '/text') return 'text' + if (path.startsWith('/players')) return 'players' + if (path.startsWith('/start')) return 'start' + if (path.startsWith('/text')) return 'text' + + return 'home' +} + +function navigateTo(path: string): void { + window.history.pushState({}, '', path) + render() } function render(): void { const app = document.querySelector('#app') if (!app) return - const page = getPageFromHash() + const route = getRouteFromPath() + const path = window.location.pathname - app.innerHTML = ` -
-

Simple Player Web App

- -
- ${renderPage(page)} -
-
- ` + if (route === 'home') { + app.innerHTML = ` +
+
+ ${renderHomePage()} +
+
+ ` + } else if (path === '/maintenance') { + const maintenancePage: Page = 'players' + app.innerHTML = ` +
+

Simple Player Web App

+ +
+ ${renderPage(maintenancePage)} +
+
+ ` + attachNavigationHandlers(maintenancePage) + } else { + const page = route as Page + app.innerHTML = ` +
+
+ ${renderPage(page)} +
+
+ ` + attachHandlers(page) + } +} - attachHandlers(page) +function renderHomePage(): string { + return ` +
+

Welcome to King Arthur

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

+
+
+

Version

+

1.0.0

+
+
+

Service Hotline

+

+1 800-123-4567

+
+
+ +
+ ` } function renderPage(page: Page): string { + if (page === 'players') { - const listItems = players.map((name) => `
  • ${name}
  • `).join('') + const listItems = players.map((name, index) => ` +
  • + ${name} + +
  • + `).join('') return `

    Add players

    @@ -68,6 +163,10 @@ function renderPage(page: Page): string { +
    + + +
    ` } @@ -87,10 +186,43 @@ function renderPage(page: Page): string { ` } +function attachNavigationHandlers(page: Page): void { + const navLinks = document.querySelectorAll('.nav-link') + navLinks.forEach((link) => { + link.addEventListener('click', (e) => { + e.preventDefault() + const href = link.getAttribute('href') + if (href) { + navigateTo(href) + updateActiveNavLink(page) + } + }) + }) + attachHandlers(page) +} + +function updateActiveNavLink(page: Page): void { + const navLinks = document.querySelectorAll('.nav-link') + navLinks.forEach((link) => { + link.classList.remove('active') + const href = link.getAttribute('href') + if ( + (page === 'players' && href === '/players') || + (page === 'start' && href === '/start') || + (page === 'text' && href === '/text') + ) { + link.classList.add('active') + } + }) +} + function attachHandlers(page: Page): void { if (page === 'players') { const input = document.querySelector('#player-input') const button = document.querySelector('#add-player') + const deleteButtons = document.querySelectorAll('.delete-btn') + const clearAllBtn = document.querySelector('#clear-all-btn') + const finishedBtn = document.querySelector('#finished-btn') if (input && button) { const addPlayer = () => { @@ -110,6 +242,31 @@ function attachHandlers(page: Page): void { } }) } + + deleteButtons.forEach((btn) => { + btn.addEventListener('click', () => { + const index = parseInt(btn.getAttribute('data-index') || '0', 10) + players.splice(index, 1) + savePlayers() + render() + }) + }) + + if (clearAllBtn) { + clearAllBtn.addEventListener('click', () => { + if (confirm('Are you sure you want to delete all players?')) { + players = [] + savePlayers() + render() + } + }) + } + + if (finishedBtn) { + finishedBtn.addEventListener('click', () => { + window.location.href = '/start' + }) + } } if (page === 'start') { @@ -118,11 +275,12 @@ function attachHandlers(page: Page): void { if (button && selectedName) { button.addEventListener('click', () => { + publishStartHit() if (players.length === 0) { currentName = 'Please add players on page 1 first.' } else { const randomIndex = Math.floor(Math.random() * players.length) - currentName = players[randomIndex] + currentName = (players[randomIndex]) } selectedName.textContent = currentName }) @@ -130,10 +288,10 @@ function attachHandlers(page: Page): void { } } -window.addEventListener('hashchange', render) +window.addEventListener('popstate', render) -if (!window.location.hash) { - window.location.hash = '#players' +if (window.location.pathname === '/' || !window.location.pathname.startsWith('/')) { + window.history.replaceState({}, '', '/') } render() diff --git a/src/style.css b/src/style.css index cf104b3..c54ff74 100644 --- a/src/style.css +++ b/src/style.css @@ -93,13 +93,161 @@ button { cursor: pointer; } -.player-list { - margin: 0; - padding-left: 20px; -} - #selected-name { margin-top: 12px; font-size: 18px; font-weight: 600; } + +.home-content { + text-align: center; +} + +.home-content h1 { + margin: 0 0 16px; + font-size: 32px; +} + +.home-content p { + margin: 0 0 24px; + line-height: 1.6; + color: #4b5563; +} + +.info-section { + display: flex; + gap: 16px; + margin: 32px 0; + justify-content: center; +} + +.info-box { + flex: 1; + max-width: 200px; + border: 1px solid #e5e7eb; + border-radius: 8px; + padding: 16px; + background: #f9fafb; +} + +.info-box h3 { + margin: 0 0 8px; + font-size: 14px; + color: #6b7280; + text-transform: uppercase; +} + +.info-box p { + margin: 0; + font-size: 18px; + font-weight: 600; + color: #111827; +} + +.home-nav { + display: flex; + flex-wrap: wrap; + gap: 12px; + justify-content: center; + margin-top: 32px; +} + +.nav-button { + text-decoration: none; + color: #ffffff; + background: #111827; + border: 1px solid #111827; + border-radius: 8px; + padding: 12px 24px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: background 0.2s, border-color 0.2s; +} + +.nav-button:hover { + background: #374151; + border-color: #374151; +} + +.player-list { + margin: 0; + padding: 0; + list-style: none; +} + +.player-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px; + margin: 8px 0; + background: #f9fafb; + border: 1px solid #e5e7eb; + border-radius: 6px; +} + +.player-name { + flex: 1; + font-weight: 500; + color: #111827; +} + +.delete-btn { + background: #ef4444; + border: 1px solid #ef4444; + color: #ffffff; + padding: 6px 10px; + border-radius: 4px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s, border-color 0.2s; +} + +.delete-btn:hover { + background: #dc2626; + border-color: #dc2626; +} + +.trash-icon { + width: 16px; + height: 16px; +} + +.players-actions { + display: flex; + gap: 12px; + margin-top: 16px; + justify-content: flex-end; +} + +.btn-primary, +.btn-secondary { + padding: 10px 16px; + border-radius: 6px; + border: none; + cursor: pointer; + font-weight: 500; + transition: background 0.2s; +} + +.btn-primary { + background: #111827; + color: #ffffff; +} + +.btn-primary:hover { + background: #1f2937; +} + +.btn-secondary { + background: #e5e7eb; + color: #111827; + border: 1px solid #d1d5db; +} + +.btn-secondary:hover { + background: #d1d5db; +}