Support temporary start.gg players until tournament end

This commit is contained in:
Pandipipas
2026-02-16 01:10:24 +01:00
parent 4e214a573a
commit 15ef0023ef
2 changed files with 112 additions and 0 deletions
+109
View File
@@ -23,6 +23,7 @@ interface StartGGTournament {
name: string;
slug: string;
startAt: number | null;
endAt: number | null;
}
interface StartGGImportedPlayer extends Player {
@@ -30,6 +31,16 @@ interface StartGGImportedPlayer extends Player {
}
const STARTGG_TOKEN_STORAGE_KEY = 'scoreko-dev.startgg-token';
const STARTGG_TEMP_PLAYERS_STORAGE_KEY = 'scoreko-dev.startgg-temp-players';
const STARTGG_TEMP_FALLBACK_DURATION_SECONDS = 12 * 60 * 60;
interface TemporaryStartGGPlayerMeta {
expiresAt: number;
tournamentSlug: string;
tournamentName: string;
}
type TemporaryStartGGPlayersMap = Record<string, TemporaryStartGGPlayerMeta>;
const playersStore = usePlayersStore();
const rows = computed<PlayerRow[]>(() => playersStore.rows);
@@ -95,6 +106,9 @@ const loadingTournamentPlayers = ref(false);
const selectedTournament = ref<StartGGTournament | null>(null);
const startGGPlayers = ref<StartGGImportedPlayer[]>([]);
const selectedStartGGPlayerIds = ref<string[]>([]);
const importAsTemporary = ref(true);
const temporaryStartGGPlayers = ref<TemporaryStartGGPlayersMap>({});
let temporaryCleanupTimer: ReturnType<typeof setInterval> | null = null;
const oauthLoading = ref(false);
const oauthSessionId = ref('');
@@ -115,6 +129,46 @@ watch(startGGToken, (value) => {
localStorage.setItem(STARTGG_TOKEN_STORAGE_KEY, value);
});
const persistTemporaryStartGGPlayers = () => {
localStorage.setItem(STARTGG_TEMP_PLAYERS_STORAGE_KEY, JSON.stringify(temporaryStartGGPlayers.value));
};
const loadTemporaryStartGGPlayers = (): TemporaryStartGGPlayersMap => {
try {
const raw = localStorage.getItem(STARTGG_TEMP_PLAYERS_STORAGE_KEY);
if (!raw) {
return {};
}
const parsed = JSON.parse(raw) as unknown;
if (typeof parsed !== 'object' || parsed === null) {
return {};
}
const result: TemporaryStartGGPlayersMap = {};
Object.entries(parsed as Record<string, unknown>).forEach(([playerId, value]) => {
if (!playerId || typeof value !== 'object' || value === null) {
return;
}
const candidate = value as Record<string, unknown>;
const expiresAt = Number(candidate.expiresAt);
const tournamentSlug = String(candidate.tournamentSlug || '').trim();
const tournamentName = String(candidate.tournamentName || '').trim();
if (!Number.isFinite(expiresAt) || expiresAt <= 0 || !tournamentSlug) {
return;
}
result[playerId] = {
expiresAt,
tournamentSlug,
tournamentName,
};
});
return result;
} catch {
return {};
}
};
const sendNodeCGMessage = <T>(messageName: string, payload: unknown): Promise<T> =>
new Promise((resolve, reject) => {
nodecg.sendMessage(messageName, payload, (error, response) => {
@@ -133,6 +187,33 @@ const clearOAuthPolling = () => {
}
};
const clearTemporaryCleanupTimer = () => {
if (temporaryCleanupTimer) {
clearInterval(temporaryCleanupTimer);
temporaryCleanupTimer = null;
}
};
const cleanupExpiredTemporaryPlayers = () => {
const now = Math.floor(Date.now() / 1000);
const expiredIds = Object.entries(temporaryStartGGPlayers.value)
.filter(([, meta]) => meta.expiresAt <= now)
.map(([playerId]) => playerId);
if (!expiredIds.length) {
return;
}
const nextMeta = { ...temporaryStartGGPlayers.value };
expiredIds.forEach((playerId) => {
playersStore.removePlayer(playerId);
delete nextMeta[playerId];
});
temporaryStartGGPlayers.value = nextMeta;
persistTemporaryStartGGPlayers();
};
const checkOAuthStatus = async () => {
if (!oauthSessionId.value) {
return;
@@ -217,6 +298,7 @@ const openStartGGImportDialog = async (tournament: StartGGTournament) => {
isImportDialogOpen.value = true;
loadingTournamentPlayers.value = true;
selectedStartGGPlayerIds.value = [];
importAsTemporary.value = true;
startGGPlayers.value = [];
try {
@@ -240,6 +322,11 @@ const importSelectedStartGGPlayers = () => {
selectedStartGGPlayerIds.value.includes(player.id),
);
const nextMeta = { ...temporaryStartGGPlayers.value };
const tournament = selectedTournament.value;
const fallbackEndAt = (tournament?.startAt ?? Math.floor(Date.now() / 1000)) + STARTGG_TEMP_FALLBACK_DURATION_SECONDS;
const expiresAt = tournament?.endAt ?? fallbackEndAt;
selectedPlayers.forEach((player) => {
playersStore.upsertPlayer(player.id, {
gamertag: player.gamertag,
@@ -248,8 +335,20 @@ const importSelectedStartGGPlayers = () => {
country: player.country,
twitter: player.twitter,
});
if (importAsTemporary.value && tournament) {
nextMeta[player.id] = {
expiresAt,
tournamentSlug: tournament.slug,
tournamentName: tournament.name,
};
} else {
delete nextMeta[player.id];
}
});
temporaryStartGGPlayers.value = nextMeta;
persistTemporaryStartGGPlayers();
isImportDialogOpen.value = false;
};
@@ -323,6 +422,10 @@ const handleImport = async (event: Event) => {
};
onMounted(() => {
temporaryStartGGPlayers.value = loadTemporaryStartGGPlayers();
cleanupExpiredTemporaryPlayers();
temporaryCleanupTimer = setInterval(cleanupExpiredTemporaryPlayers, 60 * 1000);
if (startGGToken.value.trim()) {
void loadRecentTournaments();
}
@@ -330,6 +433,7 @@ onMounted(() => {
onBeforeUnmount(() => {
clearOAuthPolling();
clearTemporaryCleanupTimer();
});
</script>
@@ -510,6 +614,11 @@ onBeforeUnmount(() => {
<span>Cargando inscritos...</span>
</div>
<div v-else>
<QCheckbox
v-model="importAsTemporary"
label="Importar como temporales (se eliminan automáticamente al terminar el torneo)"
class="q-mb-sm"
/>
<QOptionGroup
v-model="selectedStartGGPlayerIds"
type="checkbox"
+3
View File
@@ -27,6 +27,7 @@ interface RecentTournament {
name: string;
slug: string;
startAt: number | null;
endAt: number | null;
}
interface ImportedPlayer {
@@ -413,6 +414,7 @@ nodecg.listenFor('startgg:fetchRecentTournaments', async (payload: unknown, ack)
name
slug
startAt
endAt
}
}
}
@@ -432,6 +434,7 @@ nodecg.listenFor('startgg:fetchRecentTournaments', async (payload: unknown, ack)
name: item.name,
slug: item.slug,
startAt: item.startAt,
endAt: item.endAt,
})) ?? [];
sendAck(ack, null, tournaments);