0.1.12
This commit is contained in:
@ -57,8 +57,6 @@ onUnmounted(() => {
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterView />
|
||||
</template>
|
||||
<template><RouterView /></template>
|
||||
|
||||
<style scoped></style>
|
||||
|
@ -111,3 +111,34 @@ export function copyToclipboard(value: string) {
|
||||
const { toClipboard } = useClipboard()
|
||||
toClipboard(value)
|
||||
}
|
||||
|
||||
export function transposeMatrix(matrix: any[][]): any[][] {
|
||||
// Get the number of rows and columns
|
||||
const numRows = matrix.length
|
||||
const numCols = matrix[0].length
|
||||
|
||||
// Create a new matrix with transposed dimensions
|
||||
const transposed = Array.from({ length: numCols }, () => Array(numRows).fill(null))
|
||||
|
||||
// Transpose the matrix
|
||||
for (let row = 0; row < numRows; row++) {
|
||||
for (let col = 0; col < numCols; col++) {
|
||||
transposed[col][row] = matrix[row][col]
|
||||
}
|
||||
}
|
||||
|
||||
return transposed
|
||||
}
|
||||
|
||||
export function createStringMatrix(
|
||||
rows: number,
|
||||
cols: number,
|
||||
initialValue: string = '',
|
||||
): string[][] {
|
||||
// Create an array of arrays (matrix)
|
||||
const matrix: string[][] = Array.from({ length: rows }, () =>
|
||||
Array.from({ length: cols }, () => initialValue),
|
||||
)
|
||||
|
||||
return matrix
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import type { MatchSessionOptions } from '@/common/interfaces'
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const emit = defineEmits(['createMatch'])
|
||||
const emit = defineEmits(['createMatch', 'startSingleMatch'])
|
||||
|
||||
let options = ref<MatchSessionOptions>({
|
||||
background: 'green',
|
||||
@ -14,9 +14,42 @@ let options = ref<MatchSessionOptions>({
|
||||
numPlayers: 1,
|
||||
})
|
||||
|
||||
const winTargetPointsList = [20, 50, 80, 100, 150, 200]
|
||||
const winTargetRoundsList = [1, 2, 3, 4, 5, 6]
|
||||
|
||||
const backgroundOptiopnList = [
|
||||
{
|
||||
label: 'green-fabric',
|
||||
value: 'green',
|
||||
},
|
||||
{
|
||||
label: 'gray-fabric',
|
||||
value: 'gray',
|
||||
},
|
||||
{
|
||||
label: 'blue-fabric',
|
||||
value: 'blue',
|
||||
},
|
||||
{
|
||||
label: 'yellow-fabric',
|
||||
value: 'yellow',
|
||||
},
|
||||
{
|
||||
label: 'red-fabric',
|
||||
value: 'red',
|
||||
},
|
||||
]
|
||||
|
||||
const isSinglePlayer = computed(() => options.value.numPlayers === 1)
|
||||
const isMultiPlayer = computed(() => options.value.numPlayers > 1)
|
||||
|
||||
function createMatch() {
|
||||
emit('createMatch', options.value)
|
||||
}
|
||||
|
||||
function startSingleMatch() {
|
||||
emit('startSingleMatch', options.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -29,7 +62,7 @@ function createMatch() {
|
||||
<div class="buttons has-addons">
|
||||
<button
|
||||
class="button"
|
||||
:class="{ 'is-primary is-selected': options.numPlayers === 1 }"
|
||||
:class="{ 'is-primary is-selected': isSinglePlayer }"
|
||||
@click="
|
||||
() => {
|
||||
console.log('options :>> ', options)
|
||||
@ -41,7 +74,7 @@ function createMatch() {
|
||||
</button>
|
||||
<button
|
||||
class="button"
|
||||
:class="{ 'is-primary is-selected': options.numPlayers > 1 }"
|
||||
:class="{ 'is-primary is-selected': isMultiPlayer }"
|
||||
@click="
|
||||
() => {
|
||||
console.log('options :>> ', options)
|
||||
@ -56,7 +89,7 @@ function createMatch() {
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div class="field" v-if="options.numPlayers > 1">
|
||||
<div class="field" v-if="isMultiPlayer">
|
||||
<label class="label">{{ $t('players-number') }}</label>
|
||||
<div class="control">
|
||||
<div class="buttons has-addons">
|
||||
@ -80,7 +113,7 @@ function createMatch() {
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div class="field" v-if="options.numPlayers > 1">
|
||||
<div class="field" v-if="isMultiPlayer">
|
||||
<div class="control">
|
||||
<label for="teamed" class="checkbox">
|
||||
<input v-model="options.teamed" name="teamed" type="checkbox" />
|
||||
@ -128,12 +161,13 @@ function createMatch() {
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select v-model="options.background" name="background">
|
||||
<option value="wood-1">{{ $t('wood-1') }}</option>
|
||||
<option value="green">{{ $t('green-fabric') }}</option>
|
||||
<option value="gray">{{ $t('gray-fabric') }}</option>
|
||||
<option value="blue">{{ $t('blue-fabric') }}</option>
|
||||
<option value="yellow">{{ $t('yellow-fabric') }}</option>
|
||||
<option value="red">{{ $t('red-fabric') }}</option>
|
||||
<option
|
||||
v-bind:key="option.value"
|
||||
v-for="option in backgroundOptiopnList"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ $t(option.label) }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@ -178,22 +212,24 @@ function createMatch() {
|
||||
<div class="control">
|
||||
<div class="select" v-if="options.winType === 'points'">
|
||||
<select v-model="options.winTarget" name="winTarget">
|
||||
<option value="20">{{ $t('n-points', [20]) }}</option>
|
||||
<option value="50">{{ $t('n-points', [50]) }}</option>
|
||||
<option value="80">{{ $t('n-points', [80]) }}</option>
|
||||
<option value="100">{{ $t('n-points', [100]) }}</option>
|
||||
<option value="150">{{ $t('n-points', [150]) }}</option>
|
||||
<option value="200">{{ $t('n-points', [200]) }}</option>
|
||||
<option
|
||||
v-bind:key="winTarget"
|
||||
v-for="winTarget in winTargetPointsList"
|
||||
:value="winTarget"
|
||||
>
|
||||
{{ $t('n-points', winTarget) }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="select" v-if="options.winType === 'rounds'">
|
||||
<select v-model="options.winTarget" name="winTarget">
|
||||
<option value="1">{{ $t('n-of-m-rounds', [1, 1]) }}</option>
|
||||
<option value="2">{{ $t('n-of-m-rounds', [2, 3]) }}</option>
|
||||
<option value="3">{{ $t('n-of-m-rounds', [3, 5]) }}</option>
|
||||
<option value="4">{{ $t('n-of-m-rounds', [4, 7]) }}</option>
|
||||
<option value="5">{{ $t('n-of-m-rounds', [5, 9]) }}</option>
|
||||
<option value="6">{{ $t('n-of-m-rounds', [6, 11]) }}</option>
|
||||
<option
|
||||
v-bind:key="winTarget"
|
||||
v-for="winTarget in winTargetRoundsList"
|
||||
:value="winTarget"
|
||||
>
|
||||
{{ $t('n-of-m-rounds', [winTarget, winTarget * 2 - 1]) }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@ -201,9 +237,12 @@ function createMatch() {
|
||||
</div>
|
||||
</div>
|
||||
<div class="buttons mt-6">
|
||||
<button class="button is-primary" @click.prevent="createMatch">
|
||||
<button class="button is-primary" @click.prevent="createMatch" v-if="isMultiPlayer">
|
||||
{{ $t('create-match-session') }}
|
||||
</button>
|
||||
<button class="button is-primary" @click.prevent="startSingleMatch" v-if="isSinglePlayer">
|
||||
{{ $t('start-game') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
24
src/components/ScoreboardComponent.vue
Normal file
24
src/components/ScoreboardComponent.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<h3 class="title is-5">{{ title }}</h3>
|
||||
<div v-if="scoreboard">
|
||||
<div v-bind:key="$index" v-for="(score, $index) in sortedScoreboard">
|
||||
<p class="">
|
||||
<span class="title is-5">{{ score.name }}</span>
|
||||
<span class="is-size-5 ml-4">{{ score.score }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { title, scoreboard } = defineProps(['scoreboard', 'title'])
|
||||
|
||||
const sortedScoreboard = computed(() => {
|
||||
const copy = [...(scoreboard || [])]
|
||||
return copy.sort((a, b) => b.score - a.score)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
77
src/components/ScoreboardTableComponent.vue
Normal file
77
src/components/ScoreboardTableComponent.vue
Normal file
@ -0,0 +1,77 @@
|
||||
<script setup lang="ts">
|
||||
import { createStringMatrix } from '@/common/helpers'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps(['games', 'finalScore', 'winner'])
|
||||
|
||||
const playerNames = computed<any[]>(() =>
|
||||
(props.finalScore || []).map((score: any) => {
|
||||
const type = score.name === props.winner.name ? 'winner-name' : 'name'
|
||||
return { type, value: score.name }
|
||||
}),
|
||||
)
|
||||
const totals = computed<any[]>(() =>
|
||||
props.finalScore.map((score: any) => {
|
||||
const type = score.name === props.winner.name ? 'winner-final-score' : 'final-score'
|
||||
return { type, value: `${score.score}` }
|
||||
}),
|
||||
)
|
||||
const matrix = computed<any[][]>(() => {
|
||||
if (props.games === undefined) {
|
||||
return []
|
||||
}
|
||||
const m = props.games.map((game: any) => {
|
||||
const winner = game.winner.name
|
||||
return game.players.map((player: any) => {
|
||||
const type = player.name === winner ? 'winner-score' : 'score'
|
||||
return { type, value: `${player.score}` }
|
||||
})
|
||||
})
|
||||
m.unshift(playerNames.value)
|
||||
m.push(totals.value)
|
||||
const rows = m.length
|
||||
const cols = m[0].length
|
||||
const t: any[][] = createStringMatrix(cols, rows)
|
||||
try {
|
||||
for (let row = 0; row < m.length; row++) {
|
||||
for (let col = 0; col < m[0].length; col++) {
|
||||
t[col][row] = m[row][col]
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('error :>> ', error)
|
||||
}
|
||||
return t
|
||||
})
|
||||
|
||||
function getCellClasses(value: any) {
|
||||
const { type } = value
|
||||
return {
|
||||
'has-text-weight-bold':
|
||||
type === 'name' ||
|
||||
type === 'final-score' ||
|
||||
type === 'winner-final-score' ||
|
||||
type === 'winner-name',
|
||||
'has-text-primary':
|
||||
type === 'winner-score' || type === 'winner-name' || type === 'winner-final-score',
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<table class="table is-striped is-fullwidth is-hoverable">
|
||||
<thead>
|
||||
<th>{{ $t('player') }}</th>
|
||||
<th v-for="(game, $index) in games" :key="game">{{ $t('round-index', [$index]) }}</th>
|
||||
<th>{{ $t('final-score') }}</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, $index) in matrix" :key="$index">
|
||||
<td :class="getCellClasses(col)" v-for="(col, $index) in row" :key="$index">
|
||||
{{ col.value }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
@ -1,24 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { RouterLink, RouterView } from 'vue-router'
|
||||
import { RouterView } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const props = defineProps({
|
||||
navbar: Boolean,
|
||||
})
|
||||
|
||||
defineOptions({
|
||||
name: 'AuthenticatedLayout'
|
||||
name: 'AuthenticatedLayout',
|
||||
})
|
||||
|
||||
function logout() {
|
||||
localStorage.removeItem('isLoggedIn')
|
||||
router.push({ name: 'landing' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="authenticated-layout">
|
||||
<header>
|
||||
<nav>
|
||||
<!-- <button @click="logout">Logout</button> -->
|
||||
<header v-if="props.navbar">
|
||||
<nav class="navbar">
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item">
|
||||
<div class="buttons">
|
||||
<button class="button is-primary" @click="logout">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
|
@ -162,6 +162,7 @@ export class Board extends EventEmitter {
|
||||
this.nextTile = tile
|
||||
lastMove.tile = tile.toPlain()
|
||||
this.movements.push(lastMove)
|
||||
console.log('this.movements :>> ', this.movements)
|
||||
await this.addTile(tile, lastMove)
|
||||
this.setFreeEnd(lastMove)
|
||||
}
|
||||
|
@ -17,14 +17,8 @@ import { Actions } from 'pixi-actions'
|
||||
import { OtherHand } from './OtherHand'
|
||||
import { GameSummayView } from './GameSummayView'
|
||||
import Config from './Config'
|
||||
|
||||
interface GameOptions {
|
||||
boardScale: number
|
||||
handScale: number
|
||||
width: number
|
||||
height: number
|
||||
background: string
|
||||
}
|
||||
import { createText, grayStyle } from './utilities/fonts'
|
||||
import { t } from '@/i18n'
|
||||
|
||||
export class Game extends EventEmitter {
|
||||
public board!: Board
|
||||
@ -91,6 +85,25 @@ export class Game extends EventEmitter {
|
||||
const background = new TilingSprite(Assets.get(`bg-${this.options.background}`))
|
||||
|
||||
this.backgroundLayer.addChild(background)
|
||||
|
||||
const actor = this.options.teamed ? t('team') : t('player')
|
||||
const type =
|
||||
this.options.winType === 'points'
|
||||
? t('n-points', this.options.winTarget)
|
||||
: t('n-rounds', this.options.winTarget)
|
||||
const helptext = t('first-actor-to-win-this-options-wintarget-this-options-wintype', [
|
||||
actor.toLowerCase(),
|
||||
type.toLowerCase(),
|
||||
])
|
||||
|
||||
this.backgroundLayer.addChild(
|
||||
createText({
|
||||
text: `${helptext}`,
|
||||
x: this.app.canvas.width / 2,
|
||||
y: 120,
|
||||
style: grayStyle(14, 'lighter', false),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
initPlayers(players: PlayerDto[]) {
|
||||
|
@ -2,6 +2,7 @@ import { createButton, createContainer } from '@/common/helpers'
|
||||
import type { GameSummary, MatchSessionDto, MatchSessionOptions } from '@/common/interfaces'
|
||||
import { EventEmitter, type Application, type Container } from 'pixi.js'
|
||||
import { createText, whiteStyle, yellowStyle } from './utilities/fonts'
|
||||
import { t } from '@/i18n'
|
||||
|
||||
export class GameSummayView extends EventEmitter {
|
||||
public width: number
|
||||
@ -59,7 +60,7 @@ export class GameSummayView extends EventEmitter {
|
||||
let line = y + 12
|
||||
this.layer.addChild(
|
||||
createText({
|
||||
text: `Winner: ${this.gameSummary.winner.name}`,
|
||||
text: t('winner-name', [this.gameSummary.winner.name]),
|
||||
x: this.width / 2,
|
||||
y: line,
|
||||
style: whiteStyle(20),
|
||||
@ -70,7 +71,7 @@ export class GameSummayView extends EventEmitter {
|
||||
line += 30
|
||||
this.layer.addChild(
|
||||
createText({
|
||||
text: '(Blocked)',
|
||||
text: `(${t('blocked')})`,
|
||||
x: this.width / 2,
|
||||
y: line,
|
||||
style: whiteStyle(),
|
||||
@ -78,15 +79,29 @@ export class GameSummayView extends EventEmitter {
|
||||
)
|
||||
}
|
||||
|
||||
line += 30
|
||||
this.layer.addChild(
|
||||
createText({
|
||||
text: `Points this round: ${this.gameSummary.winner.score}`,
|
||||
x: this.width / 2,
|
||||
y: line,
|
||||
style: whiteStyle(20),
|
||||
}),
|
||||
)
|
||||
if (this.options.winType === 'points') {
|
||||
line += 30
|
||||
this.layer.addChild(
|
||||
createText({
|
||||
text: `Points this round: ${this.gameSummary.winner.score}`,
|
||||
// text: `Points this round: ${this.gameSummary.winner.score}, needed to win: ${this.options.winTarget}`,
|
||||
x: this.width / 2,
|
||||
y: line,
|
||||
style: whiteStyle(20),
|
||||
}),
|
||||
)
|
||||
}
|
||||
// } else if (this.options.winType === 'rounds') {
|
||||
// line += 30
|
||||
// this.layer.addChild(
|
||||
// createText({
|
||||
// text: `Rounds needed to win: ${this.options.winTarget}`,
|
||||
// x: this.width / 2,
|
||||
// y: line,
|
||||
// style: whiteStyle(20),
|
||||
// }),
|
||||
// )
|
||||
// }
|
||||
return line + 16
|
||||
}
|
||||
|
||||
@ -159,7 +174,7 @@ export class GameSummayView extends EventEmitter {
|
||||
}
|
||||
|
||||
render() {
|
||||
const title: string = this.type === 'round' ? 'Round Summary' : 'Match Finished!'
|
||||
const title: string = this.type === 'round' ? t('round-summary') : t('match-finished')
|
||||
this.layer.removeChildren()
|
||||
let y = this.renderTitle(30, title.toUpperCase())
|
||||
y = this.renderWinner(y)
|
||||
|
@ -57,19 +57,19 @@ export const scoreText = new TextStyle({
|
||||
function getStyle(styleOptions: TextStyleOptions = {}) {
|
||||
const {
|
||||
fill = 0xa2a2a2,
|
||||
stroke = 0x565656,
|
||||
fontSize = 15,
|
||||
fontFamily = 'Arial, Helvetica, sans-serif',
|
||||
fontWeight = 'normal',
|
||||
fontStyle = 'normal',
|
||||
dropShadow,
|
||||
letterSpacing = 1,
|
||||
stroke,
|
||||
} = styleOptions
|
||||
const style = new TextStyle({
|
||||
fill,
|
||||
fontFamily,
|
||||
letterSpacing,
|
||||
stroke,
|
||||
stroke: stroke ? stroke : undefined,
|
||||
fontSize,
|
||||
fontStyle,
|
||||
fontWeight: fontWeight as any,
|
||||
@ -78,6 +78,20 @@ function getStyle(styleOptions: TextStyleOptions = {}) {
|
||||
return style
|
||||
}
|
||||
|
||||
const styleFactory = (fill: number) => {
|
||||
return (
|
||||
fontSize: number = 15,
|
||||
fontWeight: TextStyleFontWeight = 'normal',
|
||||
dropShadow: boolean = false,
|
||||
) =>
|
||||
getStyle({
|
||||
fill,
|
||||
fontSize,
|
||||
fontWeight,
|
||||
dropShadow,
|
||||
})
|
||||
}
|
||||
|
||||
export const whiteStyle = (
|
||||
fontSize: number = 15,
|
||||
fontWeight: TextStyleFontWeight = 'normal',
|
||||
@ -101,6 +115,8 @@ export const yellowStyle = (
|
||||
dropShadow,
|
||||
})
|
||||
|
||||
export const grayStyle = styleFactory(0x444444)
|
||||
|
||||
interface TextOptions {
|
||||
text: string
|
||||
x: number
|
||||
|
@ -55,9 +55,20 @@
|
||||
"win-type": "Win unit",
|
||||
"points": "Points",
|
||||
"rounds": "Rounds",
|
||||
"n-points": "{value} Points",
|
||||
"n-points": "{count} Points",
|
||||
"n-rounds": "One Round|{count} Rounds",
|
||||
"n-of-m-rounds": "{0} of {1} Rounds",
|
||||
"create-session": "Create Session",
|
||||
"join-a-multiplayer-session": "Join a Multiplayer Session",
|
||||
"tournaments": "Tournaments"
|
||||
"join-a-multiplayer-session": "Join a Multiplayer Session (No sessions)|Join a Multiplayer Session ({count})|Join a Multiplayer Session ({count})",
|
||||
"tournaments": "Tournaments",
|
||||
"start-game": "Start Game",
|
||||
"player": "Player",
|
||||
"final-score": "Final Score",
|
||||
"round-index": "Round #{0}",
|
||||
"first-actor-to-win-this-options-wintarget-this-options-wintype": "First {0} to win {1}",
|
||||
"team": "team",
|
||||
"winner-name": "Winner: {0}",
|
||||
"blocked": "Blocked",
|
||||
"round-summary": "Round Summary",
|
||||
"match-finished": "Match Finished"
|
||||
}
|
||||
|
@ -55,9 +55,20 @@
|
||||
"win-type": "Unidad de puntaje",
|
||||
"points": "Puntos",
|
||||
"rounds": "Rondas",
|
||||
"n-points": "{0} puntos",
|
||||
"n-points": "{count} puntos",
|
||||
"n-of-m-rounds": "{0} de {1} rondas",
|
||||
"create-session": "Crear sesión",
|
||||
"join-a-multiplayer-session": "Únete a una sesión multijugador",
|
||||
"tournaments": "Torneos"
|
||||
"join-a-multiplayer-session": "Únete a una sesión multijugador|Únete a una sesión multijugador ({count})|Únete a una sesión multijugador ({count})",
|
||||
"tournaments": "Torneos",
|
||||
"start-game": "Empezar la partida",
|
||||
"player": "Jugador",
|
||||
"final-score": "Puntuación final",
|
||||
"round-index": "Juego #{0}",
|
||||
"first-actor-to-win-this-options-wintarget-this-options-wintype": "Primer {0} en ganar {1}",
|
||||
"n-rounds": "Una ronda|{count} rondas",
|
||||
"winner-name": "Ganador: {0}",
|
||||
"blocked": "Cerrado",
|
||||
"round-summary": "Resumen de la ronda",
|
||||
"match-finished": "Partida terminado",
|
||||
"team": "equipo"
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import { SocketIoClientService } from '@/services/SocketIoClientService'
|
||||
import { LoggingService } from '@/services/LoggingService'
|
||||
import { AuthenticationService } from './services/AuthenticationService'
|
||||
import { GameService } from './services/GameService'
|
||||
import { PersistenceService } from './services/PersistenceService'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
@ -23,5 +24,6 @@ app.provide('socket', new SocketIoClientService(import.meta.env.VITE_SOCKET_URL)
|
||||
app.provide('logger', new LoggingService())
|
||||
app.provide('auth', new AuthenticationService())
|
||||
app.provide('game', new GameService())
|
||||
PersistenceService.getInstance()
|
||||
|
||||
app.mount('#app')
|
||||
|
@ -3,6 +3,9 @@ import AuthenticatedLayout from '@/components/layouts/AuthenticatedLayout.vue'
|
||||
import UnauthenticatedLayout from '@/components/layouts/UnauthenticatedLayout.vue'
|
||||
import HomeView from '@/views/HomeView.vue'
|
||||
import LandingView from '@/views/LandingView.vue'
|
||||
import { PersistenceService } from '@/services/PersistenceService'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
@ -19,6 +22,7 @@ const router = createRouter({
|
||||
],
|
||||
},
|
||||
{
|
||||
props: { navbar: true },
|
||||
path: '/home',
|
||||
component: AuthenticatedLayout,
|
||||
children: [
|
||||
@ -37,6 +41,7 @@ const router = createRouter({
|
||||
// component: () => import('../views/AboutView.vue')
|
||||
},
|
||||
{
|
||||
props: { navbar: true },
|
||||
path: '/match/:id',
|
||||
component: AuthenticatedLayout,
|
||||
children: [
|
||||
@ -51,6 +56,7 @@ const router = createRouter({
|
||||
],
|
||||
},
|
||||
{
|
||||
props: { navbar: false },
|
||||
path: '/game/:id',
|
||||
component: AuthenticatedLayout,
|
||||
children: [
|
||||
@ -68,8 +74,13 @@ const router = createRouter({
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
const isLoggedIn = !!sessionStorage.getItem('token')
|
||||
if (to.matched.some((record) => record.meta.requiresAuth) && !isLoggedIn) {
|
||||
const auth = useAuthStore()
|
||||
const { user } = storeToRefs(auth)
|
||||
console.log('user.value :>> ', user.value)
|
||||
const isLoggedIn = user.value === undefined ? false : true
|
||||
if (to.name === 'landing' && isLoggedIn) {
|
||||
next({ name: 'home' })
|
||||
} else if (to.matched.some((record) => record.meta.requiresAuth) && !isLoggedIn) {
|
||||
next({ name: 'landing' })
|
||||
} else {
|
||||
next()
|
||||
|
@ -3,9 +3,12 @@ import { useAuthStore } from '@/stores/auth'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { NetworkService } from '@/services/NetworkService'
|
||||
import dayjs from 'dayjs'
|
||||
import { PersistenceService } from './PersistenceService'
|
||||
|
||||
export class AuthenticationService extends ServiceBase {
|
||||
private networkService = new NetworkService()
|
||||
private auth = useAuthStore()
|
||||
private persistanceService: PersistenceService = PersistenceService.getInstance()
|
||||
|
||||
isAuthenticated() {
|
||||
const auth = useAuthStore()
|
||||
@ -14,17 +17,13 @@ export class AuthenticationService extends ServiceBase {
|
||||
}
|
||||
|
||||
async login(username: string, password: string) {
|
||||
try {
|
||||
const res = await this.networkService.post({
|
||||
uri: '/login',
|
||||
body: { username, password }
|
||||
})
|
||||
const { token } = res
|
||||
this.persist(token)
|
||||
return token
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
const response = await this.networkService.post({
|
||||
uri: '/login',
|
||||
body: { username, password },
|
||||
})
|
||||
const { token, refreshToken } = response
|
||||
this.persist(token, refreshToken)
|
||||
return token
|
||||
}
|
||||
|
||||
async logout() {
|
||||
@ -32,20 +31,32 @@ export class AuthenticationService extends ServiceBase {
|
||||
}
|
||||
|
||||
private removePersistence() {
|
||||
const auth = useAuthStore()
|
||||
const { setJwt, setUser } = auth
|
||||
setJwt(undefined)
|
||||
setUser(undefined)
|
||||
sessionStorage.removeItem('token')
|
||||
const { clearUser, clearToken, clearRefreshToken } = this.auth
|
||||
clearToken()
|
||||
clearUser()
|
||||
clearRefreshToken()
|
||||
this.persistanceService.saveToken('')
|
||||
this.persistanceService.saveRefreshToken('')
|
||||
}
|
||||
|
||||
private persist(jwt: string) {
|
||||
const auth = useAuthStore()
|
||||
const { setJwt, setUser } = auth
|
||||
async persist(jwt: string, refreshJwt?: string) {
|
||||
const { setToken, setUser, setRefreshToken } = this.auth
|
||||
const loggedUser = this.parseJwt(jwt)
|
||||
setJwt(jwt)
|
||||
setToken(jwt)
|
||||
setUser(loggedUser)
|
||||
sessionStorage.setItem('token', jwt)
|
||||
try {
|
||||
await this.persistanceService.saveToken(jwt)
|
||||
} catch (error) {
|
||||
this.logger.error(error, 'Error saving token')
|
||||
}
|
||||
if (refreshJwt) {
|
||||
setRefreshToken(refreshJwt)
|
||||
try {
|
||||
await this.persistanceService.saveRefreshToken(refreshJwt)
|
||||
} catch (error) {
|
||||
this.logger.error(error, 'Error saving refresh token')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private parseJwt(token: string) {
|
||||
@ -57,9 +68,14 @@ export class AuthenticationService extends ServiceBase {
|
||||
return JSON.parse(window.atob(base64))
|
||||
}
|
||||
|
||||
fromStorage() {
|
||||
const token = sessionStorage.getItem('token')
|
||||
if (token) {
|
||||
async fromStorage() {
|
||||
console.log('fromStorage')
|
||||
const auth = useAuthStore()
|
||||
const { setToken, setUser } = auth
|
||||
const token = await this.persistanceService.readToken()
|
||||
const refreshToken = await this.persistanceService.readRefreshToken()
|
||||
|
||||
if (token && refreshToken) {
|
||||
try {
|
||||
const parsed = this.parseJwt(token)
|
||||
const isAfter = dayjs().isAfter(parsed.exp * 1000)
|
||||
@ -67,7 +83,8 @@ export class AuthenticationService extends ServiceBase {
|
||||
this.removePersistence()
|
||||
return
|
||||
}
|
||||
this.persist(token)
|
||||
setToken(token)
|
||||
setUser(parsed)
|
||||
this.logger.debug('Token loaded from storage', parsed)
|
||||
} catch (error) {
|
||||
this.logger.error(error, 'Error parsing token')
|
||||
|
30
src/services/LocalStorageService.ts
Normal file
30
src/services/LocalStorageService.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import type { StorageInterface } from './StorageInterface'
|
||||
|
||||
export class LocalStorageService implements StorageInterface {
|
||||
async saveUserDataText(fileName: string, content: any) {
|
||||
localStorage.setItem(`net.xintanalabs.domino.${fileName}`, content)
|
||||
}
|
||||
|
||||
async saveUserDataJson(fileName: string, content: any) {
|
||||
localStorage.setItem(`net.xintanalabs.domino.${fileName}`, JSON.stringify(content))
|
||||
}
|
||||
|
||||
async saveConfigData(content: any) {
|
||||
localStorage.setItem('net.xintanalabs.domino.config', JSON.stringify(content))
|
||||
}
|
||||
|
||||
async readUserDataText(fileName: string) {
|
||||
const content: string | null = localStorage.getItem(`net.xintanalabs.domino.${fileName}`)
|
||||
return content || ''
|
||||
}
|
||||
|
||||
async readUserDataJson(fileName: string) {
|
||||
const content: string | null = localStorage.getItem(`net.xintanalabs.domino.${fileName}`)
|
||||
return JSON.parse(content || 'null')
|
||||
}
|
||||
|
||||
async readConfigData() {
|
||||
const content: string | null = localStorage.getItem('net.xintanalabs.domino.config')
|
||||
return JSON.parse(content || '{}')
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import type { AuthenticationService } from './AuthenticationService'
|
||||
|
||||
interface RequestOptions {
|
||||
uri: string
|
||||
@ -45,12 +46,20 @@ export class NetworkService {
|
||||
}
|
||||
const fetchOptions = this.getFetchOptions(options)
|
||||
const urlParams = this.getURLParams(params)
|
||||
const res = await fetch(`${this.API_URL}${uri}${urlParams}`, fetchOptions)
|
||||
let response = await fetch(`${this.API_URL}${uri}${urlParams}`, fetchOptions)
|
||||
|
||||
if (!res.ok) {
|
||||
if (response.status === 401) {
|
||||
const newAccessToken = await this.refresh()
|
||||
if (newAccessToken) {
|
||||
fetchOptions.headers.Authorization = newAccessToken
|
||||
response = await fetch(`${this.API_URL}${uri}${urlParams}`, fetchOptions)
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok')
|
||||
}
|
||||
const text = await res.text()
|
||||
const text = await response.text()
|
||||
|
||||
if (text === '') {
|
||||
return
|
||||
@ -59,6 +68,18 @@ export class NetworkService {
|
||||
}
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
const { refreshToken } = storeToRefs(this.auth)
|
||||
const { setToken } = this.auth
|
||||
const response = await await this.post({
|
||||
uri: '/refresh',
|
||||
body: { token: refreshToken.value },
|
||||
})
|
||||
const { token } = response
|
||||
setToken(token)
|
||||
return token
|
||||
}
|
||||
|
||||
getURLParams(params: any) {
|
||||
if (!params) {
|
||||
return ''
|
||||
@ -72,7 +93,7 @@ export class NetworkService {
|
||||
const { body, auth, method = 'GET' } = opts
|
||||
const options: any = {
|
||||
method,
|
||||
headers: this.getHeaders({ auth })
|
||||
headers: this.getHeaders({ auth }),
|
||||
}
|
||||
if (!['GET', 'HEAD'].includes(method) && body) {
|
||||
options.body = typeof body === 'string' ? body : JSON.stringify(body)
|
||||
@ -84,7 +105,7 @@ export class NetworkService {
|
||||
getHeaders({ auth = true }): any {
|
||||
const { jwt } = storeToRefs(this.auth)
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
if (auth) {
|
||||
headers.Authorization = jwt.value
|
||||
|
58
src/services/PersistenceService.ts
Normal file
58
src/services/PersistenceService.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { LocalStorageService } from './LocalStorageService'
|
||||
import type { StorageInterface } from './StorageInterface'
|
||||
import { TauriFileStorageService } from './TauriFileStorageService'
|
||||
|
||||
export class PersistenceService {
|
||||
private static instance: PersistenceService
|
||||
private isTauri: boolean = false
|
||||
private storage: StorageInterface
|
||||
private constructor() {
|
||||
this.isTauri = window.__TAURI_METADATA__ ? true : false
|
||||
this.storage = this.isTauri ? new TauriFileStorageService() : new LocalStorageService()
|
||||
console.log('PersistenceService created', this.isTauri)
|
||||
}
|
||||
|
||||
static getInstance(): PersistenceService {
|
||||
if (!PersistenceService.instance) {
|
||||
PersistenceService.instance = new PersistenceService()
|
||||
}
|
||||
return PersistenceService.instance
|
||||
}
|
||||
|
||||
async saveToken(token: string) {
|
||||
await this.storage.saveUserDataText('token', token)
|
||||
}
|
||||
|
||||
async saveRefreshToken(refreshToken: string) {
|
||||
await this.storage.saveUserDataText('refreshToken', refreshToken)
|
||||
}
|
||||
|
||||
async readToken(): Promise<string> {
|
||||
try {
|
||||
const token = await this.storage.readUserDataText('token')
|
||||
return token
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
async readRefreshToken() {
|
||||
try {
|
||||
const refreshToken = await this.storage.readUserDataText('refreshToken')
|
||||
return refreshToken
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
async saveConfig(config: any) {
|
||||
await this.storage.saveConfigData(config)
|
||||
}
|
||||
|
||||
async readConfig() {
|
||||
const config = await this.storage.readConfigData()
|
||||
return config
|
||||
}
|
||||
}
|
8
src/services/StorageInterface.ts
Normal file
8
src/services/StorageInterface.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface StorageInterface {
|
||||
saveUserDataText(fileName: string, content: any): Promise<void>
|
||||
saveUserDataJson(fileName: string, content: any): Promise<void>
|
||||
saveConfigData(content: any): Promise<void>
|
||||
readUserDataText(fileName: string): Promise<string>
|
||||
readUserDataJson(fileName: string): Promise<any>
|
||||
readConfigData(): Promise<any>
|
||||
}
|
94
src/services/TauriFileStorageService.ts
Normal file
94
src/services/TauriFileStorageService.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import { appConfigDir, appLocalDataDir, appCacheDir, appDataDir, join } from '@tauri-apps/api/path'
|
||||
import { writeTextFile, readTextFile, exists, createDir } from '@tauri-apps/api/fs'
|
||||
import type { StorageInterface } from './StorageInterface'
|
||||
import { ServiceBase } from './ServiceBase'
|
||||
|
||||
export class TauriFileStorageService extends ServiceBase implements StorageInterface {
|
||||
constructor() {
|
||||
super()
|
||||
this.showDirs()
|
||||
}
|
||||
|
||||
async showDirs() {
|
||||
this.logger.debug(`=> appConfigDir ${await appConfigDir()}`)
|
||||
this.logger.debug(`=> appLocalDataDir ${await appLocalDataDir()}`)
|
||||
this.logger.debug(`=> appCacheDir ${await appCacheDir()}`)
|
||||
this.logger.debug(`=> appDataDir ${await appDataDir()}`)
|
||||
}
|
||||
|
||||
async saveUserDataText(fileName: string, content: any) {
|
||||
const userAppDir = await appDataDir()
|
||||
await this.ensureDirExists(userAppDir)
|
||||
const filePath = await join(userAppDir, fileName + '.txt')
|
||||
await this.write(filePath, content, false)
|
||||
}
|
||||
|
||||
async saveUserDataJson(fileName: string, content: any) {
|
||||
const userAppDir = await appDataDir()
|
||||
await this.ensureDirExists(userAppDir)
|
||||
const filePath = await join(userAppDir, fileName + '.json')
|
||||
await this.write(filePath, content, true)
|
||||
}
|
||||
|
||||
async readUserDataText(fileName: string) {
|
||||
const userAppDir = await appDataDir()
|
||||
await this.ensureDirExists(userAppDir)
|
||||
const filePath = await join(userAppDir, fileName + '.txt')
|
||||
const content = await this.read(filePath, false)
|
||||
return content
|
||||
}
|
||||
|
||||
async readUserDataJson(fileName: string) {
|
||||
const userAppDir = await appDataDir()
|
||||
await this.ensureDirExists(userAppDir)
|
||||
const filePath = await join(userAppDir, fileName + '.json')
|
||||
const content = await this.read(filePath, true)
|
||||
return content
|
||||
}
|
||||
|
||||
async saveConfigData(content: any) {
|
||||
const userAppDir = await appConfigDir()
|
||||
await this.ensureDirExists(userAppDir)
|
||||
const filePath = await join(userAppDir, 'config.json')
|
||||
await this.write(filePath, content)
|
||||
}
|
||||
|
||||
async readConfigData() {
|
||||
const userAppDir = await appConfigDir()
|
||||
await this.ensureDirExists(userAppDir)
|
||||
const filePath = await join(userAppDir, 'config.json')
|
||||
const content = await this.read(filePath, true)
|
||||
return content
|
||||
}
|
||||
|
||||
private async ensureDirExists(dir: string) {
|
||||
const dirExists = await exists(dir)
|
||||
if (!dirExists) {
|
||||
await createDir(dir)
|
||||
}
|
||||
}
|
||||
|
||||
private async write(filePath: string, content: any, json: boolean = false) {
|
||||
this.logger.trace(`write ${filePath}`, content)
|
||||
if (json) {
|
||||
return writeTextFile(filePath, JSON.stringify(content, null, 2))
|
||||
}
|
||||
return writeTextFile(filePath, content)
|
||||
}
|
||||
|
||||
private async read(filePath: string, json: boolean = false) {
|
||||
let content = null
|
||||
try {
|
||||
if (await exists(filePath)) {
|
||||
content = await readTextFile(filePath)
|
||||
|
||||
if (json) {
|
||||
content = JSON.parse(content as string)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error)
|
||||
}
|
||||
return content
|
||||
}
|
||||
}
|
@ -3,15 +3,42 @@ import { computed, ref } from 'vue'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const jwt = ref<string | undefined>(undefined)
|
||||
const refreshJwt = ref<string | undefined>(undefined)
|
||||
const user = ref<any | undefined>(undefined)
|
||||
const roles = ref<string[]>([])
|
||||
|
||||
const isLoggedIn = computed(() => jwt.value !== undefined)
|
||||
|
||||
function setJwt(token: string | undefined) {
|
||||
const token = computed(() => {
|
||||
if (jwt.value) {
|
||||
return jwt.value
|
||||
}
|
||||
return sessionStorage.getItem('token')
|
||||
})
|
||||
|
||||
const refreshToken = computed(() => {
|
||||
if (refreshJwt.value) {
|
||||
return refreshJwt.value
|
||||
}
|
||||
return sessionStorage.getItem('token_refresh')
|
||||
})
|
||||
|
||||
function setToken(token: string | undefined) {
|
||||
jwt.value = token
|
||||
}
|
||||
|
||||
function clearToken() {
|
||||
jwt.value = undefined
|
||||
}
|
||||
|
||||
function setRefreshToken(token: string | undefined) {
|
||||
refreshJwt.value = token
|
||||
}
|
||||
|
||||
function clearRefreshToken() {
|
||||
refreshJwt.value = undefined
|
||||
}
|
||||
|
||||
function setUser(userIn: any | undefined) {
|
||||
user.value = userIn
|
||||
setRoles(userIn?.roles ?? [])
|
||||
@ -21,13 +48,23 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
roles.value = rolesIn
|
||||
}
|
||||
|
||||
function clearUser() {
|
||||
user.value = undefined
|
||||
}
|
||||
|
||||
return {
|
||||
jwt,
|
||||
user,
|
||||
roles,
|
||||
isLoggedIn,
|
||||
setJwt,
|
||||
token,
|
||||
refreshToken,
|
||||
setToken,
|
||||
clearToken,
|
||||
setRefreshToken,
|
||||
clearRefreshToken,
|
||||
setUser,
|
||||
setRoles
|
||||
clearUser,
|
||||
setRoles,
|
||||
}
|
||||
})
|
||||
|
@ -9,7 +9,7 @@ import { useRouter } from 'vue-router'
|
||||
|
||||
const { toClipboard } = useClipboard()
|
||||
const gameStore = useGameStore()
|
||||
const { moveToMake, canMakeMove, sessionState, gameState, playerState } = storeToRefs(gameStore)
|
||||
const { moveToMake, canMakeMove, sessionState, playerState } = storeToRefs(gameStore)
|
||||
|
||||
onMounted(async () => {
|
||||
// startMatch()
|
||||
@ -39,7 +39,7 @@ function copySeed() {
|
||||
|
||||
<template>
|
||||
<div class="block">
|
||||
<section class="block info">
|
||||
<!-- <section class="block info">
|
||||
<p>Running: {{ sessionState?.sessionInProgress }}</p>
|
||||
<p>Seed: {{ sessionState?.seed }}</p>
|
||||
<p>
|
||||
@ -50,7 +50,7 @@ function copySeed() {
|
||||
<p>Score: {{ sessionState?.scoreboard }}</p>
|
||||
<p v-if="sessionState?.id">SessionID: {{ sessionState.id }}</p>
|
||||
<p>PlayerID: {{ playerState?.id }}</p>
|
||||
</section>
|
||||
</section> -->
|
||||
<section class="block">
|
||||
<div class="game-container">
|
||||
<GameComponent :playerId="playerState?.id" :canMakeMove="canMakeMove" @move="makeMove" />
|
||||
|
@ -8,7 +8,7 @@ import type { GameService } from '@/services/GameService'
|
||||
import type { MatchSessionOptions, MatchSessionDto } from '@/common/interfaces'
|
||||
import { useEventBusStore } from '@/stores/eventBus'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { copyToclipboard } from '@/common/helpers'
|
||||
import { copyToclipboard, wait } from '@/common/helpers'
|
||||
import { useGameOptionsStore } from '@/stores/gameOptions'
|
||||
import MatchConfiguration from '@/components/MatchConfiguration.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@ -127,14 +127,20 @@ const canStart = computed(() => {
|
||||
return (!options?.teamed && allReady) || (options?.teamed && !!teamedWith.value && allReady)
|
||||
})
|
||||
|
||||
const isMultiplayer = computed(
|
||||
() => (sessionState?.value?.options?.numPlayers || gameOptions.value?.numPlayers || 0) > 1,
|
||||
)
|
||||
|
||||
async function loadData() {
|
||||
loadingSessions.value = true
|
||||
const listResponse = await gameService.listMatchSessions()
|
||||
loadingSessions.value = false
|
||||
matchSessions.value = listResponse.data
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// loadData()
|
||||
// dataInterval = setInterval(loadData, 5000)
|
||||
loadData()
|
||||
dataInterval = setInterval(loadData, 5000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@ -145,31 +151,34 @@ function copy(sessionSeed: string) {
|
||||
copyToclipboard(sessionSeed)
|
||||
}
|
||||
|
||||
let tabs = ref<any[]>([
|
||||
{ label: t('create-session'), id: 'create-tab', active: true, disabled: false },
|
||||
{ label: t('join-a-multiplayer-session'), id: 'join-tab', active: false, disabled: false },
|
||||
{ label: t('tournaments'), id: 'torunaments-tab', active: false, disabled: true },
|
||||
])
|
||||
|
||||
const selectedTab = computed(() => tabs.value.find((t) => t.active)?.id)
|
||||
let selectedTab = ref('create-tab')
|
||||
const isCreateTab = computed(() => selectedTab.value === 'create-tab')
|
||||
const isJoinTab = computed(() => selectedTab.value === 'join-tab')
|
||||
const isTournamentTab = computed(() => selectedTab.value === 'torunaments-tab')
|
||||
|
||||
const tabs = computed<any[]>((): any => [
|
||||
{ label: t('create-session'), id: 'create-tab', disabled: false },
|
||||
{
|
||||
label: t('join-a-multiplayer-session', matchSessions.value.length),
|
||||
id: 'join-tab',
|
||||
disabled: matchSessions.value.length <= 0,
|
||||
},
|
||||
{ label: t('tournaments'), id: 'torunaments-tab', disabled: true },
|
||||
])
|
||||
|
||||
async function tabClick(tab: any) {
|
||||
tabs.value.forEach((t) => (t.active = t === tab))
|
||||
if (tab.id === 'join-tab') {
|
||||
loadingSessions.value = true
|
||||
await loadData()
|
||||
dataInterval = setInterval(loadData, 5000)
|
||||
} else {
|
||||
clearInterval(dataInterval)
|
||||
}
|
||||
selectedTab.value = tab.id
|
||||
}
|
||||
|
||||
function onCreateMatch(options: MatchSessionOptions) {
|
||||
console.log('Creating match', options)
|
||||
createMatch(options)
|
||||
}
|
||||
|
||||
async function onStartSingleMatch(options: MatchSessionOptions) {
|
||||
await createMatch(options)
|
||||
await wait(1000)
|
||||
startMatch()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -185,7 +194,7 @@ function onCreateMatch(options: MatchSessionOptions) {
|
||||
<li
|
||||
v-bind:key="tab.label"
|
||||
v-for="tab in tabs"
|
||||
:class="{ 'is-active': tab.active, 'is-disabled': tab.disabled }"
|
||||
:class="{ 'is-active': selectedTab === tab.id, 'is-disabled': tab.disabled }"
|
||||
>
|
||||
<a @click="() => tabClick(tab)">{{ tab.label }}</a>
|
||||
</li>
|
||||
@ -194,7 +203,10 @@ function onCreateMatch(options: MatchSessionOptions) {
|
||||
<!-- Tabs End -->
|
||||
<!-- Match Configuration -->
|
||||
<section class="section" v-if="isCreateTab">
|
||||
<MatchConfiguration @create-match="onCreateMatch" />
|
||||
<MatchConfiguration
|
||||
@create-match="onCreateMatch"
|
||||
@start-single-match="onStartSingleMatch"
|
||||
/>
|
||||
</section>
|
||||
<!-- Match Configuration End -->
|
||||
<!-- Join a Multiplayer Session -->
|
||||
@ -244,8 +256,7 @@ function onCreateMatch(options: MatchSessionOptions) {
|
||||
<section class="section" v-if="isTournamentTab"></section>
|
||||
<!-- Tournaments End -->
|
||||
</div>
|
||||
|
||||
<div class="block" v-if="isSessionStarted">
|
||||
<div class="block" v-if="isSessionStarted && isMultiplayer">
|
||||
<h2 class="title is-4">{{ sessionState?.name }}</h2>
|
||||
<h6 class="title is-size-5">Players</h6>
|
||||
<div v-for="player in sessionState?.players" :key="player.id">
|
||||
|
@ -3,11 +3,14 @@ import { AuthenticationService } from '@/services/AuthenticationService'
|
||||
import { inject, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { emit, listen } from '@tauri-apps/api/event'
|
||||
|
||||
const router = useRouter()
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const errorLogin = ref(false)
|
||||
const { t } = useI18n()
|
||||
const isTauri = window.__TAURI_METADATA__ ? true : false
|
||||
|
||||
const authService = inject<AuthenticationService>('auth')
|
||||
|
||||
@ -16,7 +19,7 @@ async function login() {
|
||||
await authService?.login(username.value, password.value)
|
||||
router.push({ name: 'home' })
|
||||
} catch (error) {
|
||||
alert(t('invalid-username-or-password'))
|
||||
errorLogin.value = true
|
||||
}
|
||||
// if (username.value === 'admin' && password.value === 'password') {
|
||||
// localStorage.setItem('token', 'true')
|
||||
@ -25,11 +28,43 @@ async function login() {
|
||||
// alert('Invalid username or password')
|
||||
// }
|
||||
}
|
||||
|
||||
// // Listen for update available event
|
||||
// listen('tauri://update-available', () => {
|
||||
// console.log('Update is available!')
|
||||
// // You can show a dialog or notify the user here
|
||||
// })
|
||||
|
||||
// // Listen for update not available event
|
||||
// listen('tauri://update-not-available', () => {
|
||||
// console.log('No update available.')
|
||||
// })
|
||||
|
||||
// // Listen for update download progress
|
||||
// listen('tauri://update-download-progress', (event) => {
|
||||
// console.log('Update download progress:', event.payload)
|
||||
// // You can update a progress bar here
|
||||
// })
|
||||
|
||||
// // Listen for update download finished
|
||||
// listen('tauri://update-download-finished', () => {
|
||||
// console.log('Update download finished.')
|
||||
// // You can notify the user to restart the app
|
||||
// })
|
||||
|
||||
function checkForUpdates() {
|
||||
emit('tauri://update')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login">
|
||||
<h1 class="title">{{ $t('login') }}</h1>
|
||||
<div class="message is-danger">
|
||||
<div class="message-body" v-if="errorLogin">
|
||||
{{ $t('invalid-username-or-password') }}
|
||||
</div>
|
||||
</div>
|
||||
<form class="form" @submit.prevent="login">
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('username') }}</label>
|
||||
@ -63,5 +98,6 @@ async function login() {
|
||||
</div> -->
|
||||
</div>
|
||||
</form>
|
||||
<a href="#" @click="checkForUpdates" v-if="isTauri">Update</a>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -2,14 +2,17 @@
|
||||
import type { MatchSessionDto } from '@/common/interfaces'
|
||||
import type { GameService } from '@/services/GameService'
|
||||
import type { LoggingService } from '@/services/LoggingService'
|
||||
import ScoreboardTableComponent from '@/components/ScoreboardTableComponent.vue'
|
||||
import { inject, onBeforeMount, ref, toRaw } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useGameStore } from '@/stores/game'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const gameStore = useGameStore()
|
||||
const gameService: GameService = inject<GameService>('game') as GameService
|
||||
const logger: LoggingService = inject<LoggingService>('logger') as LoggingService
|
||||
|
||||
const { updateSessionState } = gameStore
|
||||
let sessionId: string
|
||||
let matchSession = ref<MatchSessionDto | undefined>(undefined)
|
||||
|
||||
@ -28,57 +31,45 @@ onBeforeMount(() => {
|
||||
router.push({ name: 'home' })
|
||||
}
|
||||
})
|
||||
|
||||
function gotoHome() {
|
||||
updateSessionState(undefined)
|
||||
router.push({ name: 'home' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<h1 class="title is-1">{{ $t('match-page') }}</h1>
|
||||
<h2 class="title is-3">{{ matchSession?.name }}</h2>
|
||||
<div class="block mt-6">
|
||||
<p class="mb-4">
|
||||
<span class="title is-5">{{ $t('winner') }}</span>
|
||||
<span class="is-size-5 ml-4">{{ matchSession?.matchWinner?.name }}</span>
|
||||
</p>
|
||||
<p class="mb-4">
|
||||
<span class="title is-5">{{ $t('win-type') }}</span>
|
||||
<span class="is-size-5 ml-4">{{ matchSession?.options.winType }}</span>
|
||||
</p>
|
||||
<p class="mb-4">
|
||||
<span class="title is-5">{{ $t('points-to-win') }}</span>
|
||||
<span class="is-size-5 ml-4">{{ matchSession?.options.winTarget }}</span>
|
||||
</p>
|
||||
<h3 class="title is-5">{{ $t('final-scoreboard') }}</h3>
|
||||
<div v-bind:key="$index" v-for="(score, $index) in matchSession?.scoreboard">
|
||||
<p class="">
|
||||
<span class="title is-5">{{ score.name }}</span>
|
||||
<span class="is-size-5 ml-4">{{ score.score }}</span>
|
||||
</p>
|
||||
|
||||
<div class="level">
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">{{ $t('winner') }}</p>
|
||||
<p class="title is-size-3">{{ matchSession?.matchWinner?.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div
|
||||
class="cell"
|
||||
v-bind:key="$index"
|
||||
v-for="(summary, $index) in matchSession?.gameSummaries"
|
||||
>
|
||||
<div class="block mt-6">
|
||||
<h3 class="title is-5">{{ $t('round-index-1', [$index + 1]) }}</h3>
|
||||
<p class="mb-4">
|
||||
<span class="title is-5">{{ $t('winner') }}</span>
|
||||
<span class="is-size-5 ml-4">{{ summary.winner?.name }}</span>
|
||||
</p>
|
||||
<h4 class="title is-6">{{ $t('scoreboard') }}</h4>
|
||||
<div v-bind:key="$index" v-for="(gameScore, $index) in summary.score">
|
||||
<p class="">
|
||||
<span class="title is-5">{{ gameScore.name }}</span>
|
||||
<span class="is-size-5 ml-4">{{ gameScore.score }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">{{ $t('win-type') }}</p>
|
||||
<p class="title">{{ matchSession?.options.winType }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">{{ $t('points-to-win') }}</p>
|
||||
<p class="title">{{ matchSession?.options.winTarget }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ScoreboardTableComponent
|
||||
:games="matchSession?.gameSummaries"
|
||||
:final-score="matchSession?.scoreboard"
|
||||
:winner="matchSession?.matchWinner"
|
||||
/>
|
||||
<div class="buttons">
|
||||
<button class="button is-primary" @click="router.push({ name: 'home' })">
|
||||
<button class="button is-primary" @click="gotoHome">
|
||||
{{ $t('back') }}
|
||||
</button>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user