This commit is contained in:
Pandipipas
2026-02-13 15:13:32 +01:00
@@ -26,8 +26,9 @@ const rightCountryOptions = ref(countryOptions);
const leftCharacterInput = ref('');
const rightCharacterInput = ref('');
const gameInput = ref('');
const fightingGameOptions = [
const allFightingGameOptions = [
'Street Fighter 6',
'TEKKEN 8',
'Guilty Gear -Strive-',
@@ -40,7 +41,12 @@ const fightingGameOptions = [
value: game,
}));
const fightingGameOptions = ref(allFightingGameOptions);
const characterOptions = computed(() => getCharactersByGame(scoreboardStore.scoreboard.game));
type CharacterOption = ReturnType<typeof getCharactersByGame>[number];
const leftCharacterOptions = ref<CharacterOption[]>([]);
const rightCharacterOptions = ref<CharacterOption[]>([]);
const leftCharacterImage = computed(() => {
const match = characterOptions.value.find((option) => option.value === scoreboardStore.scoreboard.leftCharacter);
@@ -52,6 +58,9 @@ const rightCharacterImage = computed(() => {
return match?.image ?? '';
});
const leftPanelImage = computed(() => leftCharacterImage.value);
const rightPanelImage = computed(() => rightCharacterImage.value);
const normalizeName = (value: string) => value.trim().toLowerCase();
@@ -90,6 +99,41 @@ const onRightCountryFilter = (value: string, update: (callback: () => void) => v
filterCountries(value, update, rightCountryOptions);
};
const filterCharacters = (
value: string,
update: (callback: () => void) => void,
target: Ref<CharacterOption[]>,
) => {
update(() => {
const needle = value.toLowerCase().trim();
if (!needle) {
target.value = characterOptions.value;
return;
}
target.value = characterOptions.value.filter((character) => character.label.toLowerCase().includes(needle));
});
};
const onLeftCharacterFilter = (value: string, update: (callback: () => void) => void) => {
filterCharacters(value, update, leftCharacterOptions);
};
const onRightCharacterFilter = (value: string, update: (callback: () => void) => void) => {
filterCharacters(value, update, rightCharacterOptions);
};
const onGameFilter = (value: string, update: (callback: () => void) => void) => {
update(() => {
const needle = value.toLowerCase().trim();
if (!needle) {
fightingGameOptions.value = allFightingGameOptions;
return;
}
fightingGameOptions.value = allFightingGameOptions.filter((game) => game.label.toLowerCase().includes(needle));
});
};
const playerOptions = computed(() => {
const base = [{ label: '(Sin asignar)', value: '' }];
const entries = Object.entries(playersStore.players) as [string, Schemas.Players[string]][];
@@ -386,6 +430,14 @@ const onRightSelect = (playerId: string) => {
applyRightPlayerData(playerId);
};
const adjustLeftScore = (delta: number) => {
scoreboardStore.leftScore = Math.max(0, scoreboardStore.leftScore + delta);
};
const adjustRightScore = (delta: number) => {
scoreboardStore.rightScore = Math.max(0, scoreboardStore.rightScore + delta);
};
const createPlayerId = (name: string) => {
const base = name
.trim()
@@ -583,10 +635,22 @@ watchEffect(() => {
}
});
watch(
() => scoreboardStore.scoreboard.game,
(value) => {
const match = allFightingGameOptions.find((option) => option.value === value);
gameInput.value = match?.label ?? '';
},
{ immediate: true },
);
watch(
() => scoreboardStore.scoreboard.game,
() => {
const options = getCharactersByGame(scoreboardStore.scoreboard.game);
leftCharacterOptions.value = options;
rightCharacterOptions.value = options;
const allowed = new Set(options.map((option) => option.value));
if (!allowed.has(scoreboardStore.scoreboard.leftCharacter)) {
@@ -630,32 +694,63 @@ watch(
</div>
</div>
<div class="scoreboard-grid">
<div class="scoreboard-grid__side">
<QCard
flat
bordered
>
<QCardSection>
<div class="text-subtitle1 text-weight-bold">
Left side
<QCard
flat
bordered
class="scoreboard-preview"
>
<div class="scoreboard-preview__side">
<div class="scoreboard-preview__side-inner">
<div class="scoreboard-preview__image-column">
<div class="scoreboard-preview__image-wrap">
<img
v-if="leftPanelImage"
:src="leftPanelImage"
:alt="`${leftDisplayName || 'Left'} preview`"
class="scoreboard-preview__image"
>
<div
v-else
class="scoreboard-preview__empty"
>
Left image
</div>
</div>
</QCardSection>
<QSeparator />
<QCardSection>
<QSelect
v-model="scoreboardStore.scoreboard.leftCharacter"
v-model:input-value="leftCharacterInput"
:options="leftCharacterOptions"
option-value="value"
option-label="label"
emit-value
map-options
label="Character"
dense
use-input
input-debounce="0"
hide-selected
fill-input
clearable
class="scoreboard-preview__field scoreboard-preview__character-field"
:disable="!scoreboardStore.scoreboard.game"
@filter="onLeftCharacterFilter"
/>
</div>
<div class="scoreboard-preview__controls">
<QSelect
v-model="scoreboardStore.scoreboard.leftPlayerId"
v-model:input-value="leftInput"
:options="leftPlayerOptions"
label="Player"
dense
outlined
emit-value
map-options
use-input
input-debounce="0"
hide-selected
fill-input
options-dense
class="scoreboard-preview__field"
@filter="onLeftFilter"
@focus="onLeftFocus"
@blur="onLeftBlur"
@@ -673,40 +768,11 @@ watch(
/>
</template>
</QSelect>
<QSelect
v-model="scoreboardStore.scoreboard.leftCharacter"
v-model:input-value="leftCharacterInput"
:options="characterOptions"
option-value="value"
option-label="label"
emit-value
map-options
label="Character"
dense
outlined
use-input
input-debounce="0"
hide-selected
fill-input
clearable
class="q-mt-sm"
:disable="!scoreboardStore.scoreboard.game"
/>
<div
v-if="leftCharacterImage"
class="character-preview q-mt-sm"
>
<img
:src="leftCharacterImage"
alt="Left character preview"
>
</div>
<QInput
v-model="scoreboardStore.scoreboard.leftTeamOverride"
label="Team"
dense
outlined
class="q-mt-sm"
class="scoreboard-preview__field"
>
<template #append>
<QBtn
@@ -735,8 +801,7 @@ watch(
clearable
label="Country"
dense
outlined
class="q-mt-sm"
class="scoreboard-preview__field"
@filter="onLeftCountryFilter"
>
<template #append>
@@ -751,82 +816,106 @@ watch(
/>
</template>
</QSelect>
<QInput
v-model.number="scoreboardStore.leftScore"
type="number"
label="Score"
</div>
</div>
</div>
<div class="scoreboard-preview__center">
<QSelect
v-model="scoreboardStore.scoreboard.game"
v-model:input-value="gameInput"
:options="fightingGameOptions"
label="Juego"
dense
emit-value
map-options
use-input
input-debounce="0"
hide-selected
fill-input
class="scoreboard-preview__field scoreboard-preview__game-field"
@filter="onGameFilter"
/>
<div class="scoreboard-preview__score-controls">
<div class="scoreboard-preview__score-side">
<QBtn
flat
dense
outlined
class="q-mt-md"
min="0"
round
size="sm"
icon="add"
@click="adjustLeftScore(1)"
/>
</QCardSection>
</QCard>
<span class="scoreboard-preview__score-value">{{ scoreboardStore.scoreboard.leftScore }}</span>
<QBtn
flat
dense
round
size="sm"
icon="remove"
@click="adjustLeftScore(-1)"
/>
</div>
<span class="scoreboard-preview__dash">-</span>
<div class="scoreboard-preview__score-side">
<QBtn
flat
dense
round
size="sm"
icon="add"
@click="adjustRightScore(1)"
/>
<span class="scoreboard-preview__score-value">{{ scoreboardStore.scoreboard.rightScore }}</span>
<QBtn
flat
dense
round
size="sm"
icon="remove"
@click="adjustRightScore(-1)"
/>
</div>
</div>
<div class="scoreboard-preview__actions">
<QBtn
flat
dense
icon="swap_horiz"
class="scoreboard-preview__action-btn"
@click="scoreboardStore.swapPlayers"
/>
<QBtn
flat
dense
icon="restart_alt"
class="scoreboard-preview__action-btn"
@click="scoreboardStore.resetScores"
/>
</div>
</div>
<div class="scoreboard-grid__center">
<QCard
flat
bordered
>
<QCardSection>
<div class="column items-center q-gutter-sm">
<div class="row items-center q-gutter-sm">
<QBtn
color="secondary"
outline
round
icon="swap_horiz"
@click="scoreboardStore.swapPlayers"
/>
<QBtn
color="secondary"
outline
round
icon="restart_alt"
@click="scoreboardStore.resetScores"
/>
</div>
<QSelect
v-model="scoreboardStore.scoreboard.game"
:options="fightingGameOptions"
label="Juego"
dense
outlined
emit-value
map-options
class="full-width"
/>
</div>
</QCardSection>
</QCard>
</div>
<div class="scoreboard-grid__side">
<QCard
flat
bordered
>
<QCardSection>
<div class="text-subtitle1 text-weight-bold">
Right side
</div>
</QCardSection>
<QSeparator />
<QCardSection>
<div class="scoreboard-preview__side scoreboard-preview__side--right">
<div class="scoreboard-preview__side-inner">
<div class="scoreboard-preview__controls">
<QSelect
v-model="scoreboardStore.scoreboard.rightPlayerId"
v-model:input-value="rightInput"
:options="rightPlayerOptions"
label="Player"
dense
outlined
emit-value
map-options
use-input
input-debounce="0"
hide-selected
fill-input
options-dense
class="scoreboard-preview__field"
@filter="onRightFilter"
@focus="onRightFocus"
@blur="onRightBlur"
@@ -844,40 +933,11 @@ watch(
/>
</template>
</QSelect>
<QSelect
v-model="scoreboardStore.scoreboard.rightCharacter"
v-model:input-value="rightCharacterInput"
:options="characterOptions"
option-value="value"
option-label="label"
emit-value
map-options
label="Character"
dense
outlined
use-input
input-debounce="0"
hide-selected
fill-input
clearable
class="q-mt-sm"
:disable="!scoreboardStore.scoreboard.game"
/>
<div
v-if="rightCharacterImage"
class="character-preview q-mt-sm"
>
<img
:src="rightCharacterImage"
alt="Right character preview"
>
</div>
<QInput
v-model="scoreboardStore.scoreboard.rightTeamOverride"
label="Team"
dense
outlined
class="q-mt-sm"
class="scoreboard-preview__field"
>
<template #append>
<QBtn
@@ -906,8 +966,7 @@ watch(
clearable
label="Country"
dense
outlined
class="q-mt-sm"
class="scoreboard-preview__field"
@filter="onRightCountryFilter"
>
<template #append>
@@ -922,19 +981,45 @@ watch(
/>
</template>
</QSelect>
<QInput
v-model.number="scoreboardStore.rightScore"
type="number"
label="Score"
</div>
<div class="scoreboard-preview__image-column">
<div class="scoreboard-preview__image-wrap">
<img
v-if="rightPanelImage"
:src="rightPanelImage"
:alt="`${rightDisplayName || 'Right'} preview`"
class="scoreboard-preview__image"
>
<div
v-else
class="scoreboard-preview__empty"
>
Right image
</div>
</div>
<QSelect
v-model="scoreboardStore.scoreboard.rightCharacter"
v-model:input-value="rightCharacterInput"
:options="rightCharacterOptions"
option-value="value"
option-label="label"
emit-value
map-options
label="Character"
dense
outlined
class="q-mt-md"
min="0"
use-input
input-debounce="0"
hide-selected
fill-input
clearable
class="scoreboard-preview__field scoreboard-preview__character-field"
:disable="!scoreboardStore.scoreboard.game"
@filter="onRightCharacterFilter"
/>
</QCardSection>
</QCard>
</div>
</div>
</div>
</div>
</QCard>
</div>
</template>
@@ -945,40 +1030,209 @@ watch(
gap: 16px;
}
.scoreboard-grid {
.scoreboard-preview {
display: grid;
grid-template-columns: 1fr;
gap: 24px;
align-items: start;
grid-template-columns: minmax(0, 1fr) minmax(320px, auto) minmax(0, 1fr);
gap: 18px;
padding: 16px;
align-items: center;
}
.scoreboard-grid__side,
.scoreboard-grid__center {
min-width: 0;
.scoreboard-preview__side {
display: flex;
align-items: center;
}
.character-preview {
.scoreboard-preview__image-column {
width: min(100%, 320px);
display: flex;
flex-direction: column;
gap: 8px;
}
.scoreboard-preview__side-inner {
width: 100%;
border-radius: 8px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(0, 0, 0, 0.25);
display: grid;
grid-template-columns: minmax(220px, 320px) minmax(180px, 1fr);
align-items: center;
gap: 14px;
}
.character-preview img {
.scoreboard-preview__side--right {
text-align: right;
}
.scoreboard-preview__side--right .scoreboard-preview__side-inner {
grid-template-columns: minmax(180px, 1fr) minmax(220px, 320px);
}
.scoreboard-preview__side .scoreboard-preview__image-column {
justify-self: start;
}
.scoreboard-preview__side--right .scoreboard-preview__image-column {
justify-self: end;
}
.scoreboard-preview__image-wrap {
width: min(100%, 320px);
aspect-ratio: 4 / 4;
border-radius: 10px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.14);
background: #2f3a4f;
}
.scoreboard-preview__image {
display: block;
width: 100%;
height: 88px;
object-fit: cover;
height: 100%;
object-fit: contain;
object-position: center;
transform: scale(2);
transform-origin: center;
}
.scoreboard-preview__empty {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.65);
font-weight: 600;
}
.scoreboard-preview__controls {
width: min(100%, 260px);
justify-self: center;
display: flex;
flex-direction: column;
gap: 6px;
}
.scoreboard-preview__field {
margin: 0;
}
.scoreboard-preview__character-field {
margin-top: 2px;
}
.scoreboard-preview__field :deep(.q-field__control) {
min-height: 28px;
padding: 0;
background: transparent !important;
border-radius: 0;
}
.scoreboard-preview__field :deep(.q-field__control:before),
.scoreboard-preview__field :deep(.q-field__control:after) {
border: 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.34);
}
.scoreboard-preview__field :deep(.q-field__native),
.scoreboard-preview__field :deep(.q-field__input),
.scoreboard-preview__field :deep(.q-field__label) {
color: rgba(255, 255, 255, 0.92);
}
.scoreboard-preview__center {
display: flex;
flex-direction: column;
align-items: center;
align-self: stretch;
justify-content: flex-start;
padding-top: 2px;
gap: 10px;
}
.scoreboard-preview__game-field {
width: min(100%, 240px);
margin-bottom: 56px;
}
.scoreboard-preview__score-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 18px;
}
.scoreboard-preview__actions {
display: flex;
align-items: center;
gap: 10px;
}
.scoreboard-preview__action-btn {
color: #fff;
opacity: 0.85;
}
.scoreboard-preview__action-btn:hover {
opacity: 1;
text-shadow: 0 0 10px rgba(255, 255, 255, 0.45);
}
.scoreboard-preview__score-side {
display: inline-flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.scoreboard-preview__score-value {
min-width: 64px;
text-align: center;
font-size: clamp(4rem, 7vw, 5.6rem);
font-weight: 800;
line-height: 1;
}
.scoreboard-preview__dash {
opacity: 0.7;
font-size: clamp(3rem, 5vw, 4rem);
font-weight: 700;
}
@media (min-width: 1024px) {
.scoreboard-grid {
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
}
.scoreboard-grid__center {
width: 220px;
.scoreboard-preview {
grid-template-columns: minmax(0, 1fr) minmax(320px, auto) minmax(0, 1fr);
}
}
@media (max-width: 900px) {
.scoreboard-preview {
grid-template-columns: 1fr;
gap: 12px;
}
.scoreboard-preview__center {
order: -1;
justify-self: center;
}
.scoreboard-preview__image-wrap {
width: min(100%, 280px);
}
.scoreboard-preview__side-inner {
grid-template-columns: 1fr;
align-items: flex-start;
justify-items: flex-start;
}
.scoreboard-preview__side--right {
text-align: left;
}
.scoreboard-preview__side--right .scoreboard-preview__side-inner {
grid-template-columns: 1fr;
}
}
</style>