diff --git a/src/dashboard/scoreko-dev/views/Players.vue b/src/dashboard/scoreko-dev/views/Players.vue index 34a4a4a..b04c50f 100644 --- a/src/dashboard/scoreko-dev/views/Players.vue +++ b/src/dashboard/scoreko-dev/views/Players.vue @@ -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; const playersStore = usePlayersStore(); const rows = computed(() => playersStore.rows); @@ -95,6 +106,9 @@ const loadingTournamentPlayers = ref(false); const selectedTournament = ref(null); const startGGPlayers = ref([]); const selectedStartGGPlayerIds = ref([]); +const importAsTemporary = ref(true); +const temporaryStartGGPlayers = ref({}); +let temporaryCleanupTimer: ReturnType | 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).forEach(([playerId, value]) => { + if (!playerId || typeof value !== 'object' || value === null) { + return; + } + const candidate = value as Record; + 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 = (messageName: string, payload: unknown): Promise => 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(); }); @@ -510,6 +614,11 @@ onBeforeUnmount(() => { Cargando inscritos...
+