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