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

550 lines
16 KiB
Vue

<script setup lang="ts">
import { computed, ref, watch, watchEffect, type Ref } from 'vue';
import type { Schemas } from '../../../types';
import { countryOptions, getCountryLabel } from '../../../shared/countries';
import { usePlayersStore } from '../stores/players';
import { useScoreboardStore } from '../stores/scoreboard';
const playersStore = usePlayersStore();
const scoreboardStore = useScoreboardStore();
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 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 leftSelectedPlayer = computed(() => playersStore.players[scoreboardStore.scoreboard.leftPlayerId]);
const rightSelectedPlayer = computed(() => playersStore.players[scoreboardStore.scoreboard.rightPlayerId]);
const getPlayerLabel = (playerId: string) => {
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 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 leftHasSelectedPlayerChanges = computed(() => {
const player = leftSelectedPlayer.value;
if (!player) {
return false;
}
return player.gamertag !== leftPendingGamertag.value
|| player.team !== scoreboardStore.scoreboard.leftTeamOverride
|| player.country !== scoreboardStore.scoreboard.leftCountryOverride;
});
const rightHasSelectedPlayerChanges = computed(() => {
const player = rightSelectedPlayer.value;
if (!player) {
return false;
}
return player.gamertag !== rightPendingGamertag.value
|| player.team !== scoreboardStore.scoreboard.rightTeamOverride
|| player.country !== scoreboardStore.scoreboard.rightCountryOverride;
});
const leftPlayerOptions = computed(() => filterOptions(playerOptions.value, leftFilter.value));
const rightPlayerOptions = computed(() => filterOptions(playerOptions.value, rightFilter.value));
const onLeftFilter = (val: string, update: (fn: () => void) => void) => {
update(() => {
leftFilter.value = val;
leftInput.value = val;
scoreboardStore.scoreboard.leftNameOverride = val;
});
};
const onRightFilter = (val: string, update: (fn: () => void) => void) => {
update(() => {
rightFilter.value = val;
rightInput.value = val;
scoreboardStore.scoreboard.rightNameOverride = val;
});
};
const onLeftFocus = () => {
leftFocused.value = true;
leftInput.value = '';
};
const onLeftBlur = () => {
leftFocused.value = false;
leftFilter.value = '';
leftInput.value = leftDisplayName.value;
};
const onRightFocus = () => {
rightFocused.value = true;
rightInput.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;
}
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;
}
scoreboardStore.scoreboard.rightNameOverride = '';
rightFilter.value = '';
rightInput.value = getPlayerLabel(playerId);
applyRightPlayerData(playerId);
};
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 saveLeftSelectedPlayerChanges = () => {
const playerId = scoreboardStore.scoreboard.leftPlayerId;
const player = playersStore.players[playerId];
if (!player) {
return;
}
playersStore.upsertPlayer(playerId, {
...player,
gamertag: leftPendingGamertag.value,
team: scoreboardStore.scoreboard.leftTeamOverride,
country: scoreboardStore.scoreboard.leftCountryOverride,
});
scoreboardStore.scoreboard.leftNameOverride = '';
};
const saveRightSelectedPlayerChanges = () => {
const playerId = scoreboardStore.scoreboard.rightPlayerId;
const player = playersStore.players[playerId];
if (!player) {
return;
}
playersStore.upsertPlayer(playerId, {
...player,
gamertag: rightPendingGamertag.value,
team: scoreboardStore.scoreboard.rightTeamOverride,
country: scoreboardStore.scoreboard.rightCountryOverride,
});
scoreboardStore.scoreboard.rightNameOverride = '';
};
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;
}
});
</script>
<template>
<div class="scoreboard-panel">
<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"
v-model:input-value="leftInput"
:options="leftPlayerOptions"
label="Jugador"
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"
/>
<QInput
v-model="scoreboardStore.scoreboard.leftTeamOverride"
label="Team"
dense
outlined
class="q-mt-sm"
/>
<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"
/>
<QBtn
v-if="leftCanSave"
color="primary"
icon="save"
label="Guardar jugador"
class="q-mt-sm"
@click="saveLeftPlayer"
/>
<QBtn
v-if="leftHasSelectedPlayerChanges"
color="primary"
icon="save"
label="Guardar cambios del jugador"
class="q-mt-sm q-ml-sm"
@click="saveLeftSelectedPlayerChanges"
/>
<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"
v-model:input-value="rightInput"
:options="rightPlayerOptions"
label="Jugador"
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"
/>
<QInput
v-model="scoreboardStore.scoreboard.rightTeamOverride"
label="Team"
dense
outlined
class="q-mt-sm"
/>
<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"
/>
<QBtn
v-if="rightCanSave"
color="primary"
icon="save"
label="Guardar jugador"
class="q-mt-sm"
@click="saveRightPlayer"
/>
<QBtn
v-if="rightHasSelectedPlayerChanges"
color="primary"
icon="save"
label="Guardar cambios del jugador"
class="q-mt-sm q-ml-sm"
@click="saveRightSelectedPlayerChanges"
/>
<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;
}
</style>