mirror of
https://github.com/Pandipipas/scoreko-dev.git
synced 2026-06-06 03:32:06 +00:00
Support temporary start.gg players until tournament end
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user