Add start.gg tournament and player import integration

This commit is contained in:
Pandipipas
2026-02-15 23:57:31 +01:00
parent 45fd2e2deb
commit a83f633506
3 changed files with 454 additions and 1 deletions
+228 -1
View File
@@ -4,7 +4,7 @@ import { useHead } from '@unhead/vue';
defineOptions({ name: 'PlayersView' });
import type { QTableColumn } from 'quasar';
import { computed, reactive, ref, watch } from 'vue';
import { computed, onMounted, reactive, ref, watch } from 'vue';
import { countryOptions, getCountryLabel } from '../../../shared/countries';
import type { Schemas } from '../../../types';
import { usePlayersStore } from '../stores/players';
@@ -18,6 +18,19 @@ interface PlayerRow extends Player {
id: string;
}
interface StartGGTournament {
id: number;
name: string;
slug: string;
startAt: number | null;
}
interface StartGGImportedPlayer extends Player {
id: string;
}
const STARTGG_TOKEN_STORAGE_KEY = 'scoreko-dev.startgg-token';
const playersStore = usePlayersStore();
const rows = computed<PlayerRow[]>(() => playersStore.rows);
@@ -73,6 +86,98 @@ watch(
{ immediate: true },
);
const startGGToken = ref(localStorage.getItem(STARTGG_TOKEN_STORAGE_KEY) ?? '');
const recentTournaments = ref<StartGGTournament[]>([]);
const loadingTournaments = ref(false);
const tournamentsError = ref('');
const isImportDialogOpen = ref(false);
const loadingTournamentPlayers = ref(false);
const selectedTournament = ref<StartGGTournament | null>(null);
const startGGPlayers = ref<StartGGImportedPlayer[]>([]);
const selectedStartGGPlayerIds = ref<string[]>([]);
watch(startGGToken, (value) => {
localStorage.setItem(STARTGG_TOKEN_STORAGE_KEY, value);
});
const sendNodeCGMessage = <T>(messageName: string, payload: unknown): Promise<T> =>
new Promise((resolve, reject) => {
nodecg.sendMessage(messageName, payload, (error, response) => {
if (error) {
reject(new Error(String(error)));
return;
}
resolve(response as T);
});
});
const loadRecentTournaments = async () => {
const token = startGGToken.value.trim();
if (!token) {
tournamentsError.value = 'Añade tu token de start.gg para cargar torneos.';
recentTournaments.value = [];
return;
}
tournamentsError.value = '';
loadingTournaments.value = true;
try {
const tournaments = await sendNodeCGMessage<StartGGTournament[]>('startgg:fetchRecentTournaments', {
token,
});
recentTournaments.value = tournaments;
if (!tournaments.length) {
tournamentsError.value = 'No hay torneos recientes para esta cuenta.';
}
} catch (error) {
tournamentsError.value = error instanceof Error ? error.message : 'No se pudieron cargar torneos.';
recentTournaments.value = [];
} finally {
loadingTournaments.value = false;
}
};
const openStartGGImportDialog = async (tournament: StartGGTournament) => {
selectedTournament.value = tournament;
isImportDialogOpen.value = true;
loadingTournamentPlayers.value = true;
selectedStartGGPlayerIds.value = [];
startGGPlayers.value = [];
try {
const importedPlayers = await sendNodeCGMessage<StartGGImportedPlayer[]>('startgg:fetchTournamentPlayers', {
token: startGGToken.value.trim(),
slug: tournament.slug,
});
startGGPlayers.value = importedPlayers;
selectedStartGGPlayerIds.value = importedPlayers.map((player) => player.id);
} catch (error) {
const message = error instanceof Error ? error.message : 'No se pudieron cargar jugadores';
window.alert(message);
isImportDialogOpen.value = false;
} finally {
loadingTournamentPlayers.value = false;
}
};
const importSelectedStartGGPlayers = () => {
const selectedPlayers = startGGPlayers.value.filter((player) =>
selectedStartGGPlayerIds.value.includes(player.id),
);
selectedPlayers.forEach((player) => {
playersStore.upsertPlayer(player.id, {
gamertag: player.gamertag,
name: player.name,
team: player.team,
country: player.country,
twitter: player.twitter,
});
});
isImportDialogOpen.value = false;
};
const generateId = () => {
if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {
return crypto.randomUUID();
@@ -141,6 +246,12 @@ const handleImport = async (event: Event) => {
}
}
};
onMounted(() => {
if (startGGToken.value.trim()) {
void loadRecentTournaments();
}
});
</script>
<template>
@@ -159,6 +270,72 @@ const handleImport = async (event: Event) => {
/>
</div>
<QCard
flat
bordered
class="q-pa-md"
>
<div class="text-h6 q-mb-sm">
Integración start.gg
</div>
<div class="text-caption q-mb-md">
Pega tu token personal de start.gg para cargar automáticamente tus torneos recientes y sus inscritos.
</div>
<div class="row q-col-gutter-sm items-center">
<div class="col-12 col-md-7">
<QInput
v-model="startGGToken"
label="start.gg API Token"
dense
outlined
type="password"
/>
</div>
<div class="col-auto">
<QBtn
color="secondary"
icon="sync"
label="Cargar torneos"
:loading="loadingTournaments"
@click="loadRecentTournaments"
/>
</div>
</div>
<div
v-if="tournamentsError"
class="text-negative q-mt-sm"
>
{{ tournamentsError }}
</div>
<QList
v-if="recentTournaments.length"
bordered
separator
class="q-mt-md startgg-tournaments-list"
>
<QItem
v-for="tournament in recentTournaments"
:key="tournament.id"
clickable
>
<QItemSection>
<QItemLabel>{{ tournament.name }}</QItemLabel>
<QItemLabel caption>
{{ tournament.slug }}
</QItemLabel>
</QItemSection>
<QItemSection side>
<QBtn
color="primary"
unelevated
label="Importar jugadores"
@click.stop="openStartGGImportDialog(tournament)"
/>
</QItemSection>
</QItem>
</QList>
</QCard>
<div class="row items-center q-gutter-sm q-mb-md">
<QInput
v-model="filter"
@@ -222,6 +399,51 @@ const handleImport = async (event: Event) => {
</template>
</QTable>
<QDialog v-model="isImportDialogOpen">
<QCard class="players-dialog">
<QCardSection>
<div class="text-h6">
Importar desde {{ selectedTournament?.name || 'start.gg' }}
</div>
</QCardSection>
<QSeparator />
<QCardSection>
<div
v-if="loadingTournamentPlayers"
class="row items-center q-gutter-sm"
>
<QSpinner />
<span>Cargando inscritos...</span>
</div>
<div v-else>
<QOptionGroup
v-model="selectedStartGGPlayerIds"
type="checkbox"
:options="startGGPlayers.map((player) => ({
label: `${player.gamertag}${player.team ? ` (${player.team})` : ''}${player.country ? ` - ${getCountryLabel(player.country)}` : ''}`,
value: player.id,
}))"
/>
</div>
</QCardSection>
<QSeparator />
<QCardActions align="right">
<QBtn
flat
label="Cancelar"
color="secondary"
@click="isImportDialogOpen = false"
/>
<QBtn
color="primary"
label="Importar seleccionados"
:disable="!selectedStartGGPlayerIds.length"
@click="importSelectedStartGGPlayers"
/>
</QCardActions>
</QCard>
</QDialog>
<QDialog v-model="isDialogOpen">
<QCard class="players-dialog">
<QCardSection>
@@ -324,6 +546,11 @@ const handleImport = async (event: Event) => {
width: min(720px, 90vw);
}
.startgg-tournaments-list {
max-height: 280px;
overflow: auto;
}
.players-underlined-field :deep(.q-field__control) {
min-height: 28px;
padding: 0;