step: migrate to vue.js
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
Vendored
+7
@@ -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
@@ -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')
|
||||
|
||||
@@ -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
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user