Files
scoreko-dev/src/dashboard/example/components/ScoreboardPanel.vue
T

1394 lines
37 KiB
Vue

<script setup lang="ts">
import { computed, ref, watch, watchEffect, type Ref } from 'vue';
import { countryOptions, getCountryLabel } from '../../../shared/countries';
import { getCharactersByGame } from '../../../shared/fighting-characters';
import type { Schemas } from '../../../types';
import { usePlayersStore } from '../stores/players';
import { useScoreboardStore } from '../stores/scoreboard';
const playersStore = usePlayersStore();
const scoreboardStore = useScoreboardStore();
const CUSTOM_LEFT_PLAYER_ID = '__custom_left_player__';
const CUSTOM_RIGHT_PLAYER_ID = '__custom_right_player__';
const leftFilter = ref('');
const rightFilter = ref('');
const leftInput = ref('');
const rightInput = ref('');
const leftFocused = ref(false);
const rightFocused = ref(false);
const leftCountryInput = ref('');
const rightCountryInput = ref('');
const leftCountryOptions = ref(countryOptions);
const rightCountryOptions = ref(countryOptions);
const leftCharacterInput = ref('');
const rightCharacterInput = ref('');
const fightingGameOptions = [
'Street Fighter 6',
'TEKKEN 8',
'Guilty Gear -Strive-',
'Mortal Kombat 1',
'The King of Fighters XV',
'Granblue Fantasy Versus: Rising',
'2XKO',
].map((game) => ({
label: game,
value: game,
}));
const characterOptions = computed(() => getCharactersByGame(scoreboardStore.scoreboard.game));
const leftCharacterImage = computed(() => {
const match = characterOptions.value.find((option) => option.value === scoreboardStore.scoreboard.leftCharacter);
return match?.image ?? '';
});
const rightCharacterImage = computed(() => {
const match = characterOptions.value.find((option) => option.value === scoreboardStore.scoreboard.rightCharacter);
return match?.image ?? '';
});
const leftPanelImage = computed(() => leftCharacterImage.value);
const rightPanelImage = computed(() => rightCharacterImage.value);
const normalizeName = (value: string) => value.trim().toLowerCase();
const filterOptions = (
options: { label: string; value: string }[],
needle: string,
) => {
if (!needle) {
return options;
}
const lowerNeedle = needle.toLowerCase();
return options.filter((option) => option.label.toLowerCase().includes(lowerNeedle));
};
const filterCountries = (
value: string,
update: (callback: () => void) => void,
target: Ref<{ value: string; label: string }[]>,
) => {
update(() => {
const needle = value.toLowerCase().trim();
if (!needle) {
target.value = countryOptions;
return;
}
target.value = countryOptions.filter((country) => country.label.toLowerCase().includes(needle));
});
};
const onLeftCountryFilter = (value: string, update: (callback: () => void) => void) => {
filterCountries(value, update, leftCountryOptions);
};
const onRightCountryFilter = (value: string, update: (callback: () => void) => void) => {
filterCountries(value, update, rightCountryOptions);
};
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);
});
const buildPlayerOptions = (
filterValue: string,
selectedPlayerId: string,
customPlayerId: string,
customNameOverride: string,
) => {
const options = filterOptions(playerOptions.value, filterValue);
if (selectedPlayerId !== customPlayerId || !customNameOverride.trim()) {
return options;
}
return [{
value: customPlayerId,
label: customNameOverride,
}].concat(options);
};
const leftPlayerOptions = computed(() => buildPlayerOptions(
leftFilter.value,
scoreboardStore.scoreboard.leftPlayerId,
CUSTOM_LEFT_PLAYER_ID,
scoreboardStore.scoreboard.leftNameOverride,
));
const rightPlayerOptions = computed(() => buildPlayerOptions(
rightFilter.value,
scoreboardStore.scoreboard.rightPlayerId,
CUSTOM_RIGHT_PLAYER_ID,
scoreboardStore.scoreboard.rightNameOverride,
));
const leftSelectedPlayer = computed(() => playersStore.players[scoreboardStore.scoreboard.leftPlayerId]);
const rightSelectedPlayer = computed(() => playersStore.players[scoreboardStore.scoreboard.rightPlayerId]);
const getPlayerLabel = (playerId: string) => {
if (playerId === CUSTOM_LEFT_PLAYER_ID) {
return scoreboardStore.scoreboard.leftNameOverride;
}
if (playerId === CUSTOM_RIGHT_PLAYER_ID) {
return scoreboardStore.scoreboard.rightNameOverride;
}
const match = playerOptions.value.find((option) => option.value === playerId);
return match ? match.label : '';
};
const playerExistsByGamertag = (name: string) => {
const normalized = normalizeName(name);
if (!normalized) {
return false;
}
return Object.values(playersStore.players).some((player) => normalizeName(player.gamertag || '') === normalized);
};
const leftDisplayName = computed(() => scoreboardStore.scoreboard.leftNameOverride || getPlayerLabel(scoreboardStore.scoreboard.leftPlayerId));
const rightDisplayName = computed(() => scoreboardStore.scoreboard.rightNameOverride || getPlayerLabel(scoreboardStore.scoreboard.rightPlayerId));
const leftCanSave = computed(
() => Boolean(scoreboardStore.scoreboard.leftNameOverride.trim())
&& !playerExistsByGamertag(scoreboardStore.scoreboard.leftNameOverride),
);
const rightCanSave = computed(
() => Boolean(scoreboardStore.scoreboard.rightNameOverride.trim())
&& !playerExistsByGamertag(scoreboardStore.scoreboard.rightNameOverride),
);
const leftTeamChanged = computed(() => {
const player = leftSelectedPlayer.value;
if (!player) {
return false;
}
return player.team !== scoreboardStore.scoreboard.leftTeamOverride;
});
const rightTeamChanged = computed(() => {
const player = rightSelectedPlayer.value;
if (!player) {
return false;
}
return player.team !== scoreboardStore.scoreboard.rightTeamOverride;
});
const leftCountryChanged = computed(() => {
const player = leftSelectedPlayer.value;
if (!player) {
return false;
}
return player.country !== scoreboardStore.scoreboard.leftCountryOverride;
});
const rightCountryChanged = computed(() => {
const player = rightSelectedPlayer.value;
if (!player) {
return false;
}
return player.country !== scoreboardStore.scoreboard.rightCountryOverride;
});
const leftPendingGamertag = computed(() => {
const override = scoreboardStore.scoreboard.leftNameOverride.trim();
if (override) {
return override;
}
return leftSelectedPlayer.value?.gamertag ?? '';
});
const rightPendingGamertag = computed(() => {
const override = scoreboardStore.scoreboard.rightNameOverride.trim();
if (override) {
return override;
}
return rightSelectedPlayer.value?.gamertag ?? '';
});
const leftNameChanged = computed(() => {
const player = leftSelectedPlayer.value;
if (!player) {
return false;
}
return player.gamertag !== leftPendingGamertag.value;
});
const rightNameChanged = computed(() => {
const player = rightSelectedPlayer.value;
if (!player) {
return false;
}
return player.gamertag !== rightPendingGamertag.value;
});
const leftCanSaveNameChange = computed(
() => leftNameChanged.value && !playerExistsByGamertag(leftPendingGamertag.value),
);
const rightCanSaveNameChange = computed(
() => rightNameChanged.value && !playerExistsByGamertag(rightPendingGamertag.value),
);
const leftShowsNameSave = computed(() => leftCanSave.value || leftCanSaveNameChange.value);
const rightShowsNameSave = computed(() => rightCanSave.value || rightCanSaveNameChange.value);
const startLeftCustomPlayer = () => {
const wasCustom = scoreboardStore.scoreboard.leftPlayerId === CUSTOM_LEFT_PLAYER_ID;
scoreboardStore.scoreboard.leftPlayerId = CUSTOM_LEFT_PLAYER_ID;
if (!wasCustom) {
scoreboardStore.scoreboard.leftTeamOverride = '';
scoreboardStore.scoreboard.leftCountryOverride = '';
}
};
const startRightCustomPlayer = () => {
const wasCustom = scoreboardStore.scoreboard.rightPlayerId === CUSTOM_RIGHT_PLAYER_ID;
scoreboardStore.scoreboard.rightPlayerId = CUSTOM_RIGHT_PLAYER_ID;
if (!wasCustom) {
scoreboardStore.scoreboard.rightTeamOverride = '';
scoreboardStore.scoreboard.rightCountryOverride = '';
}
};
const filterPlayerInput = (
val: string,
update: (fn: () => void) => void,
filterValue: Ref<string>,
focused: Ref<boolean>,
inputValue: Ref<string>,
selectedPlayerId: string,
customPlayerId: string,
customNameOverride: string,
setCustomNameOverride: (value: string) => void,
startCustomPlayer: () => void,
) => {
update(() => {
filterValue.value = val;
if (!focused.value) {
return;
}
if (!val.trim() && selectedPlayerId === customPlayerId) {
inputValue.value = customNameOverride;
return;
}
inputValue.value = val;
setCustomNameOverride(val);
if (val.trim()) {
startCustomPlayer();
}
});
};
const onLeftFilter = (val: string, update: (fn: () => void) => void) => {
filterPlayerInput(
val,
update,
leftFilter,
leftFocused,
leftInput,
scoreboardStore.scoreboard.leftPlayerId,
CUSTOM_LEFT_PLAYER_ID,
scoreboardStore.scoreboard.leftNameOverride,
(value) => {
scoreboardStore.scoreboard.leftNameOverride = value;
},
startLeftCustomPlayer,
);
};
const onRightFilter = (val: string, update: (fn: () => void) => void) => {
filterPlayerInput(
val,
update,
rightFilter,
rightFocused,
rightInput,
scoreboardStore.scoreboard.rightPlayerId,
CUSTOM_RIGHT_PLAYER_ID,
scoreboardStore.scoreboard.rightNameOverride,
(value) => {
scoreboardStore.scoreboard.rightNameOverride = value;
},
startRightCustomPlayer,
);
};
const onLeftFocus = () => {
leftFocused.value = true;
leftInput.value = leftDisplayName.value;
};
const onLeftBlur = () => {
leftFocused.value = false;
leftFilter.value = '';
leftInput.value = leftDisplayName.value;
};
const onRightFocus = () => {
rightFocused.value = true;
rightInput.value = rightDisplayName.value;
};
const onRightBlur = () => {
rightFocused.value = false;
rightFilter.value = '';
rightInput.value = rightDisplayName.value;
};
const applyLeftPlayerData = (playerId: string) => {
const player = playersStore.players[playerId];
scoreboardStore.scoreboard.leftTeamOverride = player?.team ?? '';
scoreboardStore.scoreboard.leftCountryOverride = player?.country ?? '';
leftCountryInput.value = getCountryLabel(scoreboardStore.scoreboard.leftCountryOverride);
};
const applyRightPlayerData = (playerId: string) => {
const player = playersStore.players[playerId];
scoreboardStore.scoreboard.rightTeamOverride = player?.team ?? '';
scoreboardStore.scoreboard.rightCountryOverride = player?.country ?? '';
rightCountryInput.value = getCountryLabel(scoreboardStore.scoreboard.rightCountryOverride);
};
const onLeftSelect = (playerId: string) => {
const hasExistingPlayer = Boolean(playerId && playersStore.players[playerId]);
if (!hasExistingPlayer) {
return;
}
leftFocused.value = false;
scoreboardStore.scoreboard.leftNameOverride = '';
leftFilter.value = '';
leftInput.value = getPlayerLabel(playerId);
applyLeftPlayerData(playerId);
};
const onRightSelect = (playerId: string) => {
const hasExistingPlayer = Boolean(playerId && playersStore.players[playerId]);
if (!hasExistingPlayer) {
return;
}
rightFocused.value = false;
scoreboardStore.scoreboard.rightNameOverride = '';
rightFilter.value = '';
rightInput.value = getPlayerLabel(playerId);
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()
.toLowerCase()
.normalize('NFD')
.replace(/[^\w\s-]/g, '')
.replace(/[\u0300-\u036f]/g, '')
.replace(/\s+/g, '-');
const seed = base || 'player';
let index = 1;
let candidate = seed;
while (playersStore.players[candidate]) {
index += 1;
candidate = `${seed}-${index}`;
}
return candidate;
};
const saveLeftPlayer = () => {
const gamertag = scoreboardStore.scoreboard.leftNameOverride.trim();
if (!gamertag || playerExistsByGamertag(gamertag)) {
return;
}
const id = createPlayerId(gamertag);
playersStore.upsertPlayer(id, {
gamertag,
name: '',
team: scoreboardStore.scoreboard.leftTeamOverride,
country: scoreboardStore.scoreboard.leftCountryOverride,
twitter: '',
});
scoreboardStore.scoreboard.leftPlayerId = id;
scoreboardStore.scoreboard.leftNameOverride = '';
leftInput.value = gamertag;
};
const saveRightPlayer = () => {
const gamertag = scoreboardStore.scoreboard.rightNameOverride.trim();
if (!gamertag || playerExistsByGamertag(gamertag)) {
return;
}
const id = createPlayerId(gamertag);
playersStore.upsertPlayer(id, {
gamertag,
name: '',
team: scoreboardStore.scoreboard.rightTeamOverride,
country: scoreboardStore.scoreboard.rightCountryOverride,
twitter: '',
});
scoreboardStore.scoreboard.rightPlayerId = id;
scoreboardStore.scoreboard.rightNameOverride = '';
rightInput.value = gamertag;
};
const saveLeftNameChange = () => {
const playerId = scoreboardStore.scoreboard.leftPlayerId;
const player = playersStore.players[playerId];
if (!player) {
return;
}
if (!leftCanSaveNameChange.value) {
return;
}
playersStore.upsertPlayer(playerId, {
...player,
gamertag: leftPendingGamertag.value,
});
scoreboardStore.scoreboard.leftNameOverride = '';
};
const saveRightNameChange = () => {
const playerId = scoreboardStore.scoreboard.rightPlayerId;
const player = playersStore.players[playerId];
if (!player) {
return;
}
if (!rightCanSaveNameChange.value) {
return;
}
playersStore.upsertPlayer(playerId, {
...player,
gamertag: rightPendingGamertag.value,
});
scoreboardStore.scoreboard.rightNameOverride = '';
};
const saveLeftTeamChange = () => {
const playerId = scoreboardStore.scoreboard.leftPlayerId;
const player = playersStore.players[playerId];
if (!player) {
return;
}
playersStore.upsertPlayer(playerId, {
...player,
team: scoreboardStore.scoreboard.leftTeamOverride,
});
};
const saveRightTeamChange = () => {
const playerId = scoreboardStore.scoreboard.rightPlayerId;
const player = playersStore.players[playerId];
if (!player) {
return;
}
playersStore.upsertPlayer(playerId, {
...player,
team: scoreboardStore.scoreboard.rightTeamOverride,
});
};
const saveLeftCountryChange = () => {
const playerId = scoreboardStore.scoreboard.leftPlayerId;
const player = playersStore.players[playerId];
if (!player) {
return;
}
playersStore.upsertPlayer(playerId, {
...player,
country: scoreboardStore.scoreboard.leftCountryOverride,
});
};
const saveRightCountryChange = () => {
const playerId = scoreboardStore.scoreboard.rightPlayerId;
const player = playersStore.players[playerId];
if (!player) {
return;
}
playersStore.upsertPlayer(playerId, {
...player,
country: scoreboardStore.scoreboard.rightCountryOverride,
});
};
const onLeftNameSave = () => {
if (leftCanSave.value) {
saveLeftPlayer();
return;
}
saveLeftNameChange();
};
const onRightNameSave = () => {
if (rightCanSave.value) {
saveRightPlayer();
return;
}
saveRightNameChange();
};
watch(
() => scoreboardStore.scoreboard.leftPlayerId,
(playerId) => {
applyLeftPlayerData(playerId);
},
{ immediate: true },
);
watch(
() => scoreboardStore.scoreboard.rightPlayerId,
(playerId) => {
applyRightPlayerData(playerId);
},
{ immediate: true },
);
watch(
() => scoreboardStore.scoreboard.leftCountryOverride,
(value) => {
leftCountryInput.value = getCountryLabel(value);
},
{ immediate: true },
);
watch(
() => scoreboardStore.scoreboard.rightCountryOverride,
(value) => {
rightCountryInput.value = getCountryLabel(value);
},
{ immediate: true },
);
watchEffect(() => {
if (!leftFocused.value) {
leftInput.value = leftDisplayName.value;
}
});
watchEffect(() => {
if (!rightFocused.value) {
rightInput.value = rightDisplayName.value;
}
});
watch(
() => scoreboardStore.scoreboard.game,
() => {
const options = getCharactersByGame(scoreboardStore.scoreboard.game);
const allowed = new Set(options.map((option) => option.value));
if (!allowed.has(scoreboardStore.scoreboard.leftCharacter)) {
scoreboardStore.scoreboard.leftCharacter = '';
leftCharacterInput.value = '';
}
if (!allowed.has(scoreboardStore.scoreboard.rightCharacter)) {
scoreboardStore.scoreboard.rightCharacter = '';
rightCharacterInput.value = '';
}
},
{ immediate: true },
);
watch(
() => scoreboardStore.scoreboard.leftCharacter,
(value) => {
const match = characterOptions.value.find((option) => option.value === value);
leftCharacterInput.value = match?.label ?? '';
},
{ immediate: true },
);
watch(
() => scoreboardStore.scoreboard.rightCharacter,
(value) => {
const match = characterOptions.value.find((option) => option.value === value);
rightCharacterInput.value = match?.label ?? '';
},
{ immediate: true },
);
</script>
<template>
<div class="scoreboard-panel">
<div class="row items-center q-mb-md">
<div class="text-h4">
Scoreboard
</div>
</div>
<QCard
flat
bordered
class="scoreboard-preview"
>
<div class="scoreboard-preview__side">
<div class="scoreboard-preview__side-inner">
<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>
<div class="scoreboard-preview__controls">
<QSelect
v-model="scoreboardStore.scoreboard.leftPlayerId"
v-model:input-value="leftInput"
:options="leftPlayerOptions"
label="Player"
dense
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"
@update:model-value="onLeftSelect"
>
<template #append>
<QBtn
v-if="leftShowsNameSave"
flat
round
dense
icon="save"
color="primary"
@click.stop="onLeftNameSave"
/>
</template>
</QSelect>
<QInput
v-model="scoreboardStore.scoreboard.leftTeamOverride"
label="Team"
dense
class="scoreboard-preview__field"
>
<template #append>
<QBtn
v-if="leftTeamChanged"
flat
round
dense
icon="save"
color="primary"
@click.stop="saveLeftTeamChange"
/>
</template>
</QInput>
<QSelect
v-model="scoreboardStore.scoreboard.leftCountryOverride"
v-model:input-value="leftCountryInput"
:options="leftCountryOptions"
option-value="value"
option-label="label"
emit-value
map-options
use-input
input-debounce="0"
hide-selected
fill-input
clearable
label="Country"
dense
class="scoreboard-preview__field"
@filter="onLeftCountryFilter"
>
<template #append>
<QBtn
v-if="leftCountryChanged"
flat
round
dense
icon="save"
color="primary"
@click.stop="saveLeftCountryChange"
/>
</template>
</QSelect>
</div>
</div>
</div>
<div class="scoreboard-preview__score-controls">
<div class="scoreboard-preview__score-side">
<QBtn
flat
dense
round
size="sm"
icon="add"
@click="adjustLeftScore(1)"
/>
<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__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
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"
@update:model-value="onRightSelect"
>
<template #append>
<QBtn
v-if="rightShowsNameSave"
flat
round
dense
icon="save"
color="primary"
@click.stop="onRightNameSave"
/>
</template>
</QSelect>
<QInput
v-model="scoreboardStore.scoreboard.rightTeamOverride"
label="Team"
dense
class="scoreboard-preview__field"
>
<template #append>
<QBtn
v-if="rightTeamChanged"
flat
round
dense
icon="save"
color="primary"
@click.stop="saveRightTeamChange"
/>
</template>
</QInput>
<QSelect
v-model="scoreboardStore.scoreboard.rightCountryOverride"
v-model:input-value="rightCountryInput"
:options="rightCountryOptions"
option-value="value"
option-label="label"
emit-value
map-options
use-input
input-debounce="0"
hide-selected
fill-input
clearable
label="Country"
dense
class="scoreboard-preview__field"
@filter="onRightCountryFilter"
>
<template #append>
<QBtn
v-if="rightCountryChanged"
flat
round
dense
icon="save"
color="primary"
@click.stop="saveRightCountryChange"
/>
</template>
</QSelect>
</div>
<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>
</div>
</div>
</QCard>
<div class="scoreboard-grid">
<div class="scoreboard-grid__side">
<QCard
flat
bordered
>
<QCardSection>
<div class="text-subtitle1 text-weight-bold">
Left side
</div>
</QCardSection>
<QSeparator />
<QCardSection>
<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
@filter="onLeftFilter"
@focus="onLeftFocus"
@blur="onLeftBlur"
@update:model-value="onLeftSelect"
>
<template #append>
<QBtn
v-if="leftShowsNameSave"
flat
round
dense
icon="save"
color="primary"
@click.stop="onLeftNameSave"
/>
</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"
>
<template #append>
<QBtn
v-if="leftTeamChanged"
flat
round
dense
icon="save"
color="primary"
@click.stop="saveLeftTeamChange"
/>
</template>
</QInput>
<QSelect
v-model="scoreboardStore.scoreboard.leftCountryOverride"
v-model:input-value="leftCountryInput"
:options="leftCountryOptions"
option-value="value"
option-label="label"
emit-value
map-options
use-input
input-debounce="0"
hide-selected
fill-input
clearable
label="Country"
dense
outlined
class="q-mt-sm"
@filter="onLeftCountryFilter"
>
<template #append>
<QBtn
v-if="leftCountryChanged"
flat
round
dense
icon="save"
color="primary"
@click.stop="saveLeftCountryChange"
/>
</template>
</QSelect>
<QInput
v-model.number="scoreboardStore.leftScore"
type="number"
label="Score"
dense
outlined
class="q-mt-md"
min="0"
/>
</QCardSection>
</QCard>
</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>
<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
@filter="onRightFilter"
@focus="onRightFocus"
@blur="onRightBlur"
@update:model-value="onRightSelect"
>
<template #append>
<QBtn
v-if="rightShowsNameSave"
flat
round
dense
icon="save"
color="primary"
@click.stop="onRightNameSave"
/>
</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"
>
<template #append>
<QBtn
v-if="rightTeamChanged"
flat
round
dense
icon="save"
color="primary"
@click.stop="saveRightTeamChange"
/>
</template>
</QInput>
<QSelect
v-model="scoreboardStore.scoreboard.rightCountryOverride"
v-model:input-value="rightCountryInput"
:options="rightCountryOptions"
option-value="value"
option-label="label"
emit-value
map-options
use-input
input-debounce="0"
hide-selected
fill-input
clearable
label="Country"
dense
outlined
class="q-mt-sm"
@filter="onRightCountryFilter"
>
<template #append>
<QBtn
v-if="rightCountryChanged"
flat
round
dense
icon="save"
color="primary"
@click.stop="saveRightCountryChange"
/>
</template>
</QSelect>
<QInput
v-model.number="scoreboardStore.rightScore"
type="number"
label="Score"
dense
outlined
class="q-mt-md"
min="0"
/>
</QCardSection>
</QCard>
</div>
</div>
</div>
</template>
<style scoped>
.scoreboard-panel {
display: flex;
flex-direction: column;
gap: 16px;
}
.scoreboard-preview {
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
gap: 10px;
padding: 16px;
align-items: center;
}
.scoreboard-preview__side {
display: flex;
align-items: center;
}
.scoreboard-preview__side-inner {
width: 100%;
display: flex;
align-items: center;
gap: 10px;
}
.scoreboard-preview__side--right {
text-align: right;
}
.scoreboard-preview__side--right .scoreboard-preview__side-inner {
justify-content: flex-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: 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);
display: flex;
flex-direction: column;
gap: 6px;
}
.scoreboard-preview__field {
margin: 0;
}
.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__score-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.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;
}
.scoreboard-grid {
display: grid;
grid-template-columns: 1fr;
gap: 24px;
align-items: start;
}
.scoreboard-grid__side,
.scoreboard-grid__center {
min-width: 0;
}
.character-preview {
width: 100%;
border-radius: 8px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(0, 0, 0, 0.25);
}
.character-preview img {
display: block;
width: 100%;
height: 88px;
object-fit: cover;
}
@media (max-width: 900px) {
.scoreboard-preview {
grid-template-columns: 1fr;
gap: 12px;
}
.scoreboard-preview__score-controls {
order: -1;
justify-self: center;
}
.scoreboard-preview__image-wrap {
width: min(100%, 280px);
}
.scoreboard-preview__side-inner {
flex-direction: column;
align-items: flex-start;
}
.scoreboard-preview__side--right {
text-align: left;
}
.scoreboard-preview__side--right .scoreboard-preview__side-inner {
justify-content: flex-start;
}
}
@media (min-width: 1024px) {
.scoreboard-preview {
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
}
.scoreboard-grid {
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
}
.scoreboard-grid__center {
width: 220px;
}
}
</style>