mirror of
https://github.com/Pandipipas/scoreko-dev.git
synced 2026-06-06 11:42:06 +00:00
Add Pinia persistence and scoreboard support (#15)
* Add scoreboard replicant and Pinia persistence * Fix scoreboard replicant sync (#16)
This commit is contained in:
@@ -2,9 +2,8 @@
|
||||
import { useHead } from '@unhead/vue';
|
||||
import type { QTableColumn } from 'quasar';
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import { playersReplicant } from '../../../browser_shared/replicants';
|
||||
import type { Schemas } from '../../../types';
|
||||
import { usePlayersStore } from '../stores/players';
|
||||
|
||||
useHead({ title: 'Players' });
|
||||
|
||||
@@ -15,22 +14,8 @@ interface PlayerRow extends Player {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const playersData = computed<PlayersMap>({
|
||||
get: () => {
|
||||
const dataRef = playersReplicant?.data as unknown as Ref<PlayersMap | undefined> | undefined;
|
||||
return dataRef?.value ?? {};
|
||||
},
|
||||
set: (value) => {
|
||||
const dataRef = playersReplicant?.data as unknown as Ref<PlayersMap | undefined> | undefined;
|
||||
if (dataRef) {
|
||||
dataRef.value = value;
|
||||
}
|
||||
},
|
||||
});
|
||||
const rows = computed<PlayerRow[]>(() => Object.entries(playersData.value).map(([id, player]) => ({
|
||||
id,
|
||||
...player,
|
||||
})));
|
||||
const playersStore = usePlayersStore();
|
||||
const rows = computed<PlayerRow[]>(() => playersStore.rows);
|
||||
|
||||
const filter = ref('');
|
||||
const isDialogOpen = ref(false);
|
||||
@@ -79,60 +64,21 @@ const openEditDialog = (row: PlayerRow) => {
|
||||
};
|
||||
|
||||
const savePlayer = () => {
|
||||
if (!playersReplicant?.data) {
|
||||
return;
|
||||
}
|
||||
const id = editingId.value ?? generateId();
|
||||
playersData.value = {
|
||||
...playersData.value,
|
||||
[id]: { ...form },
|
||||
};
|
||||
playersStore.upsertPlayer(id, { ...form });
|
||||
isDialogOpen.value = false;
|
||||
};
|
||||
|
||||
const deletePlayer = (row: PlayerRow) => {
|
||||
if (!playersReplicant?.data) {
|
||||
return;
|
||||
}
|
||||
const confirmed = window.confirm(`¿Eliminar a ${row.gamertag || 'este jugador'}?`);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
const next = { ...playersData.value };
|
||||
delete next[row.id];
|
||||
playersData.value = next;
|
||||
};
|
||||
|
||||
const normalizePlayer = (input: unknown): Player => {
|
||||
const candidate = typeof input === 'object' && input !== null ? (input as Record<string, unknown>) : {};
|
||||
return {
|
||||
gamertag: typeof candidate.gamertag === 'string' ? candidate.gamertag : '',
|
||||
team: typeof candidate.team === 'string' ? candidate.team : '',
|
||||
country: typeof candidate.country === 'string' ? candidate.country : '',
|
||||
twitter: typeof candidate.twitter === 'string' ? candidate.twitter : '',
|
||||
realName: typeof candidate.realName === 'string' ? candidate.realName : '',
|
||||
pronouns: typeof candidate.pronouns === 'string' ? candidate.pronouns : '',
|
||||
twitch: typeof candidate.twitch === 'string' ? candidate.twitch : '',
|
||||
notes: typeof candidate.notes === 'string' ? candidate.notes : '',
|
||||
};
|
||||
};
|
||||
|
||||
const normalizePlayers = (input: unknown): PlayersMap => {
|
||||
if (typeof input !== 'object' || input === null) {
|
||||
return {};
|
||||
}
|
||||
const result: PlayersMap = {};
|
||||
Object.entries(input as Record<string, unknown>).forEach(([id, value]) => {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
result[id] = normalizePlayer(value);
|
||||
});
|
||||
return result;
|
||||
playersStore.removePlayer(row.id);
|
||||
};
|
||||
|
||||
const exportPlayers = () => {
|
||||
const data = JSON.stringify(playersData.value, null, 2);
|
||||
const data = JSON.stringify(playersStore.players, null, 2);
|
||||
const blob = new Blob([data], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
@@ -147,9 +93,6 @@ const triggerImport = () => {
|
||||
};
|
||||
|
||||
const handleImport = async (event: Event) => {
|
||||
if (!playersReplicant?.data) {
|
||||
return;
|
||||
}
|
||||
const target = event.target as HTMLInputElement | null;
|
||||
const file = target?.files?.[0];
|
||||
if (!file) {
|
||||
@@ -158,7 +101,7 @@ const handleImport = async (event: Event) => {
|
||||
try {
|
||||
const text = await file.text();
|
||||
const parsed = JSON.parse(text) as unknown;
|
||||
playersData.value = normalizePlayers(parsed);
|
||||
playersStore.setPlayers(parsed as PlayersMap);
|
||||
} catch (error) {
|
||||
window.alert('No se pudo importar el JSON. Verifica el formato.');
|
||||
} finally {
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
<script setup lang="ts">
|
||||
import { useHead } from '@unhead/vue';
|
||||
import { computed } from 'vue';
|
||||
import type { Schemas } from '../../../types';
|
||||
import { usePlayersStore } from '../stores/players';
|
||||
import { useScoreboardStore } from '../stores/scoreboard';
|
||||
|
||||
useHead({ title: 'Scoreboard' });
|
||||
|
||||
const playersStore = usePlayersStore();
|
||||
const scoreboardStore = useScoreboardStore();
|
||||
|
||||
const playerOptions = computed(() => {
|
||||
const base = [{ label: '(Sin asignar)', value: '' }];
|
||||
const entries = Object.entries(playersStore.players) as [string, Schemas.Players[string]][];
|
||||
const options = entries.map(([id, player]) => ({
|
||||
value: id,
|
||||
label: player.gamertag || id,
|
||||
}));
|
||||
return base.concat(options);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<QPage class="q-pa-lg scoreboard-page">
|
||||
<div class="row items-center q-mb-md">
|
||||
<div class="text-h4">Scoreboard</div>
|
||||
<QSpace />
|
||||
<QBtn
|
||||
color="secondary"
|
||||
outline
|
||||
icon="swap_horiz"
|
||||
label="Intercambiar lados"
|
||||
class="q-mr-sm"
|
||||
@click="scoreboardStore.swapPlayers"
|
||||
/>
|
||||
<QBtn
|
||||
color="secondary"
|
||||
outline
|
||||
icon="restart_alt"
|
||||
label="Reset scores"
|
||||
@click="scoreboardStore.resetScores"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="row q-col-gutter-lg">
|
||||
<div class="col-12 col-md-6">
|
||||
<QCard flat bordered>
|
||||
<QCardSection>
|
||||
<div class="text-subtitle1 text-weight-bold">Lado izquierdo</div>
|
||||
</QCardSection>
|
||||
<QSeparator />
|
||||
<QCardSection>
|
||||
<QSelect
|
||||
v-model="scoreboardStore.scoreboard.leftPlayerId"
|
||||
:options="playerOptions"
|
||||
label="Jugador"
|
||||
dense
|
||||
outlined
|
||||
emit-value
|
||||
map-options
|
||||
/>
|
||||
<QInput
|
||||
v-model="scoreboardStore.scoreboard.leftNameOverride"
|
||||
label="Nombre override"
|
||||
dense
|
||||
outlined
|
||||
class="q-mt-md"
|
||||
/>
|
||||
<QInput
|
||||
v-model.number="scoreboardStore.leftScore"
|
||||
type="number"
|
||||
label="Score"
|
||||
dense
|
||||
outlined
|
||||
class="q-mt-md"
|
||||
min="0"
|
||||
/>
|
||||
</QCardSection>
|
||||
</QCard>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<QCard flat bordered>
|
||||
<QCardSection>
|
||||
<div class="text-subtitle1 text-weight-bold">Lado derecho</div>
|
||||
</QCardSection>
|
||||
<QSeparator />
|
||||
<QCardSection>
|
||||
<QSelect
|
||||
v-model="scoreboardStore.scoreboard.rightPlayerId"
|
||||
:options="playerOptions"
|
||||
label="Jugador"
|
||||
dense
|
||||
outlined
|
||||
emit-value
|
||||
map-options
|
||||
/>
|
||||
<QInput
|
||||
v-model="scoreboardStore.scoreboard.rightNameOverride"
|
||||
label="Nombre override"
|
||||
dense
|
||||
outlined
|
||||
class="q-mt-md"
|
||||
/>
|
||||
<QInput
|
||||
v-model.number="scoreboardStore.rightScore"
|
||||
type="number"
|
||||
label="Score"
|
||||
dense
|
||||
outlined
|
||||
class="q-mt-md"
|
||||
min="0"
|
||||
/>
|
||||
</QCardSection>
|
||||
</QCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<QCard flat bordered class="q-mt-lg">
|
||||
<QCardSection>
|
||||
<div class="text-subtitle1 text-weight-bold">Detalles de la ronda</div>
|
||||
</QCardSection>
|
||||
<QSeparator />
|
||||
<QCardSection>
|
||||
<QInput
|
||||
v-model="scoreboardStore.scoreboard.round"
|
||||
label="Ronda (ej. Winners Finals)"
|
||||
dense
|
||||
outlined
|
||||
/>
|
||||
</QCardSection>
|
||||
</QCard>
|
||||
</QPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.scoreboard-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user