fix: use vue-query; make less useless api call

This commit is contained in:
2026-10-16 01:22:16 +02:00
parent b403311ce1
commit acd08e216a
17 changed files with 1127 additions and 110 deletions
+1 -7
View File
@@ -1,20 +1,14 @@
<script setup lang="ts">
import { computed, onMounted } from "vue";
import { computed } from "vue";
import { RouterView, useRoute } from "vue-router";
import AppHeader from "@/layout/AppHeader.vue";
import AppFooter from "@/layout/AppFooter.vue";
import { useUserStore } from "@/stores/user";
const userStore = useUserStore();
const route = useRoute();
// Embed routes (e.g. place/embed) are meant to be dropped into an <iframe>
// as-is - the header/footer chrome would just eat into the fixed space a
// stream overlay or OBS browser source gives it.
const isEmbed = computed(() => route.meta.embed === true);
onMounted(() => {
userStore.loadUserData();
});
</script>
<template>
+4 -13
View File
@@ -1,9 +1,10 @@
<script setup lang="ts">
import { ref, onMounted, computed } from "vue";
import { ref, computed } from "vue";
import { useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
import { useTitle } from "@vueuse/core";
import { gamesApiListEventGames } from "./api";
import { useQuery } from "@tanstack/vue-query";
import { eventGamesQuery } from "@/queries";
import type { GamesApiListEventGamesResponse } from "./api/types.gen";
interface Props {
@@ -16,8 +17,7 @@ const router = useRouter();
const { t } = useI18n();
useTitle(computed(() => t("home.title")));
const games = ref<GamesApiListEventGamesResponse | undefined>();
const loading = ref(true);
const { data: games, isPending: loading } = useQuery(eventGamesQuery(props.eventPath));
const hasSteamGames = computed(() => games.value?.some((game) => game.steam_app_id) ?? false);
const imageErrors = ref<Set<number>>(new Set());
@@ -40,15 +40,6 @@ const goToGame = (game: GamesApiListEventGamesResponse[number]) => {
router.push(`/${props.eventPath}${path}`);
};
onMounted(async () => {
const gamesResponse = await gamesApiListEventGames({
path: { event_path: props.eventPath },
});
if (gamesResponse.data) {
games.value = gamesResponse.data;
}
loading.value = false;
});
</script>
<template>
+3 -8
View File
@@ -9,10 +9,11 @@ import Results from "@/components/Results.vue";
import Winners from "@/components/Winners.vue";
import PuzzleResults from "@/components/PuzzleResults.vue";
import TopUsersLeaderboard from "@/components/TopUsersLeaderboard.vue";
import { polylanSubmitterApiGetUserInfo, opusMagnumApiGetCollection } from "@/api";
import { opusMagnumApiGetCollection } from "@/api";
import { apiService, errorHelpers } from "@/services/apiService";
import { usePuzzlesStore } from "@/stores/puzzles";
import { useSubmissionsStore } from "@/stores/submissions";
import { queryClient, userQuery } from "@/queries";
import { provideEventPath } from "@/composables/useEventPath";
import type { PuzzleResponse, UserInfo, SteamCollection } from "@/types";
import { useCountdown } from "@vueuse/core";
@@ -74,13 +75,7 @@ async function initialize() {
// Load user info
console.log("Loading user info...");
const userResponse = await polylanSubmitterApiGetUserInfo();
if (userResponse.data) {
userInfo.value = userResponse.data as UserInfo;
console.log("User info loaded:", userResponse.data);
} else if (userResponse.error) {
console.warn("User info error:", userResponse.error);
}
userInfo.value = (await queryClient.fetchQuery(userQuery()).catch(() => null)) as UserInfo | null;
// Load collection data
console.log("Loading collection...");
+9 -9
View File
@@ -1,12 +1,13 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from "vue";
import { useRouter } from "vue-router";
import { storeToRefs } from "pinia";
import { useI18n } from "vue-i18n";
import { useTitle } from "@vueuse/core";
import { useUserStore } from "@/stores/user";
import { useQueryClient } from "@tanstack/vue-query";
import { useUser } from "@/composables/useUser";
import { eventGamesQuery } from "@/queries";
import { usePageHeaderStore } from "@/stores/pageHeader";
import { gamesApiListEventGames, submissionsApiListAvailableScopes, submissionsApiCreateSubmission, submissionsApiListUserSubmissions, submissionsApiCreateImageCrop, submissionsApiDeleteImageCrop, submissionsApiProcessFileOcr, submissionsApiProcessCropOcr } from "@/api";
import { submissionsApiListAvailableScopes, submissionsApiCreateSubmission, submissionsApiListUserSubmissions, submissionsApiCreateImageCrop, submissionsApiDeleteImageCrop, submissionsApiProcessFileOcr, submissionsApiProcessCropOcr } from "@/api";
import { useSubmissionsWebSocket } from "@/composables/useSubmissionsWebSocket";
import { provideEventPath } from "@/composables/useEventPath";
@@ -31,8 +32,8 @@ const gameSteamId = ref("");
useTitle(computed(() => t("submissions.pageTitle", { game: gameName.value, scope: scopeName.value })));
const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore);
const queryClient = useQueryClient();
const userInfo = useUser();
const uploadedFiles = ref<File[]>([]);
const isDragover = ref(false);
@@ -503,17 +504,16 @@ const handlePasteOnUploadArea = (event: ClipboardEvent) => {
};
onMounted(async () => {
userStore.loadUserData();
loadUserSubmissions();
const [gamesResponse, scopesResponse] = await Promise.all([
gamesApiListEventGames({ path: { event_path: props.eventPath } }),
const [games, scopesResponse] = await Promise.all([
queryClient.ensureQueryData(eventGamesQuery(props.eventPath)),
submissionsApiListAvailableScopes({
path: { event_path: props.eventPath, game_path: props.gamePath },
}),
]);
const game = gamesResponse.data?.find((g) => g.path === props.gamePath);
const game = games.find((g) => g.path === props.gamePath);
if (game) {
gameName.value = game.name;
gameImage.value = game.image ?? "";
+7 -4
View File
@@ -3,7 +3,9 @@ import { computed, onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
import { useTitle } from "@vueuse/core";
import { gamesApiListEventGames, submissionsApiListAvailableScopes } from "@/api";
import { submissionsApiListAvailableScopes } from "@/api";
import { useQueryClient } from "@tanstack/vue-query";
import { eventGamesQuery } from "@/queries";
import type { ScopeOut } from "@/api/types.gen";
interface Props {
@@ -14,6 +16,7 @@ interface Props {
const props = defineProps<Props>();
const router = useRouter();
const queryClient = useQueryClient();
const { t } = useI18n();
useTitle(computed(() => t("submissionsSelect.title")));
@@ -32,14 +35,14 @@ const goToScope = (scope: ScopeOut) => {
};
onMounted(async () => {
const [gamesResponse, scopesResponse] = await Promise.all([
gamesApiListEventGames({ path: { event_path: props.eventPath } }),
const [games, scopesResponse] = await Promise.all([
queryClient.ensureQueryData(eventGamesQuery(props.eventPath)),
submissionsApiListAvailableScopes({
path: { event_path: props.eventPath, game_path: props.gamePath },
}),
]);
gameName.value = gamesResponse.data?.find((game) => game.path === props.gamePath)?.name ?? "";
gameName.value = games.find((game) => game.path === props.gamePath)?.name ?? "";
const availableScopes = scopesResponse.data ?? [];
@@ -0,0 +1,947 @@
// This file is auto-generated by @hey-api/openapi-ts
import { type DefaultError, type InfiniteData, infiniteQueryOptions, queryOptions, type UseMutationOptions } from '@tanstack/vue-query';
import { client } from '../client.gen';
import { animationsApiPuzzleResults, animationsApiResults, animationsApiTopSubmissions, clockApiCurrentState, clockApiMyStats, gamesApiListEventGames, gamesApiListEventSteamGames, gamesApiListGames, marketApiBetHistory, marketApiCloseMarket, marketApiCreateBet, marketApiGetUserPoints, marketApiListMarkets, marketApiListUserBets, marketApiResolveMarket, noitaApiGetLeaderboard, noitaApiGetResults, noitaApiSubmitLogFile, type Options, opusMagnumApiCreateSubmission, opusMagnumApiDeleteSubmission, opusMagnumApiGetCollection, opusMagnumApiGetStats, opusMagnumApiGetSubmission, opusMagnumApiListPuzzles, opusMagnumApiListResponsesNeedingValidation, opusMagnumApiListSubmissions, opusMagnumApiValidateAuto, opusMagnumApiValidateResponse, opusMagnumApiValidateSubmission, placeApiCanvas, placeApiEvolutionGif, polylanSubmitterApiClearCache, polylanSubmitterApiGetUserInfo, polylanSubmitterApiHealthCheck, submissionsApiCreateImageCrop, submissionsApiCreateSubmission, submissionsApiDeleteImageCrop, submissionsApiGetSubmission, submissionsApiListAvailableScopes, submissionsApiListMyScopes, submissionsApiListSubmissionsByGame, submissionsApiListUserSubmissions, submissionsApiProcessCropOcr, submissionsApiProcessFileOcr } from '../sdk.gen';
import type { AnimationsApiPuzzleResultsData, AnimationsApiPuzzleResultsResponse, AnimationsApiResultsData, AnimationsApiResultsResponse, AnimationsApiTopSubmissionsData, AnimationsApiTopSubmissionsResponse, ClockApiCurrentStateData, ClockApiCurrentStateResponse, ClockApiMyStatsData, ClockApiMyStatsResponse, GamesApiListEventGamesData, GamesApiListEventGamesResponse, GamesApiListEventSteamGamesData, GamesApiListEventSteamGamesResponse, GamesApiListGamesData, GamesApiListGamesResponse, MarketApiBetHistoryData, MarketApiBetHistoryResponse, MarketApiCloseMarketData, MarketApiCreateBetData, MarketApiCreateBetResponse, MarketApiGetUserPointsData, MarketApiGetUserPointsResponse, MarketApiListMarketsData, MarketApiListMarketsResponse, MarketApiListUserBetsData, MarketApiListUserBetsResponse, MarketApiResolveMarketData, MarketApiResolveMarketResponse, NoitaApiGetLeaderboardData, NoitaApiGetLeaderboardResponse, NoitaApiGetResultsData, NoitaApiGetResultsResponse, NoitaApiSubmitLogFileData, NoitaApiSubmitLogFileError, NoitaApiSubmitLogFileResponse, OpusMagnumApiCreateSubmissionData, OpusMagnumApiCreateSubmissionResponse, OpusMagnumApiDeleteSubmissionData, OpusMagnumApiGetCollectionData, OpusMagnumApiGetCollectionResponse, OpusMagnumApiGetStatsData, OpusMagnumApiGetSubmissionData, OpusMagnumApiGetSubmissionResponse, OpusMagnumApiListPuzzlesData, OpusMagnumApiListPuzzlesResponse, OpusMagnumApiListResponsesNeedingValidationData, OpusMagnumApiListResponsesNeedingValidationResponse, OpusMagnumApiListSubmissionsData, OpusMagnumApiListSubmissionsResponse, OpusMagnumApiValidateAutoData, OpusMagnumApiValidateAutoResponse, OpusMagnumApiValidateResponseData, OpusMagnumApiValidateResponseResponse, OpusMagnumApiValidateSubmissionData, OpusMagnumApiValidateSubmissionResponse, PlaceApiCanvasData, PlaceApiEvolutionGifData, PolylanSubmitterApiClearCacheData, PolylanSubmitterApiGetUserInfoData, PolylanSubmitterApiGetUserInfoResponse, PolylanSubmitterApiHealthCheckData, SubmissionsApiCreateImageCropData, SubmissionsApiCreateImageCropResponse, SubmissionsApiCreateSubmissionData, SubmissionsApiCreateSubmissionError, SubmissionsApiCreateSubmissionResponse, SubmissionsApiDeleteImageCropData, SubmissionsApiDeleteImageCropResponse, SubmissionsApiGetSubmissionData, SubmissionsApiGetSubmissionResponse, SubmissionsApiListAvailableScopesData, SubmissionsApiListAvailableScopesError, SubmissionsApiListAvailableScopesResponse, SubmissionsApiListMyScopesData, SubmissionsApiListMyScopesError, SubmissionsApiListMyScopesResponse, SubmissionsApiListSubmissionsByGameData, SubmissionsApiListSubmissionsByGameResponse, SubmissionsApiListUserSubmissionsData, SubmissionsApiListUserSubmissionsError, SubmissionsApiListUserSubmissionsResponse, SubmissionsApiProcessCropOcrData, SubmissionsApiProcessCropOcrResponse, SubmissionsApiProcessFileOcrData, SubmissionsApiProcessFileOcrResponse } from '../types.gen';
export type QueryKey<TOptions extends Options> = [
Pick<TOptions, 'baseUrl' | 'body' | 'headers' | 'path' | 'query'> & {
_id: string;
_infinite?: boolean;
tags?: ReadonlyArray<string>;
}
];
const createQueryKey = <TOptions extends Options>(id: string, options?: TOptions, infinite?: boolean, tags?: ReadonlyArray<string>): [
QueryKey<TOptions>[0]
] => {
const params: QueryKey<TOptions>[0] = { _id: id, baseUrl: options?.baseUrl || (options?.client ?? client).getConfig().baseUrl } as QueryKey<TOptions>[0];
if (infinite) {
params._infinite = infinite;
}
if (tags) {
params.tags = tags;
}
if (options?.body) {
params.body = options.body;
}
if (options?.headers) {
params.headers = options.headers;
}
if (options?.path) {
params.path = options.path;
}
if (options?.query) {
params.query = options.query;
}
return [params];
};
export const polylanSubmitterApiHealthCheckQueryKey = (options?: Options<PolylanSubmitterApiHealthCheckData>) => createQueryKey('polylanSubmitterApiHealthCheck', options);
/**
* Health Check
*
* Health check endpoint
*/
export const polylanSubmitterApiHealthCheckOptions = (options?: Options<PolylanSubmitterApiHealthCheckData>) => queryOptions<unknown, DefaultError, unknown, ReturnType<typeof polylanSubmitterApiHealthCheckQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await polylanSubmitterApiHealthCheck({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: polylanSubmitterApiHealthCheckQueryKey(options)
});
/**
* Clear Cache
*
* Clear API caches (admin only). Pass event_path to clear just that event's keys.
*/
export const polylanSubmitterApiClearCacheMutation = (options?: Partial<Options<PolylanSubmitterApiClearCacheData>>): UseMutationOptions<unknown, DefaultError, Options<PolylanSubmitterApiClearCacheData>> => {
const mutationOptions: UseMutationOptions<unknown, DefaultError, Options<PolylanSubmitterApiClearCacheData>> = {
mutationFn: async (fnOptions) => {
const { data } = await polylanSubmitterApiClearCache({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
export const polylanSubmitterApiGetUserInfoQueryKey = (options?: Options<PolylanSubmitterApiGetUserInfoData>) => createQueryKey('polylanSubmitterApiGetUserInfo', options);
/**
* Get User Info
*
* Get current user information
*/
export const polylanSubmitterApiGetUserInfoOptions = (options?: Options<PolylanSubmitterApiGetUserInfoData>) => queryOptions<PolylanSubmitterApiGetUserInfoResponse, DefaultError, PolylanSubmitterApiGetUserInfoResponse, ReturnType<typeof polylanSubmitterApiGetUserInfoQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await polylanSubmitterApiGetUserInfo({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: polylanSubmitterApiGetUserInfoQueryKey(options)
});
export const gamesApiListGamesQueryKey = (options?: Options<GamesApiListGamesData>) => createQueryKey('gamesApiListGames', options);
/**
* List Games
*/
export const gamesApiListGamesOptions = (options?: Options<GamesApiListGamesData>) => queryOptions<GamesApiListGamesResponse, DefaultError, GamesApiListGamesResponse, ReturnType<typeof gamesApiListGamesQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await gamesApiListGames({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: gamesApiListGamesQueryKey(options)
});
export const opusMagnumApiListPuzzlesQueryKey = (options: Options<OpusMagnumApiListPuzzlesData>) => createQueryKey('opusMagnumApiListPuzzles', options);
/**
* List Puzzles
*
* Get list of available puzzles
*/
export const opusMagnumApiListPuzzlesOptions = (options: Options<OpusMagnumApiListPuzzlesData>) => queryOptions<OpusMagnumApiListPuzzlesResponse, DefaultError, OpusMagnumApiListPuzzlesResponse, ReturnType<typeof opusMagnumApiListPuzzlesQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await opusMagnumApiListPuzzles({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: opusMagnumApiListPuzzlesQueryKey(options)
});
export const opusMagnumApiGetCollectionQueryKey = (options: Options<OpusMagnumApiGetCollectionData>) => createQueryKey('opusMagnumApiGetCollection', options);
/**
* Get Collection
*
* Get the active collection details
*/
export const opusMagnumApiGetCollectionOptions = (options: Options<OpusMagnumApiGetCollectionData>) => queryOptions<OpusMagnumApiGetCollectionResponse, DefaultError, OpusMagnumApiGetCollectionResponse, ReturnType<typeof opusMagnumApiGetCollectionQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await opusMagnumApiGetCollection({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: opusMagnumApiGetCollectionQueryKey(options)
});
export const opusMagnumApiListSubmissionsQueryKey = (options: Options<OpusMagnumApiListSubmissionsData>) => createQueryKey('opusMagnumApiListSubmissions', options);
/**
* List Submissions
*
* Get paginated list of submissions
*/
export const opusMagnumApiListSubmissionsOptions = (options: Options<OpusMagnumApiListSubmissionsData>) => queryOptions<OpusMagnumApiListSubmissionsResponse, DefaultError, OpusMagnumApiListSubmissionsResponse, ReturnType<typeof opusMagnumApiListSubmissionsQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await opusMagnumApiListSubmissions({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: opusMagnumApiListSubmissionsQueryKey(options)
});
const createInfiniteParams = <K extends Pick<QueryKey<Options>[0], 'body' | 'headers' | 'path' | 'query'>>(queryKey: QueryKey<Options>, page: K) => {
const params = { ...queryKey[0] };
if (page.body) {
params.body = {
...queryKey[0].body as any,
...page.body as any
};
}
if (page.headers) {
params.headers = {
...queryKey[0].headers,
...page.headers
};
}
if (page.path) {
params.path = {
...queryKey[0].path as any,
...page.path as any
};
}
if (page.query) {
params.query = {
...queryKey[0].query as any,
...page.query as any
};
}
return params as unknown as typeof page;
};
export const opusMagnumApiListSubmissionsInfiniteQueryKey = (options: Options<OpusMagnumApiListSubmissionsData>): QueryKey<Options<OpusMagnumApiListSubmissionsData>> => createQueryKey('opusMagnumApiListSubmissions', options, true);
/**
* List Submissions
*
* Get paginated list of submissions
*/
export const opusMagnumApiListSubmissionsInfiniteOptions = (options: Options<OpusMagnumApiListSubmissionsData>) => infiniteQueryOptions<OpusMagnumApiListSubmissionsResponse, DefaultError, InfiniteData<OpusMagnumApiListSubmissionsResponse>, QueryKey<Options<OpusMagnumApiListSubmissionsData>>, number | Pick<QueryKey<Options<OpusMagnumApiListSubmissionsData>>[0], 'body' | 'headers' | 'path' | 'query'>>(
// @ts-ignore
{
queryFn: async ({ pageParam, queryKey, signal }) => {
// @ts-ignore
const page: Pick<QueryKey<Options<OpusMagnumApiListSubmissionsData>>[0], 'body' | 'headers' | 'path' | 'query'> = typeof pageParam === 'object' ? pageParam : {
query: {
offset: pageParam
}
};
const params = createInfiniteParams(queryKey, page);
const { data } = await opusMagnumApiListSubmissions({
...options,
...params,
signal,
throwOnError: true
});
return data;
},
queryKey: opusMagnumApiListSubmissionsInfiniteQueryKey(options)
});
/**
* Create Submission
*
* Create a new submission with multiple puzzle responses
*/
export const opusMagnumApiCreateSubmissionMutation = (options?: Partial<Options<OpusMagnumApiCreateSubmissionData>>): UseMutationOptions<OpusMagnumApiCreateSubmissionResponse, DefaultError, Options<OpusMagnumApiCreateSubmissionData>> => {
const mutationOptions: UseMutationOptions<OpusMagnumApiCreateSubmissionResponse, DefaultError, Options<OpusMagnumApiCreateSubmissionData>> = {
mutationFn: async (fnOptions) => {
const { data } = await opusMagnumApiCreateSubmission({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
/**
* Delete Submission
*
* Delete a submission (admin only)
*/
export const opusMagnumApiDeleteSubmissionMutation = (options?: Partial<Options<OpusMagnumApiDeleteSubmissionData>>): UseMutationOptions<unknown, DefaultError, Options<OpusMagnumApiDeleteSubmissionData>> => {
const mutationOptions: UseMutationOptions<unknown, DefaultError, Options<OpusMagnumApiDeleteSubmissionData>> = {
mutationFn: async (fnOptions) => {
const { data } = await opusMagnumApiDeleteSubmission({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
export const opusMagnumApiGetSubmissionQueryKey = (options: Options<OpusMagnumApiGetSubmissionData>) => createQueryKey('opusMagnumApiGetSubmission', options);
/**
* Get Submission
*
* Get detailed submission by ID
*/
export const opusMagnumApiGetSubmissionOptions = (options: Options<OpusMagnumApiGetSubmissionData>) => queryOptions<OpusMagnumApiGetSubmissionResponse, DefaultError, OpusMagnumApiGetSubmissionResponse, ReturnType<typeof opusMagnumApiGetSubmissionQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await opusMagnumApiGetSubmission({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: opusMagnumApiGetSubmissionQueryKey(options)
});
/**
* Validate Response
*
* Manually validate a puzzle response
*/
export const opusMagnumApiValidateResponseMutation = (options?: Partial<Options<OpusMagnumApiValidateResponseData>>): UseMutationOptions<OpusMagnumApiValidateResponseResponse, DefaultError, Options<OpusMagnumApiValidateResponseData>> => {
const mutationOptions: UseMutationOptions<OpusMagnumApiValidateResponseResponse, DefaultError, Options<OpusMagnumApiValidateResponseData>> = {
mutationFn: async (fnOptions) => {
const { data } = await opusMagnumApiValidateResponse({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
/**
* Validate Auto
*
* Try to auto validate a puzzle response
*/
export const opusMagnumApiValidateAutoMutation = (options?: Partial<Options<OpusMagnumApiValidateAutoData>>): UseMutationOptions<OpusMagnumApiValidateAutoResponse, DefaultError, Options<OpusMagnumApiValidateAutoData>> => {
const mutationOptions: UseMutationOptions<OpusMagnumApiValidateAutoResponse, DefaultError, Options<OpusMagnumApiValidateAutoData>> = {
mutationFn: async (fnOptions) => {
const { data } = await opusMagnumApiValidateAuto({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
export const opusMagnumApiListResponsesNeedingValidationQueryKey = (options: Options<OpusMagnumApiListResponsesNeedingValidationData>) => createQueryKey('opusMagnumApiListResponsesNeedingValidation', options);
/**
* List Responses Needing Validation
*
* Get all responses that need manual validation
*/
export const opusMagnumApiListResponsesNeedingValidationOptions = (options: Options<OpusMagnumApiListResponsesNeedingValidationData>) => queryOptions<OpusMagnumApiListResponsesNeedingValidationResponse, DefaultError, OpusMagnumApiListResponsesNeedingValidationResponse, ReturnType<typeof opusMagnumApiListResponsesNeedingValidationQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await opusMagnumApiListResponsesNeedingValidation({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: opusMagnumApiListResponsesNeedingValidationQueryKey(options)
});
/**
* Validate Submission
*
* Mark entire submission as validated
*/
export const opusMagnumApiValidateSubmissionMutation = (options?: Partial<Options<OpusMagnumApiValidateSubmissionData>>): UseMutationOptions<OpusMagnumApiValidateSubmissionResponse, DefaultError, Options<OpusMagnumApiValidateSubmissionData>> => {
const mutationOptions: UseMutationOptions<OpusMagnumApiValidateSubmissionResponse, DefaultError, Options<OpusMagnumApiValidateSubmissionData>> = {
mutationFn: async (fnOptions) => {
const { data } = await opusMagnumApiValidateSubmission({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
export const opusMagnumApiGetStatsQueryKey = (options: Options<OpusMagnumApiGetStatsData>) => createQueryKey('opusMagnumApiGetStats', options);
/**
* Get Stats
*
* Get submission statistics
*/
export const opusMagnumApiGetStatsOptions = (options: Options<OpusMagnumApiGetStatsData>) => queryOptions<unknown, DefaultError, unknown, ReturnType<typeof opusMagnumApiGetStatsQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await opusMagnumApiGetStats({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: opusMagnumApiGetStatsQueryKey(options)
});
export const animationsApiResultsQueryKey = (options: Options<AnimationsApiResultsData>) => createQueryKey('animationsApiResults', options);
/**
* Results
*/
export const animationsApiResultsOptions = (options: Options<AnimationsApiResultsData>) => queryOptions<AnimationsApiResultsResponse, DefaultError, AnimationsApiResultsResponse, ReturnType<typeof animationsApiResultsQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await animationsApiResults({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: animationsApiResultsQueryKey(options)
});
export const animationsApiTopSubmissionsQueryKey = (options: Options<AnimationsApiTopSubmissionsData>) => createQueryKey('animationsApiTopSubmissions', options);
/**
* Top Submissions
*
* Get tournament top submissions for each puzzle. Only available when tournament is closed.
*/
export const animationsApiTopSubmissionsOptions = (options: Options<AnimationsApiTopSubmissionsData>) => queryOptions<AnimationsApiTopSubmissionsResponse, DefaultError, AnimationsApiTopSubmissionsResponse, ReturnType<typeof animationsApiTopSubmissionsQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await animationsApiTopSubmissions({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: animationsApiTopSubmissionsQueryKey(options)
});
export const animationsApiPuzzleResultsQueryKey = (options: Options<AnimationsApiPuzzleResultsData>) => createQueryKey('animationsApiPuzzleResults', options);
/**
* Puzzle Results
*
* Get tournament results organized by puzzle with coefficients. Only available when tournament is closed.
*/
export const animationsApiPuzzleResultsOptions = (options: Options<AnimationsApiPuzzleResultsData>) => queryOptions<AnimationsApiPuzzleResultsResponse, DefaultError, AnimationsApiPuzzleResultsResponse, ReturnType<typeof animationsApiPuzzleResultsQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await animationsApiPuzzleResults({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: animationsApiPuzzleResultsQueryKey(options)
});
export const noitaApiGetResultsQueryKey = (options: Options<NoitaApiGetResultsData>) => createQueryKey('noitaApiGetResults', options);
/**
* Get Results
*/
export const noitaApiGetResultsOptions = (options: Options<NoitaApiGetResultsData>) => queryOptions<NoitaApiGetResultsResponse, DefaultError, NoitaApiGetResultsResponse, ReturnType<typeof noitaApiGetResultsQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await noitaApiGetResults({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: noitaApiGetResultsQueryKey(options)
});
export const noitaApiGetLeaderboardQueryKey = (options: Options<NoitaApiGetLeaderboardData>) => createQueryKey('noitaApiGetLeaderboard', options);
/**
* Get Leaderboard
*
* Get the global leaderboard for all users ranked by total score.
*
* Uses Window functions to rank users by their total score in descending order.
*/
export const noitaApiGetLeaderboardOptions = (options: Options<NoitaApiGetLeaderboardData>) => queryOptions<NoitaApiGetLeaderboardResponse, DefaultError, NoitaApiGetLeaderboardResponse, ReturnType<typeof noitaApiGetLeaderboardQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await noitaApiGetLeaderboard({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: noitaApiGetLeaderboardQueryKey(options)
});
/**
* Submit Log File
*
* Submit a Noita run file (log file, screenshot, or video).
*
* Accepts:
* - Text files (.txt) for polylan_mod_log.txt
* - Images (.png, .jpg, .gif)
* - Videos (.mp4, .webm)
*
* Max file size: 256 MB
*/
export const noitaApiSubmitLogFileMutation = (options?: Partial<Options<NoitaApiSubmitLogFileData>>): UseMutationOptions<NoitaApiSubmitLogFileResponse, NoitaApiSubmitLogFileError, Options<NoitaApiSubmitLogFileData>> => {
const mutationOptions: UseMutationOptions<NoitaApiSubmitLogFileResponse, NoitaApiSubmitLogFileError, Options<NoitaApiSubmitLogFileData>> = {
mutationFn: async (fnOptions) => {
const { data } = await noitaApiSubmitLogFile({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
export const gamesApiListEventGamesQueryKey = (options: Options<GamesApiListEventGamesData>) => createQueryKey('gamesApiListEventGames', options);
/**
* List Event Games
*/
export const gamesApiListEventGamesOptions = (options: Options<GamesApiListEventGamesData>) => queryOptions<GamesApiListEventGamesResponse, DefaultError, GamesApiListEventGamesResponse, ReturnType<typeof gamesApiListEventGamesQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await gamesApiListEventGames({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: gamesApiListEventGamesQueryKey(options)
});
export const gamesApiListEventSteamGamesQueryKey = (options: Options<GamesApiListEventSteamGamesData>) => createQueryKey('gamesApiListEventSteamGames', options);
/**
* List Event Steam Games
*
* The event's Steam games with their store info (price, discount, blurb).
*/
export const gamesApiListEventSteamGamesOptions = (options: Options<GamesApiListEventSteamGamesData>) => queryOptions<GamesApiListEventSteamGamesResponse, DefaultError, GamesApiListEventSteamGamesResponse, ReturnType<typeof gamesApiListEventSteamGamesQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await gamesApiListEventSteamGames({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: gamesApiListEventSteamGamesQueryKey(options)
});
export const submissionsApiListMyScopesQueryKey = (options: Options<SubmissionsApiListMyScopesData>) => createQueryKey('submissionsApiListMyScopes', options);
/**
* List My Scopes
*/
export const submissionsApiListMyScopesOptions = (options: Options<SubmissionsApiListMyScopesData>) => queryOptions<SubmissionsApiListMyScopesResponse, SubmissionsApiListMyScopesError, SubmissionsApiListMyScopesResponse, ReturnType<typeof submissionsApiListMyScopesQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await submissionsApiListMyScopes({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: submissionsApiListMyScopesQueryKey(options)
});
export const submissionsApiListAvailableScopesQueryKey = (options: Options<SubmissionsApiListAvailableScopesData>) => createQueryKey('submissionsApiListAvailableScopes', options);
/**
* List Available Scopes
*
* Scopes the current user may submit to for this game - superusers see
* every active scope, everyone else only the ones assigned to them. Used
* by the submissions-select and submissions-detail pages, both to list
* scopes and (for the detail page) to confirm the requested scope_id is
* one the current user is actually allowed to see.
*/
export const submissionsApiListAvailableScopesOptions = (options: Options<SubmissionsApiListAvailableScopesData>) => queryOptions<SubmissionsApiListAvailableScopesResponse, SubmissionsApiListAvailableScopesError, SubmissionsApiListAvailableScopesResponse, ReturnType<typeof submissionsApiListAvailableScopesQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await submissionsApiListAvailableScopes({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: submissionsApiListAvailableScopesQueryKey(options)
});
/**
* Create Submission
*/
export const submissionsApiCreateSubmissionMutation = (options?: Partial<Options<SubmissionsApiCreateSubmissionData>>): UseMutationOptions<SubmissionsApiCreateSubmissionResponse, SubmissionsApiCreateSubmissionError, Options<SubmissionsApiCreateSubmissionData>> => {
const mutationOptions: UseMutationOptions<SubmissionsApiCreateSubmissionResponse, SubmissionsApiCreateSubmissionError, Options<SubmissionsApiCreateSubmissionData>> = {
mutationFn: async (fnOptions) => {
const { data } = await submissionsApiCreateSubmission({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
export const submissionsApiListUserSubmissionsQueryKey = (options: Options<SubmissionsApiListUserSubmissionsData>) => createQueryKey('submissionsApiListUserSubmissions', options);
/**
* List User Submissions
*/
export const submissionsApiListUserSubmissionsOptions = (options: Options<SubmissionsApiListUserSubmissionsData>) => queryOptions<SubmissionsApiListUserSubmissionsResponse, SubmissionsApiListUserSubmissionsError, SubmissionsApiListUserSubmissionsResponse, ReturnType<typeof submissionsApiListUserSubmissionsQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await submissionsApiListUserSubmissions({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: submissionsApiListUserSubmissionsQueryKey(options)
});
export const submissionsApiListSubmissionsByGameQueryKey = (options: Options<SubmissionsApiListSubmissionsByGameData>) => createQueryKey('submissionsApiListSubmissionsByGame', options);
/**
* List Submissions By Game
*/
export const submissionsApiListSubmissionsByGameOptions = (options: Options<SubmissionsApiListSubmissionsByGameData>) => queryOptions<SubmissionsApiListSubmissionsByGameResponse, DefaultError, SubmissionsApiListSubmissionsByGameResponse, ReturnType<typeof submissionsApiListSubmissionsByGameQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await submissionsApiListSubmissionsByGame({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: submissionsApiListSubmissionsByGameQueryKey(options)
});
export const submissionsApiGetSubmissionQueryKey = (options: Options<SubmissionsApiGetSubmissionData>) => createQueryKey('submissionsApiGetSubmission', options);
/**
* Get Submission
*/
export const submissionsApiGetSubmissionOptions = (options: Options<SubmissionsApiGetSubmissionData>) => queryOptions<SubmissionsApiGetSubmissionResponse, DefaultError, SubmissionsApiGetSubmissionResponse, ReturnType<typeof submissionsApiGetSubmissionQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await submissionsApiGetSubmission({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: submissionsApiGetSubmissionQueryKey(options)
});
/**
* Create Image Crop
*/
export const submissionsApiCreateImageCropMutation = (options?: Partial<Options<SubmissionsApiCreateImageCropData>>): UseMutationOptions<SubmissionsApiCreateImageCropResponse, DefaultError, Options<SubmissionsApiCreateImageCropData>> => {
const mutationOptions: UseMutationOptions<SubmissionsApiCreateImageCropResponse, DefaultError, Options<SubmissionsApiCreateImageCropData>> = {
mutationFn: async (fnOptions) => {
const { data } = await submissionsApiCreateImageCrop({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
/**
* Delete Image Crop
*/
export const submissionsApiDeleteImageCropMutation = (options?: Partial<Options<SubmissionsApiDeleteImageCropData>>): UseMutationOptions<SubmissionsApiDeleteImageCropResponse, DefaultError, Options<SubmissionsApiDeleteImageCropData>> => {
const mutationOptions: UseMutationOptions<SubmissionsApiDeleteImageCropResponse, DefaultError, Options<SubmissionsApiDeleteImageCropData>> = {
mutationFn: async (fnOptions) => {
const { data } = await submissionsApiDeleteImageCrop({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
/**
* Process File Ocr
*/
export const submissionsApiProcessFileOcrMutation = (options?: Partial<Options<SubmissionsApiProcessFileOcrData>>): UseMutationOptions<SubmissionsApiProcessFileOcrResponse, DefaultError, Options<SubmissionsApiProcessFileOcrData>> => {
const mutationOptions: UseMutationOptions<SubmissionsApiProcessFileOcrResponse, DefaultError, Options<SubmissionsApiProcessFileOcrData>> = {
mutationFn: async (fnOptions) => {
const { data } = await submissionsApiProcessFileOcr({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
/**
* Process Crop Ocr
*/
export const submissionsApiProcessCropOcrMutation = (options?: Partial<Options<SubmissionsApiProcessCropOcrData>>): UseMutationOptions<SubmissionsApiProcessCropOcrResponse, DefaultError, Options<SubmissionsApiProcessCropOcrData>> => {
const mutationOptions: UseMutationOptions<SubmissionsApiProcessCropOcrResponse, DefaultError, Options<SubmissionsApiProcessCropOcrData>> = {
mutationFn: async (fnOptions) => {
const { data } = await submissionsApiProcessCropOcr({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
export const clockApiCurrentStateQueryKey = (options: Options<ClockApiCurrentStateData>) => createQueryKey('clockApiCurrentState', options);
/**
* Current State
*/
export const clockApiCurrentStateOptions = (options: Options<ClockApiCurrentStateData>) => queryOptions<ClockApiCurrentStateResponse, DefaultError, ClockApiCurrentStateResponse, ReturnType<typeof clockApiCurrentStateQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await clockApiCurrentState({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: clockApiCurrentStateQueryKey(options)
});
export const clockApiMyStatsQueryKey = (options: Options<ClockApiMyStatsData>) => createQueryKey('clockApiMyStats', options);
/**
* My Stats
*/
export const clockApiMyStatsOptions = (options: Options<ClockApiMyStatsData>) => queryOptions<ClockApiMyStatsResponse, DefaultError, ClockApiMyStatsResponse, ReturnType<typeof clockApiMyStatsQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await clockApiMyStats({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: clockApiMyStatsQueryKey(options)
});
export const placeApiCanvasQueryKey = (options: Options<PlaceApiCanvasData>) => createQueryKey('placeApiCanvas', options);
/**
* Canvas
*
* The one-time full-grid load. Every placement after that arrives as a
* single websocket message, never a re-fetch.
*/
export const placeApiCanvasOptions = (options: Options<PlaceApiCanvasData>) => queryOptions<unknown, DefaultError, unknown, ReturnType<typeof placeApiCanvasQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await placeApiCanvas({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: placeApiCanvasQueryKey(options)
});
export const placeApiEvolutionGifQueryKey = (options: Options<PlaceApiEvolutionGifData>) => createQueryKey('placeApiEvolutionGif', options);
/**
* Evolution Gif
*
* Admin-only: replays the full placement history as an animated GIF.
*/
export const placeApiEvolutionGifOptions = (options: Options<PlaceApiEvolutionGifData>) => queryOptions<unknown, DefaultError, unknown, ReturnType<typeof placeApiEvolutionGifQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await placeApiEvolutionGif({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: placeApiEvolutionGifQueryKey(options)
});
export const marketApiListMarketsQueryKey = (options: Options<MarketApiListMarketsData>) => createQueryKey('marketApiListMarkets', options);
/**
* List Markets
*
* List all markets (excludes draft markets).
*/
export const marketApiListMarketsOptions = (options: Options<MarketApiListMarketsData>) => queryOptions<MarketApiListMarketsResponse, DefaultError, MarketApiListMarketsResponse, ReturnType<typeof marketApiListMarketsQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await marketApiListMarkets({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: marketApiListMarketsQueryKey(options)
});
export const marketApiListUserBetsQueryKey = (options: Options<MarketApiListUserBetsData>) => createQueryKey('marketApiListUserBets', options);
/**
* List User Bets
*
* List all bets placed by the current user.
*/
export const marketApiListUserBetsOptions = (options: Options<MarketApiListUserBetsData>) => queryOptions<MarketApiListUserBetsResponse, DefaultError, MarketApiListUserBetsResponse, ReturnType<typeof marketApiListUserBetsQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await marketApiListUserBets({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: marketApiListUserBetsQueryKey(options)
});
export const marketApiGetUserPointsQueryKey = (options: Options<MarketApiGetUserPointsData>) => createQueryKey('marketApiGetUserPoints', options);
/**
* Get User Points
*
* Get the current user's points balance for this event.
*/
export const marketApiGetUserPointsOptions = (options: Options<MarketApiGetUserPointsData>) => queryOptions<MarketApiGetUserPointsResponse, DefaultError, MarketApiGetUserPointsResponse, ReturnType<typeof marketApiGetUserPointsQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await marketApiGetUserPoints({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: marketApiGetUserPointsQueryKey(options)
});
export const marketApiBetHistoryQueryKey = (options: Options<MarketApiBetHistoryData>) => createQueryKey('marketApiBetHistory', options);
/**
* Bet History
*
* Points added to each option over time (anonymous, one entry per bet placed/increased).
*/
export const marketApiBetHistoryOptions = (options: Options<MarketApiBetHistoryData>) => queryOptions<MarketApiBetHistoryResponse, DefaultError, MarketApiBetHistoryResponse, ReturnType<typeof marketApiBetHistoryQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await marketApiBetHistory({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: marketApiBetHistoryQueryKey(options)
});
/**
* Close Market
*
* Close a market. Admin only.
*/
export const marketApiCloseMarketMutation = (options?: Partial<Options<MarketApiCloseMarketData>>): UseMutationOptions<unknown, DefaultError, Options<MarketApiCloseMarketData>> => {
const mutationOptions: UseMutationOptions<unknown, DefaultError, Options<MarketApiCloseMarketData>> = {
mutationFn: async (fnOptions) => {
const { data } = await marketApiCloseMarket({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
/**
* Resolve Market
*
* Resolve a market with a winning option. Admin only.
*/
export const marketApiResolveMarketMutation = (options?: Partial<Options<MarketApiResolveMarketData>>): UseMutationOptions<MarketApiResolveMarketResponse, DefaultError, Options<MarketApiResolveMarketData>> => {
const mutationOptions: UseMutationOptions<MarketApiResolveMarketResponse, DefaultError, Options<MarketApiResolveMarketData>> = {
mutationFn: async (fnOptions) => {
const { data } = await marketApiResolveMarket({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
/**
* Create Bet
*
* Place a bet on a market option.
*/
export const marketApiCreateBetMutation = (options?: Partial<Options<MarketApiCreateBetData>>): UseMutationOptions<MarketApiCreateBetResponse, DefaultError, Options<MarketApiCreateBetData>> => {
const mutationOptions: UseMutationOptions<MarketApiCreateBetResponse, DefaultError, Options<MarketApiCreateBetData>> = {
mutationFn: async (fnOptions) => {
const { data } = await marketApiCreateBet({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
+3 -3
View File
@@ -2,7 +2,8 @@
import { ref, onMounted } from "vue";
import { useI18n } from "vue-i18n";
import RankBadge from "./RankBadge.vue";
import { animationsApiResults, polylanSubmitterApiClearCache, polylanSubmitterApiGetUserInfo } from "@/api";
import { animationsApiResults, polylanSubmitterApiClearCache } from "@/api";
import { queryClient, userQuery } from "@/queries";
import { useEventPath } from "@/composables/useEventPath";
const { t } = useI18n();
@@ -142,8 +143,7 @@ const clearCache = async () => {
const loadUserData = async () => {
try {
const response = await polylanSubmitterApiGetUserInfo();
const user = response.data;
const user = await queryClient.fetchQuery(userQuery());
if (user?.is_authenticated) {
userInfo.value.username = user.username ?? "";
userInfo.value.isStaff = user.is_staff || false;
@@ -0,0 +1,13 @@
import { computed } from "vue";
import { useQuery } from "@tanstack/vue-query";
import { userQuery } from "@/queries";
export const useUser = () => {
const { data } = useQuery(userQuery());
return computed(() => ({
username: data.value?.username ?? "",
isAuthenticated: data.value?.is_authenticated ?? false,
isStaff: data.value?.is_staff ?? false,
}));
};
+2 -3
View File
@@ -3,14 +3,13 @@ import { computed } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
import { storeToRefs } from "pinia";
import { useUserStore } from "@/stores/user";
import { useUser } from "@/composables/useUser";
import { usePageHeaderStore } from "@/stores/pageHeader";
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore);
const userInfo = useUser();
const pageHeaderStore = usePageHeaderStore();
const { title: dynamicTitle, backRoute: dynamicBackRoute } = storeToRefs(pageHeaderStore);
+7
View File
@@ -1,12 +1,19 @@
import { createApp } from "vue";
import { VueQueryPlugin } from "@tanstack/vue-query";
import App from "@/App.vue";
import { router } from "@/router";
import { pinia } from "@/stores";
import { i18n } from "@/i18n";
import { onQueryCacheRestored, queryClient, restoreQueryCache } from "@/queries";
import "@/style.css";
const app = createApp(App);
app.use(pinia);
app.use(VueQueryPlugin, {
queryClient,
clientPersister: restoreQueryCache,
clientPersisterOnSuccess: onQueryCacheRestored,
});
app.use(router);
app.use(i18n);
app.mount("#app");
+45
View File
@@ -0,0 +1,45 @@
import { QueryClient } from "@tanstack/vue-query";
import { persistQueryClient } from "@tanstack/query-persist-client-core";
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister";
import {
gamesApiListEventGamesOptions,
polylanSubmitterApiGetUserInfoOptions,
polylanSubmitterApiGetUserInfoQueryKey,
} from "@/api/@tanstack/vue-query.gen";
// Exported (not just provided by VueQueryPlugin) so Pinia stores can
// fetchQuery() through the same cache.
export const queryClient = new QueryClient();
// Login/logout are full-page redirects, so the user can't change within a
// page load - only refetched once after a reload (see restoreQueryCache).
export const userQuery = () => ({
...polylanSubmitterApiGetUserInfoOptions(),
staleTime: Infinity,
});
// Games only change when staff enable/disable one, a few minutes of
// staleness is fine.
export const eventGamesQuery = (eventPath: string) => ({
...gamesApiListEventGamesOptions({ path: { event_path: eventPath } }),
staleTime: 5 * 60 * 1000,
});
const PERSISTED_QUERIES = new Set(["polylanSubmitterApiGetUserInfo", "gamesApiListEventGames"]);
// Restores user + games from localStorage before the app mounts, so pages
// render right away on reload instead of waiting on the API.
export const restoreQueryCache = (client: QueryClient) =>
persistQueryClient({
queryClient: client,
persister: createSyncStoragePersister({ storage: window.localStorage }),
dehydrateOptions: {
shouldDehydrateQuery: (query) =>
query.state.status === "success" &&
PERSISTED_QUERIES.has((query.queryKey[0] as { _id: string })._id),
},
});
// The restored user is only a placeholder until the real one comes back.
export const onQueryCacheRestored = (client: QueryClient) =>
client.invalidateQueries({ queryKey: polylanSubmitterApiGetUserInfoQueryKey() });
+3 -10
View File
@@ -1,7 +1,8 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { polylanSubmitterApiGetUserInfo, marketApiListMarkets, marketApiListUserBets, marketApiGetUserPoints } from '@/api'
import { marketApiListMarkets, marketApiListUserBets, marketApiGetUserPoints } from '@/api'
import type { Market } from '@/types'
import { queryClient, userQuery } from '@/queries'
import type { UserInfoOut, UserBetSchema } from '@/api/types.gen'
export const useMarketStore = defineStore('market', () => {
@@ -15,15 +16,7 @@ export const useMarketStore = defineStore('market', () => {
// Actions
const loadUserInfo = async () => {
try {
const response = await polylanSubmitterApiGetUserInfo()
if (response.data) {
userInfo.value = response.data
}
} catch (err) {
error.value = 'Failed to load user info'
console.error('Error loading user info:', err)
}
userInfo.value = await queryClient.fetchQuery(userQuery()).catch(() => undefined)
}
const loadMarkets = async (eventPath: string) => {
+5 -9
View File
@@ -4,10 +4,10 @@ import {
noitaApiGetResults,
noitaApiGetLeaderboard,
noitaApiSubmitLogFile,
polylanSubmitterApiGetUserInfo,
polylanSubmitterApiClearCache,
} from '@/api'
import type { ObjectivResultOut, LeaderboardEntryOut } from '@/api/types.gen'
import { queryClient, userQuery } from '@/queries'
export type Objective = ObjectivResultOut
@@ -80,14 +80,10 @@ export const useNoitaStore = defineStore('noita', () => {
}
const loadUserData = async (eventPath: string) => {
try {
const response = await polylanSubmitterApiGetUserInfo()
if (response.data?.is_authenticated) {
userInfo.value.username = response.data.username ?? ''
userInfo.value.isStaff = response.data.is_staff || false
}
} catch (err) {
console.error('Error fetching user info:', err)
const user = await queryClient.fetchQuery(userQuery()).catch(() => undefined)
if (user?.is_authenticated) {
userInfo.value.username = user.username ?? ''
userInfo.value.isStaff = user.is_staff || false
}
await Promise.all([fetchUserResults(eventPath), fetchLeaderboard(eventPath)])
-44
View File
@@ -1,44 +0,0 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { polylanSubmitterApiGetUserInfo } from '@/api'
interface UserInfo {
username: string
isAuthenticated: boolean
isStaff: boolean
}
export const useUserStore = defineStore('user', () => {
const userInfo = ref<UserInfo>({
username: '',
isAuthenticated: false,
isStaff: false,
})
const isLoading = ref(false)
const error = ref<string>('')
const loadUserData = async () => {
isLoading.value = true
try {
const response = await polylanSubmitterApiGetUserInfo()
if (response.data) {
const user = response.data
userInfo.value.isAuthenticated = user.is_authenticated || false
userInfo.value.username = user.username || ''
userInfo.value.isStaff = user.is_staff || false
}
} catch (err) {
error.value = 'Failed to fetch user info'
console.error('Error fetching user info:', err)
} finally {
isLoading.value = false
}
}
return {
userInfo,
isLoading,
error,
loadUserData,
}
})