This commit is contained in:
Jose Conde
2024-07-22 21:04:51 +02:00
parent a8d6129d3e
commit cd5f4ad91a
38 changed files with 1311 additions and 182 deletions

View File

@ -57,8 +57,6 @@ onUnmounted(() => {
})
</script>
<template>
<RouterView />
</template>
<template><RouterView /></template>
<style scoped></style>

View File

@ -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
}

View File

@ -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>

View 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>

View 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>

View File

@ -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>

View File

@ -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)
}

View File

@ -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[]) {

View File

@ -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)

View File

@ -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

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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')

View File

@ -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()

View File

@ -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')

View 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 || '{}')
}
}

View File

@ -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

View 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
}
}

View 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>
}

View 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
}
}

View File

@ -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,
}
})

View File

@ -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" />

View File

@ -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">

View File

@ -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>

View File

@ -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>