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;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
startAt: number | null;
|
startAt: number | null;
|
||||||
|
endAt: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StartGGImportedPlayer extends Player {
|
interface StartGGImportedPlayer extends Player {
|
||||||
@@ -30,6 +31,16 @@ interface StartGGImportedPlayer extends Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const STARTGG_TOKEN_STORAGE_KEY = 'scoreko-dev.startgg-token';
|
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 playersStore = usePlayersStore();
|
||||||
const rows = computed<PlayerRow[]>(() => playersStore.rows);
|
const rows = computed<PlayerRow[]>(() => playersStore.rows);
|
||||||
@@ -95,6 +106,9 @@ const loadingTournamentPlayers = ref(false);
|
|||||||
const selectedTournament = ref<StartGGTournament | null>(null);
|
const selectedTournament = ref<StartGGTournament | null>(null);
|
||||||
const startGGPlayers = ref<StartGGImportedPlayer[]>([]);
|
const startGGPlayers = ref<StartGGImportedPlayer[]>([]);
|
||||||
const selectedStartGGPlayerIds = ref<string[]>([]);
|
const selectedStartGGPlayerIds = ref<string[]>([]);
|
||||||
|
const importAsTemporary = ref(true);
|
||||||
|
const temporaryStartGGPlayers = ref<TemporaryStartGGPlayersMap>({});
|
||||||
|
let temporaryCleanupTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
const oauthLoading = ref(false);
|
const oauthLoading = ref(false);
|
||||||
const oauthSessionId = ref('');
|
const oauthSessionId = ref('');
|
||||||
@@ -115,6 +129,46 @@ watch(startGGToken, (value) => {
|
|||||||
localStorage.setItem(STARTGG_TOKEN_STORAGE_KEY, 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> =>
|
const sendNodeCGMessage = <T>(messageName: string, payload: unknown): Promise<T> =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
nodecg.sendMessage(messageName, payload, (error, response) => {
|
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 () => {
|
const checkOAuthStatus = async () => {
|
||||||
if (!oauthSessionId.value) {
|
if (!oauthSessionId.value) {
|
||||||
return;
|
return;
|
||||||
@@ -217,6 +298,7 @@ const openStartGGImportDialog = async (tournament: StartGGTournament) => {
|
|||||||
isImportDialogOpen.value = true;
|
isImportDialogOpen.value = true;
|
||||||
loadingTournamentPlayers.value = true;
|
loadingTournamentPlayers.value = true;
|
||||||
selectedStartGGPlayerIds.value = [];
|
selectedStartGGPlayerIds.value = [];
|
||||||
|
importAsTemporary.value = true;
|
||||||
startGGPlayers.value = [];
|
startGGPlayers.value = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -240,6 +322,11 @@ const importSelectedStartGGPlayers = () => {
|
|||||||
selectedStartGGPlayerIds.value.includes(player.id),
|
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) => {
|
selectedPlayers.forEach((player) => {
|
||||||
playersStore.upsertPlayer(player.id, {
|
playersStore.upsertPlayer(player.id, {
|
||||||
gamertag: player.gamertag,
|
gamertag: player.gamertag,
|
||||||
@@ -248,8 +335,20 @@ const importSelectedStartGGPlayers = () => {
|
|||||||
country: player.country,
|
country: player.country,
|
||||||
twitter: player.twitter,
|
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;
|
isImportDialogOpen.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -323,6 +422,10 @@ const handleImport = async (event: Event) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
temporaryStartGGPlayers.value = loadTemporaryStartGGPlayers();
|
||||||
|
cleanupExpiredTemporaryPlayers();
|
||||||
|
temporaryCleanupTimer = setInterval(cleanupExpiredTemporaryPlayers, 60 * 1000);
|
||||||
|
|
||||||
if (startGGToken.value.trim()) {
|
if (startGGToken.value.trim()) {
|
||||||
void loadRecentTournaments();
|
void loadRecentTournaments();
|
||||||
}
|
}
|
||||||
@@ -330,6 +433,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
clearOAuthPolling();
|
clearOAuthPolling();
|
||||||
|
clearTemporaryCleanupTimer();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -510,6 +614,11 @@ onBeforeUnmount(() => {
|
|||||||
<span>Cargando inscritos...</span>
|
<span>Cargando inscritos...</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
|
<QCheckbox
|
||||||
|
v-model="importAsTemporary"
|
||||||
|
label="Importar como temporales (se eliminan automáticamente al terminar el torneo)"
|
||||||
|
class="q-mb-sm"
|
||||||
|
/>
|
||||||
<QOptionGroup
|
<QOptionGroup
|
||||||
v-model="selectedStartGGPlayerIds"
|
v-model="selectedStartGGPlayerIds"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ interface RecentTournament {
|
|||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
startAt: number | null;
|
startAt: number | null;
|
||||||
|
endAt: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportedPlayer {
|
interface ImportedPlayer {
|
||||||
@@ -413,6 +414,7 @@ nodecg.listenFor('startgg:fetchRecentTournaments', async (payload: unknown, ack)
|
|||||||
name
|
name
|
||||||
slug
|
slug
|
||||||
startAt
|
startAt
|
||||||
|
endAt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -432,6 +434,7 @@ nodecg.listenFor('startgg:fetchRecentTournaments', async (payload: unknown, ack)
|
|||||||
name: item.name,
|
name: item.name,
|
||||||
slug: item.slug,
|
slug: item.slug,
|
||||||
startAt: item.startAt,
|
startAt: item.startAt,
|
||||||
|
endAt: item.endAt,
|
||||||
})) ?? [];
|
})) ?? [];
|
||||||
|
|
||||||
sendAck(ack, null, tournaments);
|
sendAck(ack, null, tournaments);
|
||||||
|
|||||||
Reference in New Issue
Block a user