step: migrate to vue.js

This commit is contained in:
2026-06-27 14:16:26 +02:00
parent e1a32ecd01
commit 1da5c4fefc
17 changed files with 865 additions and 415 deletions
+9
View File
@@ -0,0 +1,9 @@
<template>
<router-view />
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>
+7
View File
@@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
+8 -294
View File
@@ -1,297 +1,11 @@
import mqtt from 'mqtt'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import { router } from './router'
import './style.css'
type Page = 'home' | 'players' | 'start' | 'text'
type Route = 'home' | 'maintenance' | 'players' | 'start' | 'text'
const app = createApp(App)
const storageKey = 'players'
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[] {
const saved = localStorage.getItem(storageKey)
if (!saved) return []
try {
const parsed = JSON.parse(saved)
if (Array.isArray(parsed)) {
return parsed.filter((item): item is string => typeof item === 'string' && item.trim().length > 0)
}
} catch {
return []
}
return []
}
function savePlayers(): void {
localStorage.setItem(storageKey, JSON.stringify(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<HTMLDivElement>('#app')
if (!app) return
const route = getRouteFromPath()
const path = window.location.pathname
if (route === 'home') {
app.innerHTML = `
<main class="app">
<section class="page-content">
${renderHomePage()}
</section>
</main>
`
} else if (path === '/maintenance') {
const maintenancePage: Page = 'players'
app.innerHTML = `
<main class="app">
<h1>Simple Player Web App</h1>
<nav class="nav">
<a href="/players" class="nav-link active">Page 1: Players</a>
<a href="/start" class="nav-link">Page 2: Start</a>
<a href="/text" class="nav-link">Page 3: Text</a>
</nav>
<section class="page-content">
${renderPage(maintenancePage)}
</section>
</main>
`
attachNavigationHandlers(maintenancePage)
} else {
const page = route as Page
app.innerHTML = `
<main class="app">
<section class="page-content">
${renderPage(page)}
</section>
</main>
`
attachHandlers(page)
}
}
function renderHomePage(): string {
return `
<div class="home-content">
<h1>Welcome to King Arthur</h1>
<p>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.</p>
<div class="info-section">
<div class="info-box">
<h3>Version</h3>
<p id="version-number">1.0.0</p>
</div>
<div class="info-box">
<h3>Service Hotline</h3>
<p id="service-hotline">+1 800-123-4567</p>
</div>
</div>
<nav class="home-nav">
<a href="/players" class="nav-button">Players</a>
<a href="/start" class="nav-button">Start</a>
<a href="/text" class="nav-button">Text</a>
<a href="/maintenance" class="nav-button">Maintenance</a>
</nav>
</div>
`
}
function renderPage(page: Page): string {
if (page === 'players') {
const listItems = players.map((name, index) => `
<li class="player-item">
<span class="player-name">${name}</span>
<button class="delete-btn" data-index="${index}" title="Delete player">
<svg class="trash-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-9l-1 1H5v2h14V4z" fill="currentColor"/>
</svg>
</button>
</li>
`).join('')
return `
<h2>Add players</h2>
<div class="row">
<input id="player-input" type="text" placeholder="Enter player name" />
<button id="add-player" type="button">Add</button>
</div>
<ul class="player-list">${listItems}</ul>
<div class="players-actions">
<button id="clear-all-btn" type="button" class="btn-secondary">Clear All</button>
<button id="finished-btn" type="button" class="btn-primary">Finished</button>
</div>
`
}
if (page === 'start') {
const shownName = currentName || 'No name selected yet'
return `
<h2>Start</h2>
<button id="start-btn" type="button">Start</button>
<p id="selected-name">${shownName}</p>
`
}
return `
<h2>Some text</h2>
<p>This is the third page with static text.</p>
`
}
function attachNavigationHandlers(page: Page): void {
const navLinks = document.querySelectorAll<HTMLAnchorElement>('.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<HTMLAnchorElement>('.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<HTMLInputElement>('#player-input')
const button = document.querySelector<HTMLButtonElement>('#add-player')
const deleteButtons = document.querySelectorAll<HTMLButtonElement>('.delete-btn')
const clearAllBtn = document.querySelector<HTMLButtonElement>('#clear-all-btn')
const finishedBtn = document.querySelector<HTMLButtonElement>('#finished-btn')
if (input && button) {
const addPlayer = () => {
const value = input.value.trim()
if (!value) return
players.push(value)
savePlayers()
input.value = ''
render()
}
button.addEventListener('click', addPlayer)
input.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
addPlayer()
}
})
}
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') {
const button = document.querySelector<HTMLButtonElement>('#start-btn')
const selectedName = document.querySelector<HTMLParagraphElement>('#selected-name')
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])
}
selectedName.textContent = currentName
})
}
}
}
window.addEventListener('popstate', render)
if (window.location.pathname === '/' || !window.location.pathname.startsWith('/')) {
window.history.replaceState({}, '', '/')
}
render()
app.use(createPinia())
app.use(router)
app.mount('#app')
+57
View File
@@ -0,0 +1,57 @@
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import Maintenance from '../views/Maintenance.vue'
import Players from '../views/Players.vue'
import Start from '../views/Start.vue'
import Text from '../views/Text.vue'
const routes: any[] = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/players',
name: 'PlayersDirect',
component: Players
},
{
path: '/start',
name: 'StartDirect',
component: Start
},
{
path: '/text',
name: 'TextDirect',
component: Text
},
{
path: '/maintenance',
name: 'Maintenance',
component: Maintenance,
redirect: '/maintenance/players',
children: [
{
path: 'players',
name: 'PlayersInMaintenance',
component: Players
},
{
path: 'start',
name: 'StartInMaintenance',
component: Start
},
{
path: 'text',
name: 'TextInMaintenance',
component: Text
}
]
}
]
export const router = createRouter({
history: createWebHistory(),
routes
})
+31
View File
@@ -0,0 +1,31 @@
import mqtt from 'mqtt'
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'
let client: any = null
export function getMqttClient(): any {
return client
}
export function publishStartHit(): void {
const protocol = mqttUseTls ? 'wss' : 'ws'
const brokerUrl = `${protocol}://${mqttBrokerHost}:${mqttBrokerPort}${mqttBrokerPath}`
const tempClient = mqtt.connect(brokerUrl)
tempClient.on('connect', () => {
tempClient.publish(mqttTopic, mqttMessage, { qos: 1 }, () => {
tempClient.end()
})
})
tempClient.on('error', (error: any) => {
console.error('MQTT publish failed:', error)
tempClient.end(true)
})
}
+70
View File
@@ -0,0 +1,70 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const usePlayersStore = defineStore('players', () => {
const players = ref<string[]>([])
const currentName = ref<string>('')
const playersCount = computed(() => players.value.length)
function loadPlayers(): void {
const saved = localStorage.getItem('players')
if (!saved) return
try {
const parsed = JSON.parse(saved)
if (Array.isArray(parsed)) {
players.value = parsed.filter((item): item is string => typeof item === 'string' && item.trim().length > 0)
}
} catch {
players.value = []
}
}
function savePlayers(): void {
localStorage.setItem('players', JSON.stringify(players.value))
}
function addPlayer(name: string): void {
const trimmed = name.trim()
if (!trimmed) return
players.value.push(trimmed)
savePlayers()
}
function deletePlayer(index: number): void {
players.value.splice(index, 1)
savePlayers()
}
function clearAllPlayers(): void {
players.value = []
savePlayers()
}
function selectRandomPlayer(): void {
if (players.value.length === 0) {
currentName.value = 'Please add players on page 1 first.'
} else {
const randomIndex = Math.floor(Math.random() * players.value.length)
currentName.value = players.value[randomIndex]
}
}
function setCurrentName(name: string): void {
currentName.value = name
}
loadPlayers()
return {
players,
currentName,
playersCount,
addPlayer,
deletePlayer,
clearAllPlayers,
selectRandomPlayer,
setCurrentName
}
})
+24
View File
@@ -0,0 +1,24 @@
<template>
<main class="app">
<nav class="nav">
<router-link to="/" class="nav-link">Home</router-link>
<router-link to="/players" class="nav-link" :class="{ active: route.path === '/players' }">Page 1: Players</router-link>
<router-link to="/start" class="nav-link" :class="{ active: route.path === '/start' }">Page 2: Start</router-link>
<router-link to="/text" class="nav-link" :class="{ active: route.path === '/text' }">Page 3: Text</router-link>
</nav>
<section class="page-content">
<Players v-if="route.path === '/players'" />
<Start v-else-if="route.path === '/start'" />
<Text v-else-if="route.path === '/text'" />
</section>
</main>
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router'
import Players from './Players.vue'
import Start from './Start.vue'
import Text from './Text.vue'
const route = useRoute()
</script>
+29
View File
@@ -0,0 +1,29 @@
<template>
<main class="app">
<section class="page-content">
<div class="home-content">
<h1>King Arthur Outdoor Adventure</h1>
<p>Outdoor adventure to find the chosen one and join him on a quest.</p>
<div class="info-section">
<div class="info-box">
<h3>Version</h3>
<p id="version-number">1.0.0</p>
</div>
<div class="info-box">
<h3>Service Hotline</h3>
<p id="service-hotline">+1 800-123-4567</p>
</div>
</div>
<nav class="home-nav">
<router-link to="/players" class="nav-button">Players</router-link>
<router-link to="/start" class="nav-button">Start</router-link>
<router-link to="/text" class="nav-button">Text</router-link>
<router-link to="/maintenance" class="nav-button">Maintenance</router-link>
</nav>
</div>
</section>
</main>
</template>
<script setup lang="ts">
</script>
+23
View File
@@ -0,0 +1,23 @@
<template>
<main class="app">
<h1>Simple Player Web App</h1>
<nav class="nav">
<router-link to="/maintenance/players" class="nav-link" :class="{ active: isActive('/maintenance/players') }">Page 1: Players</router-link>
<router-link to="/maintenance/start" class="nav-link" :class="{ active: isActive('/maintenance/start') }">Page 2: Start</router-link>
<router-link to="/maintenance/text" class="nav-link" :class="{ active: isActive('/maintenance/text') }">Page 3: Text</router-link>
</nav>
<section class="page-content">
<router-view></router-view>
</section>
</main>
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router'
const route = useRoute()
function isActive(path: string): boolean {
return route.path === path
}
</script>
+55
View File
@@ -0,0 +1,55 @@
<template>
<div>
<h2>Add players</h2>
<div class="row">
<input
v-model="playerInput"
type="text"
placeholder="Enter player name"
@keydown.enter="addPlayer"
/>
<button @click="addPlayer" type="button">Add</button>
</div>
<ul class="player-list">
<li v-for="(player, index) in playersStore.players" :key="index" class="player-item">
<span class="player-name">{{ player }}</span>
<button class="delete-btn" @click="deletePlayer(index)" title="Delete player">
<svg class="trash-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-9l-1 1H5v2h14V4z" fill="currentColor"/>
</svg>
</button>
</li>
</ul>
<div class="players-actions">
<button @click="clearAll" type="button" class="btn-secondary">Clear All</button>
<button @click="finished" type="button" class="btn-primary">Finished</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { usePlayersStore } from '../stores/players'
const playerInput = ref<string>('')
const playersStore = usePlayersStore()
function addPlayer(): void {
playersStore.addPlayer(playerInput.value)
playerInput.value = ''
}
function deletePlayer(index: number): void {
playersStore.deletePlayer(index)
}
function clearAll(): void {
if (confirm('Are you sure you want to delete all players?')) {
playersStore.clearAllPlayers()
}
}
function finished(): void {
router.push('/start')
}
</script>
+19
View File
@@ -0,0 +1,19 @@
<template>
<div>
<h2>Start</h2>
<button @click="startGame" id="start-btn" type="button">Start</button>
<p id="selected-name">{{ playersStore.currentName || 'No name selected yet' }}</p>
</div>
</template>
<script setup lang="ts">
import { usePlayersStore } from '../stores/players'
import { publishStartHit } from '../services/mqtt'
const playersStore = usePlayersStore()
function startGame(): void {
publishStartHit()
playersStore.selectRandomPlayer()
}
</script>
+9
View File
@@ -0,0 +1,9 @@
<template>
<div>
<h2>Some text</h2>
<p>This is the third page with static text.</p>
</div>
</template>
<script setup lang="ts">
</script>