mirror of
https://github.com/Pandipipas/scoreko-dev.git
synced 2026-06-06 03:32:06 +00:00
1394 lines
37 KiB
Vue
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>
|