diff --git a/polylan_submitter/market/api.py b/polylan_submitter/market/api.py index d746471..a344b4a 100644 --- a/polylan_submitter/market/api.py +++ b/polylan_submitter/market/api.py @@ -14,6 +14,7 @@ from market.models import ( get_event_points, ) from market.schemas import ( + BetHistoryPointSchema, MarketListSchema, ResolveMarketSchema, UserBetCreateSchema, @@ -61,6 +62,36 @@ def get_user_points(request, event_path: str): return {"points": get_event_points(request.user, request.event)} +@router.get(f"{PREFIX}/{{market_uuid}}/history", response=List[BetHistoryPointSchema]) +def bet_history(request, event_path: str, market_uuid: str): + """Points added to each option over time (anonymous, one entry per bet placed/increased).""" + market = get_object_or_404( + Market.objects.exclude(status=Market.Status.DRAFT), + uuid=market_uuid, + event=request.event, + ) + + # A user can only bet on one option per market, so their option is unique + option_by_user = dict( + UserBet.objects.filter(option__market=market).values_list( + "user_id", "option__uuid" + ) + ) + changes = UserPointChange.objects.filter( + market=market, reason=UserPointChange.Reason.BET_PLACED + ).order_by("created_at") + + return [ + { + "option_uuid": str(option_by_user[change.user_id]), + "created_at": change.created_at, + "amount": -change.amount, + } + for change in changes + if change.user_id in option_by_user + ] + + @router.post(f"{PREFIX}/{{market_uuid}}/actions/close") def close_market(request, event_path: str, market_uuid: str): """Close a market. Admin only.""" diff --git a/polylan_submitter/market/schemas.py b/polylan_submitter/market/schemas.py index 9248745..893779e 100644 --- a/polylan_submitter/market/schemas.py +++ b/polylan_submitter/market/schemas.py @@ -39,6 +39,12 @@ class UserPointsSchema(Schema): points: int +class BetHistoryPointSchema(Schema): + option_uuid: str + created_at: datetime + amount: int + + class UserBetCreateSchema(Schema): option_uuid: str amount: int diff --git a/polylan_submitter/src/api/index.ts b/polylan_submitter/src/api/index.ts index 65a3e46..4624e03 100644 --- a/polylan_submitter/src/api/index.ts +++ b/polylan_submitter/src/api/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts -export { animationsApiPuzzleResults, animationsApiResults, animationsApiTopSubmissions, clockApiCurrentState, clockApiMyStats, gamesApiListEventGames, gamesApiListGames, 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, polylanSubmitterApiGetConfig, polylanSubmitterApiGetUserInfo, polylanSubmitterApiHealthCheck, submissionsApiCreateImageCrop, submissionsApiCreateSubmission, submissionsApiDeleteImageCrop, submissionsApiGetSubmission, submissionsApiListAvailableScopes, submissionsApiListMyScopes, submissionsApiListSubmissionsByGame, submissionsApiListUserSubmissions, submissionsApiProcessCropOcr, submissionsApiProcessFileOcr } from './sdk.gen'; -export type { AnimationsApiPuzzleResultsData, AnimationsApiPuzzleResultsResponse, AnimationsApiPuzzleResultsResponses, AnimationsApiResultsData, AnimationsApiResultsResponse, AnimationsApiResultsResponses, AnimationsApiTopSubmissionsData, AnimationsApiTopSubmissionsResponse, AnimationsApiTopSubmissionsResponses, ClientOptions, ClockApiCurrentStateData, ClockApiCurrentStateResponse, ClockApiCurrentStateResponses, ClockApiMyStatsData, ClockApiMyStatsResponse, ClockApiMyStatsResponses, ClockStateOut, ConfigOut, GameOut, GamesApiListEventGamesData, GamesApiListEventGamesResponse, GamesApiListEventGamesResponses, GamesApiListGamesData, GamesApiListGamesResponse, GamesApiListGamesResponses, ImageCropIn, ImageCropOut, Input, LeaderboardEntryOut, LeaderboardOut, MarketApiCloseMarketData, MarketApiCloseMarketResponses, MarketApiCreateBetData, MarketApiCreateBetResponse, MarketApiCreateBetResponses, MarketApiGetUserPointsData, MarketApiGetUserPointsResponse, MarketApiGetUserPointsResponses, MarketApiListMarketsData, MarketApiListMarketsResponse, MarketApiListMarketsResponses, MarketApiListUserBetsData, MarketApiListUserBetsResponse, MarketApiListUserBetsResponses, MarketApiResolveMarketData, MarketApiResolveMarketResponse, MarketApiResolveMarketResponses, MarketListSchema, MarketOptionSchema, NoitaApiGetLeaderboardData, NoitaApiGetLeaderboardResponse, NoitaApiGetLeaderboardResponses, NoitaApiGetResultsData, NoitaApiGetResultsResponse, NoitaApiGetResultsResponses, NoitaApiSubmitLogFileData, NoitaApiSubmitLogFileError, NoitaApiSubmitLogFileErrors, NoitaApiSubmitLogFileResponse, NoitaApiSubmitLogFileResponses, NoitaSubmissionOut, ObjectivResultOut, OpusMagnumApiCreateSubmissionData, OpusMagnumApiCreateSubmissionResponse, OpusMagnumApiCreateSubmissionResponses, OpusMagnumApiDeleteSubmissionData, OpusMagnumApiDeleteSubmissionResponses, OpusMagnumApiGetCollectionData, OpusMagnumApiGetCollectionResponse, OpusMagnumApiGetCollectionResponses, OpusMagnumApiGetStatsData, OpusMagnumApiGetStatsResponses, OpusMagnumApiGetSubmissionData, OpusMagnumApiGetSubmissionResponse, OpusMagnumApiGetSubmissionResponses, OpusMagnumApiListPuzzlesData, OpusMagnumApiListPuzzlesResponse, OpusMagnumApiListPuzzlesResponses, OpusMagnumApiListResponsesNeedingValidationData, OpusMagnumApiListResponsesNeedingValidationResponse, OpusMagnumApiListResponsesNeedingValidationResponses, OpusMagnumApiListSubmissionsData, OpusMagnumApiListSubmissionsResponse, OpusMagnumApiListSubmissionsResponses, OpusMagnumApiValidateAutoData, OpusMagnumApiValidateAutoResponse, OpusMagnumApiValidateAutoResponses, OpusMagnumApiValidateResponseData, OpusMagnumApiValidateResponseResponse, OpusMagnumApiValidateResponseResponses, OpusMagnumApiValidateSubmissionData, OpusMagnumApiValidateSubmissionResponse, OpusMagnumApiValidateSubmissionResponses, PagedSubmissionOut, PlaceApiCanvasData, PlaceApiCanvasResponses, PlaceApiEvolutionGifData, PlaceApiEvolutionGifResponses, PlayerStatsOut, PolylanSubmitterApiClearCacheData, PolylanSubmitterApiClearCacheResponses, PolylanSubmitterApiGetConfigData, PolylanSubmitterApiGetConfigResponse, PolylanSubmitterApiGetConfigResponses, PolylanSubmitterApiGetUserInfoData, PolylanSubmitterApiGetUserInfoResponse, PolylanSubmitterApiGetUserInfoResponses, PolylanSubmitterApiHealthCheckData, PolylanSubmitterApiHealthCheckResponses, PuzzlePointsFactorOut, PuzzleResponseIn, PuzzleResponseOut, PuzzleResponseRankingOut, PuzzleResultsOut, PuzzleSubmissionsOut, PuzzleSubmissionWithRankOut, RankingSchema, ResolveMarketSchema, ResultsOut, ScopeOut, SteamCollectionItemOut, SteamCollectionOut, SubmissionFileOut, SubmissionIn, SubmissionOut, SubmissionsApiCreateImageCropData, SubmissionsApiCreateImageCropResponse, SubmissionsApiCreateImageCropResponses, SubmissionsApiCreateSubmissionData, SubmissionsApiCreateSubmissionError, SubmissionsApiCreateSubmissionErrors, SubmissionsApiCreateSubmissionResponse, SubmissionsApiCreateSubmissionResponses, SubmissionsApiDeleteImageCropData, SubmissionsApiDeleteImageCropResponse, SubmissionsApiDeleteImageCropResponses, SubmissionsApiGetSubmissionData, SubmissionsApiGetSubmissionResponse, SubmissionsApiGetSubmissionResponses, SubmissionsApiListAvailableScopesData, SubmissionsApiListAvailableScopesError, SubmissionsApiListAvailableScopesErrors, SubmissionsApiListAvailableScopesResponse, SubmissionsApiListAvailableScopesResponses, SubmissionsApiListMyScopesData, SubmissionsApiListMyScopesError, SubmissionsApiListMyScopesErrors, SubmissionsApiListMyScopesResponse, SubmissionsApiListMyScopesResponses, SubmissionsApiListSubmissionsByGameData, SubmissionsApiListSubmissionsByGameResponse, SubmissionsApiListSubmissionsByGameResponses, SubmissionsApiListUserSubmissionsData, SubmissionsApiListUserSubmissionsError, SubmissionsApiListUserSubmissionsErrors, SubmissionsApiListUserSubmissionsResponse, SubmissionsApiListUserSubmissionsResponses, SubmissionsApiProcessCropOcrData, SubmissionsApiProcessCropOcrResponse, SubmissionsApiProcessCropOcrResponses, SubmissionsApiProcessFileOcrData, SubmissionsApiProcessFileOcrResponse, SubmissionsApiProcessFileOcrResponses, TournamentPuzzleResultsOut, TournamentSubmissionsOut, UserBetCreateSchema, UserBetSchema, UserDisplayOut, UserInfoOut, UserPointsSchema, ValidationIn, WinnerFileOut, WinnerResponseOut } from './types.gen'; +export { animationsApiPuzzleResults, animationsApiResults, animationsApiTopSubmissions, clockApiCurrentState, clockApiMyStats, gamesApiListEventGames, 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, polylanSubmitterApiGetConfig, polylanSubmitterApiGetUserInfo, polylanSubmitterApiHealthCheck, submissionsApiCreateImageCrop, submissionsApiCreateSubmission, submissionsApiDeleteImageCrop, submissionsApiGetSubmission, submissionsApiListAvailableScopes, submissionsApiListMyScopes, submissionsApiListSubmissionsByGame, submissionsApiListUserSubmissions, submissionsApiProcessCropOcr, submissionsApiProcessFileOcr } from './sdk.gen'; +export type { AnimationsApiPuzzleResultsData, AnimationsApiPuzzleResultsResponse, AnimationsApiPuzzleResultsResponses, AnimationsApiResultsData, AnimationsApiResultsResponse, AnimationsApiResultsResponses, AnimationsApiTopSubmissionsData, AnimationsApiTopSubmissionsResponse, AnimationsApiTopSubmissionsResponses, BetHistoryPointSchema, ClientOptions, ClockApiCurrentStateData, ClockApiCurrentStateResponse, ClockApiCurrentStateResponses, ClockApiMyStatsData, ClockApiMyStatsResponse, ClockApiMyStatsResponses, ClockStateOut, ConfigOut, GameOut, GamesApiListEventGamesData, GamesApiListEventGamesResponse, GamesApiListEventGamesResponses, GamesApiListGamesData, GamesApiListGamesResponse, GamesApiListGamesResponses, ImageCropIn, ImageCropOut, Input, LeaderboardEntryOut, LeaderboardOut, MarketApiBetHistoryData, MarketApiBetHistoryResponse, MarketApiBetHistoryResponses, MarketApiCloseMarketData, MarketApiCloseMarketResponses, MarketApiCreateBetData, MarketApiCreateBetResponse, MarketApiCreateBetResponses, MarketApiGetUserPointsData, MarketApiGetUserPointsResponse, MarketApiGetUserPointsResponses, MarketApiListMarketsData, MarketApiListMarketsResponse, MarketApiListMarketsResponses, MarketApiListUserBetsData, MarketApiListUserBetsResponse, MarketApiListUserBetsResponses, MarketApiResolveMarketData, MarketApiResolveMarketResponse, MarketApiResolveMarketResponses, MarketListSchema, MarketOptionSchema, NoitaApiGetLeaderboardData, NoitaApiGetLeaderboardResponse, NoitaApiGetLeaderboardResponses, NoitaApiGetResultsData, NoitaApiGetResultsResponse, NoitaApiGetResultsResponses, NoitaApiSubmitLogFileData, NoitaApiSubmitLogFileError, NoitaApiSubmitLogFileErrors, NoitaApiSubmitLogFileResponse, NoitaApiSubmitLogFileResponses, NoitaSubmissionOut, ObjectivResultOut, OpusMagnumApiCreateSubmissionData, OpusMagnumApiCreateSubmissionResponse, OpusMagnumApiCreateSubmissionResponses, OpusMagnumApiDeleteSubmissionData, OpusMagnumApiDeleteSubmissionResponses, OpusMagnumApiGetCollectionData, OpusMagnumApiGetCollectionResponse, OpusMagnumApiGetCollectionResponses, OpusMagnumApiGetStatsData, OpusMagnumApiGetStatsResponses, OpusMagnumApiGetSubmissionData, OpusMagnumApiGetSubmissionResponse, OpusMagnumApiGetSubmissionResponses, OpusMagnumApiListPuzzlesData, OpusMagnumApiListPuzzlesResponse, OpusMagnumApiListPuzzlesResponses, OpusMagnumApiListResponsesNeedingValidationData, OpusMagnumApiListResponsesNeedingValidationResponse, OpusMagnumApiListResponsesNeedingValidationResponses, OpusMagnumApiListSubmissionsData, OpusMagnumApiListSubmissionsResponse, OpusMagnumApiListSubmissionsResponses, OpusMagnumApiValidateAutoData, OpusMagnumApiValidateAutoResponse, OpusMagnumApiValidateAutoResponses, OpusMagnumApiValidateResponseData, OpusMagnumApiValidateResponseResponse, OpusMagnumApiValidateResponseResponses, OpusMagnumApiValidateSubmissionData, OpusMagnumApiValidateSubmissionResponse, OpusMagnumApiValidateSubmissionResponses, PagedSubmissionOut, PlaceApiCanvasData, PlaceApiCanvasResponses, PlaceApiEvolutionGifData, PlaceApiEvolutionGifResponses, PlayerStatsOut, PolylanSubmitterApiClearCacheData, PolylanSubmitterApiClearCacheResponses, PolylanSubmitterApiGetConfigData, PolylanSubmitterApiGetConfigResponse, PolylanSubmitterApiGetConfigResponses, PolylanSubmitterApiGetUserInfoData, PolylanSubmitterApiGetUserInfoResponse, PolylanSubmitterApiGetUserInfoResponses, PolylanSubmitterApiHealthCheckData, PolylanSubmitterApiHealthCheckResponses, PuzzlePointsFactorOut, PuzzleResponseIn, PuzzleResponseOut, PuzzleResponseRankingOut, PuzzleResultsOut, PuzzleSubmissionsOut, PuzzleSubmissionWithRankOut, RankingSchema, ResolveMarketSchema, ResultsOut, ScopeOut, SteamCollectionItemOut, SteamCollectionOut, SubmissionFileOut, SubmissionIn, SubmissionOut, SubmissionsApiCreateImageCropData, SubmissionsApiCreateImageCropResponse, SubmissionsApiCreateImageCropResponses, SubmissionsApiCreateSubmissionData, SubmissionsApiCreateSubmissionError, SubmissionsApiCreateSubmissionErrors, SubmissionsApiCreateSubmissionResponse, SubmissionsApiCreateSubmissionResponses, SubmissionsApiDeleteImageCropData, SubmissionsApiDeleteImageCropResponse, SubmissionsApiDeleteImageCropResponses, SubmissionsApiGetSubmissionData, SubmissionsApiGetSubmissionResponse, SubmissionsApiGetSubmissionResponses, SubmissionsApiListAvailableScopesData, SubmissionsApiListAvailableScopesError, SubmissionsApiListAvailableScopesErrors, SubmissionsApiListAvailableScopesResponse, SubmissionsApiListAvailableScopesResponses, SubmissionsApiListMyScopesData, SubmissionsApiListMyScopesError, SubmissionsApiListMyScopesErrors, SubmissionsApiListMyScopesResponse, SubmissionsApiListMyScopesResponses, SubmissionsApiListSubmissionsByGameData, SubmissionsApiListSubmissionsByGameResponse, SubmissionsApiListSubmissionsByGameResponses, SubmissionsApiListUserSubmissionsData, SubmissionsApiListUserSubmissionsError, SubmissionsApiListUserSubmissionsErrors, SubmissionsApiListUserSubmissionsResponse, SubmissionsApiListUserSubmissionsResponses, SubmissionsApiProcessCropOcrData, SubmissionsApiProcessCropOcrResponse, SubmissionsApiProcessCropOcrResponses, SubmissionsApiProcessFileOcrData, SubmissionsApiProcessFileOcrResponse, SubmissionsApiProcessFileOcrResponses, TournamentPuzzleResultsOut, TournamentSubmissionsOut, UserBetCreateSchema, UserBetSchema, UserDisplayOut, UserInfoOut, UserPointsSchema, ValidationIn, WinnerFileOut, WinnerResponseOut } from './types.gen'; diff --git a/polylan_submitter/src/api/sdk.gen.ts b/polylan_submitter/src/api/sdk.gen.ts index b9cb5d0..c73156b 100644 --- a/polylan_submitter/src/api/sdk.gen.ts +++ b/polylan_submitter/src/api/sdk.gen.ts @@ -2,7 +2,7 @@ import { type Client, formDataBodySerializer, type Options as Options2, type TDataShape } from './client'; import { client } from './client.gen'; -import type { AnimationsApiPuzzleResultsData, AnimationsApiPuzzleResultsResponses, AnimationsApiResultsData, AnimationsApiResultsResponses, AnimationsApiTopSubmissionsData, AnimationsApiTopSubmissionsResponses, ClockApiCurrentStateData, ClockApiCurrentStateResponses, ClockApiMyStatsData, ClockApiMyStatsResponses, GamesApiListEventGamesData, GamesApiListEventGamesResponses, GamesApiListGamesData, GamesApiListGamesResponses, MarketApiCloseMarketData, MarketApiCloseMarketResponses, MarketApiCreateBetData, MarketApiCreateBetResponses, MarketApiGetUserPointsData, MarketApiGetUserPointsResponses, MarketApiListMarketsData, MarketApiListMarketsResponses, MarketApiListUserBetsData, MarketApiListUserBetsResponses, MarketApiResolveMarketData, MarketApiResolveMarketResponses, NoitaApiGetLeaderboardData, NoitaApiGetLeaderboardResponses, NoitaApiGetResultsData, NoitaApiGetResultsResponses, NoitaApiSubmitLogFileData, NoitaApiSubmitLogFileErrors, NoitaApiSubmitLogFileResponses, OpusMagnumApiCreateSubmissionData, OpusMagnumApiCreateSubmissionResponses, OpusMagnumApiDeleteSubmissionData, OpusMagnumApiDeleteSubmissionResponses, OpusMagnumApiGetCollectionData, OpusMagnumApiGetCollectionResponses, OpusMagnumApiGetStatsData, OpusMagnumApiGetStatsResponses, OpusMagnumApiGetSubmissionData, OpusMagnumApiGetSubmissionResponses, OpusMagnumApiListPuzzlesData, OpusMagnumApiListPuzzlesResponses, OpusMagnumApiListResponsesNeedingValidationData, OpusMagnumApiListResponsesNeedingValidationResponses, OpusMagnumApiListSubmissionsData, OpusMagnumApiListSubmissionsResponses, OpusMagnumApiValidateAutoData, OpusMagnumApiValidateAutoResponses, OpusMagnumApiValidateResponseData, OpusMagnumApiValidateResponseResponses, OpusMagnumApiValidateSubmissionData, OpusMagnumApiValidateSubmissionResponses, PlaceApiCanvasData, PlaceApiCanvasResponses, PlaceApiEvolutionGifData, PlaceApiEvolutionGifResponses, PolylanSubmitterApiClearCacheData, PolylanSubmitterApiClearCacheResponses, PolylanSubmitterApiGetConfigData, PolylanSubmitterApiGetConfigResponses, PolylanSubmitterApiGetUserInfoData, PolylanSubmitterApiGetUserInfoResponses, PolylanSubmitterApiHealthCheckData, PolylanSubmitterApiHealthCheckResponses, SubmissionsApiCreateImageCropData, SubmissionsApiCreateImageCropResponses, SubmissionsApiCreateSubmissionData, SubmissionsApiCreateSubmissionErrors, SubmissionsApiCreateSubmissionResponses, SubmissionsApiDeleteImageCropData, SubmissionsApiDeleteImageCropResponses, SubmissionsApiGetSubmissionData, SubmissionsApiGetSubmissionResponses, SubmissionsApiListAvailableScopesData, SubmissionsApiListAvailableScopesErrors, SubmissionsApiListAvailableScopesResponses, SubmissionsApiListMyScopesData, SubmissionsApiListMyScopesErrors, SubmissionsApiListMyScopesResponses, SubmissionsApiListSubmissionsByGameData, SubmissionsApiListSubmissionsByGameResponses, SubmissionsApiListUserSubmissionsData, SubmissionsApiListUserSubmissionsErrors, SubmissionsApiListUserSubmissionsResponses, SubmissionsApiProcessCropOcrData, SubmissionsApiProcessCropOcrResponses, SubmissionsApiProcessFileOcrData, SubmissionsApiProcessFileOcrResponses } from './types.gen'; +import type { AnimationsApiPuzzleResultsData, AnimationsApiPuzzleResultsResponses, AnimationsApiResultsData, AnimationsApiResultsResponses, AnimationsApiTopSubmissionsData, AnimationsApiTopSubmissionsResponses, ClockApiCurrentStateData, ClockApiCurrentStateResponses, ClockApiMyStatsData, ClockApiMyStatsResponses, GamesApiListEventGamesData, GamesApiListEventGamesResponses, GamesApiListGamesData, GamesApiListGamesResponses, MarketApiBetHistoryData, MarketApiBetHistoryResponses, MarketApiCloseMarketData, MarketApiCloseMarketResponses, MarketApiCreateBetData, MarketApiCreateBetResponses, MarketApiGetUserPointsData, MarketApiGetUserPointsResponses, MarketApiListMarketsData, MarketApiListMarketsResponses, MarketApiListUserBetsData, MarketApiListUserBetsResponses, MarketApiResolveMarketData, MarketApiResolveMarketResponses, NoitaApiGetLeaderboardData, NoitaApiGetLeaderboardResponses, NoitaApiGetResultsData, NoitaApiGetResultsResponses, NoitaApiSubmitLogFileData, NoitaApiSubmitLogFileErrors, NoitaApiSubmitLogFileResponses, OpusMagnumApiCreateSubmissionData, OpusMagnumApiCreateSubmissionResponses, OpusMagnumApiDeleteSubmissionData, OpusMagnumApiDeleteSubmissionResponses, OpusMagnumApiGetCollectionData, OpusMagnumApiGetCollectionResponses, OpusMagnumApiGetStatsData, OpusMagnumApiGetStatsResponses, OpusMagnumApiGetSubmissionData, OpusMagnumApiGetSubmissionResponses, OpusMagnumApiListPuzzlesData, OpusMagnumApiListPuzzlesResponses, OpusMagnumApiListResponsesNeedingValidationData, OpusMagnumApiListResponsesNeedingValidationResponses, OpusMagnumApiListSubmissionsData, OpusMagnumApiListSubmissionsResponses, OpusMagnumApiValidateAutoData, OpusMagnumApiValidateAutoResponses, OpusMagnumApiValidateResponseData, OpusMagnumApiValidateResponseResponses, OpusMagnumApiValidateSubmissionData, OpusMagnumApiValidateSubmissionResponses, PlaceApiCanvasData, PlaceApiCanvasResponses, PlaceApiEvolutionGifData, PlaceApiEvolutionGifResponses, PolylanSubmitterApiClearCacheData, PolylanSubmitterApiClearCacheResponses, PolylanSubmitterApiGetConfigData, PolylanSubmitterApiGetConfigResponses, PolylanSubmitterApiGetUserInfoData, PolylanSubmitterApiGetUserInfoResponses, PolylanSubmitterApiHealthCheckData, PolylanSubmitterApiHealthCheckResponses, SubmissionsApiCreateImageCropData, SubmissionsApiCreateImageCropResponses, SubmissionsApiCreateSubmissionData, SubmissionsApiCreateSubmissionErrors, SubmissionsApiCreateSubmissionResponses, SubmissionsApiDeleteImageCropData, SubmissionsApiDeleteImageCropResponses, SubmissionsApiGetSubmissionData, SubmissionsApiGetSubmissionResponses, SubmissionsApiListAvailableScopesData, SubmissionsApiListAvailableScopesErrors, SubmissionsApiListAvailableScopesResponses, SubmissionsApiListMyScopesData, SubmissionsApiListMyScopesErrors, SubmissionsApiListMyScopesResponses, SubmissionsApiListSubmissionsByGameData, SubmissionsApiListSubmissionsByGameResponses, SubmissionsApiListUserSubmissionsData, SubmissionsApiListUserSubmissionsErrors, SubmissionsApiListUserSubmissionsResponses, SubmissionsApiProcessCropOcrData, SubmissionsApiProcessCropOcrResponses, SubmissionsApiProcessFileOcrData, SubmissionsApiProcessFileOcrResponses } from './types.gen'; export type Options = Options2 & { /** @@ -326,6 +326,13 @@ export const marketApiListUserBets = (opti */ export const marketApiGetUserPoints = (options: Options) => (options.client ?? client).get({ url: '/api/events/{event_path}/market/user/points', ...options }); +/** + * Bet History + * + * Points added to each option over time (anonymous, one entry per bet placed/increased). + */ +export const marketApiBetHistory = (options: Options) => (options.client ?? client).get({ url: '/api/events/{event_path}/market/{market_uuid}/history', ...options }); + /** * Close Market * diff --git a/polylan_submitter/src/api/types.gen.ts b/polylan_submitter/src/api/types.gen.ts index ec2ea7b..6689498 100644 --- a/polylan_submitter/src/api/types.gen.ts +++ b/polylan_submitter/src/api/types.gen.ts @@ -1163,6 +1163,24 @@ export type UserPointsSchema = { points: number; }; +/** + * BetHistoryPointSchema + */ +export type BetHistoryPointSchema = { + /** + * Option Uuid + */ + option_uuid: string; + /** + * Created At + */ + created_at: string; + /** + * Amount + */ + amount: number; +}; + /** * ResolveMarketSchema */ @@ -2204,6 +2222,33 @@ export type MarketApiGetUserPointsResponses = { export type MarketApiGetUserPointsResponse = MarketApiGetUserPointsResponses[keyof MarketApiGetUserPointsResponses]; +export type MarketApiBetHistoryData = { + body?: never; + path: { + /** + * Event Path + */ + event_path: string; + /** + * Market Uuid + */ + market_uuid: string; + }; + query?: never; + url: '/api/events/{event_path}/market/{market_uuid}/history'; +}; + +export type MarketApiBetHistoryResponses = { + /** + * Response + * + * OK + */ + 200: Array; +}; + +export type MarketApiBetHistoryResponse = MarketApiBetHistoryResponses[keyof MarketApiBetHistoryResponses]; + export type MarketApiCloseMarketData = { body?: never; path: { diff --git a/polylan_submitter/src/components/MarketCard.vue b/polylan_submitter/src/components/MarketCard.vue index f664489..be5c77c 100644 --- a/polylan_submitter/src/components/MarketCard.vue +++ b/polylan_submitter/src/components/MarketCard.vue @@ -7,6 +7,8 @@ import { useMarketStore } from "../stores/market"; import { useEventPath } from "../composables/useEventPath"; import type { Market, MarketOption } from "../types"; import type { UserBetSchema } from "../api/types.gen"; +import PayoutChart from "./charts/PayoutChart.vue"; +import BetHistoryChart from "./charts/BetHistoryChart.vue"; const props = defineProps<{ market: Market; @@ -19,7 +21,7 @@ const emit = defineEmits<{ const { t } = useI18n(); const eventPath = useEventPath(); const marketStore = useMarketStore(); -const { userInfo, userBets } = storeToRefs(marketStore); +const { userInfo, userBets, userPoints } = storeToRefs(marketStore); const selectedOption = ref(null); const betAmount = ref(0); @@ -29,6 +31,7 @@ const existingBet = ref(null); const showResolveModal = ref(false); const selectedWinningOption = ref(null); const resolveLoading = ref(false); +const showCharts = ref(true); const timeRemaining = computed(() => { const endDate = new Date(props.market.end_date).getTime(); @@ -80,6 +83,19 @@ const getPotentialGain = (option: MarketOption) => { return Math.round(existingBet.value.amount * multiplier * props.market.multiplier); }; +const selectedMarketOption = computed(() => + props.market.options.find((opt) => opt.uuid === selectedOption.value) ?? null, +); + +// Payout chart only makes sense while the user can still bet on the selected option +const showPayoutChart = computed( + () => props.market.status === "open" && !!userInfo.value?.is_authenticated && !!selectedMarketOption.value, +); + +const existingAmountOnSelected = computed(() => + existingBet.value && existingBet.value.option.uuid === selectedOption.value ? existingBet.value.amount : 0, +); + const closeMarket = async () => { loading.value = true; error.value = ""; @@ -147,7 +163,7 @@ const placeBet = async () => { if (!response.error) { // Reload user bets to reflect the new bet - await marketStore.loadUserBets(eventPath); + await Promise.all([marketStore.loadUserBets(eventPath), marketStore.loadUserPoints(eventPath)]); updateExistingBet(); betAmount.value = 0; } else { @@ -286,6 +302,20 @@ const updateExistingBet = () => { + +
+ +
+
+ + +
+
diff --git a/polylan_submitter/src/components/charts/BetHistoryChart.vue b/polylan_submitter/src/components/charts/BetHistoryChart.vue new file mode 100644 index 0000000..e708e6c --- /dev/null +++ b/polylan_submitter/src/components/charts/BetHistoryChart.vue @@ -0,0 +1,222 @@ + + + + + diff --git a/polylan_submitter/src/components/charts/PayoutChart.vue b/polylan_submitter/src/components/charts/PayoutChart.vue new file mode 100644 index 0000000..b7071c1 --- /dev/null +++ b/polylan_submitter/src/components/charts/PayoutChart.vue @@ -0,0 +1,173 @@ + + + + + diff --git a/polylan_submitter/src/components/charts/chart.css b/polylan_submitter/src/components/charts/chart.css new file mode 100644 index 0000000..4fec218 --- /dev/null +++ b/polylan_submitter/src/components/charts/chart.css @@ -0,0 +1,64 @@ +/* Shared chart chrome. Marks carry series colors; text and chrome use theme ink. */ +.grid { + stroke: color-mix(in oklab, var(--color-base-content) 12%, transparent); + stroke-width: 1; +} + +.tick { + fill: color-mix(in oklab, var(--color-base-content) 55%, transparent); + font-size: 11px; + font-variant-numeric: tabular-nums; +} + +.label { + fill: var(--color-base-content); + font-size: 11px; + font-weight: 600; +} + +.series-line { + fill: none; + stroke-width: 2; + stroke-linejoin: round; + stroke-linecap: round; +} + +.stake-line { + fill: none; + stroke: color-mix(in oklab, var(--color-base-content) 35%, transparent); + stroke-width: 1.5; +} + +.marker { + stroke: var(--color-base-100); + stroke-width: 2; +} + +.crosshair { + stroke: color-mix(in oklab, var(--color-base-content) 35%, transparent); + stroke-width: 1; +} + +.legend-line { + display: inline-block; + width: 14px; + height: 2px; + border-radius: 1px; +} + +.legend-line--muted { + background: color-mix(in oklab, var(--color-base-content) 35%, transparent); +} + +.tooltip-box { + position: absolute; + pointer-events: none; + min-width: 150px; + padding: 6px 10px; + border-radius: var(--radius-box); + background: var(--color-base-200); + border: 1px solid color-mix(in oklab, var(--color-base-content) 10%, transparent); + font-size: 12px; + line-height: 1.5; + z-index: 10; +} diff --git a/polylan_submitter/src/components/charts/chartUtils.ts b/polylan_submitter/src/components/charts/chartUtils.ts new file mode 100644 index 0000000..6624144 --- /dev/null +++ b/polylan_submitter/src/components/charts/chartUtils.ts @@ -0,0 +1,68 @@ +// Categorical slots, dark steps (the site always runs the dark "dim" theme). +// Validated against the dim card surface (#2a303c): adjacent CVD ΔE >= 8.4. +// Order matters for colorblind safety - assign in order, never cycle. +export const SERIES_COLORS = [ + '#3987e5', // blue + '#d95926', // orange + '#199e70', // aqua + '#c98500', // yellow + '#d55181', // magenta + '#008300', // green + '#9085e9', // violet + '#e66767', // red +] + +export const MAX_SERIES = SERIES_COLORS.length + +export const linearScale = (domain: [number, number], range: [number, number]) => { + const [d0, d1] = domain + const [r0, r1] = range + const span = d1 - d0 || 1 + return (v: number) => r0 + ((v - d0) / span) * (r1 - r0) +} + +// Round tick values (0 / 250 / 500 ...) covering [0, max] +export const niceTicks = (max: number, count = 4): number[] => { + if (max <= 0) return [0] + const raw = max / count + const mag = 10 ** Math.floor(Math.log10(raw)) + const step = [1, 2, 2.5, 5, 10].map((m) => m * mag).find((s) => s >= raw) ?? raw + const ticks: number[] = [] + for (let v = 0; v <= max + step * 0.001; v += step) ticks.push(Math.round(v)) + if (ticks[ticks.length - 1] < max) ticks.push(Math.round(ticks[ticks.length - 1] + step)) + return ticks +} + +export const formatNumber = (n: number) => Math.round(n).toLocaleString() + +// Smooth SVG path through the points (monotone cubic, like d3.curveMonotoneX): +// it never overshoots, so a growing total never appears to dip. +export const monotonePath = (points: [number, number][]): string => { + const n = points.length + if (n === 0) return '' + if (n < 3) return points.map(([px, py], i) => `${i ? 'L' : 'M'}${px},${py}`).join('') + + const slopes: number[] = [] + for (let i = 0; i < n - 1; i++) { + const dx = points[i + 1][0] - points[i][0] + slopes.push(dx === 0 ? 0 : (points[i + 1][1] - points[i][1]) / dx) + } + + // Tangents: zero at local extrema, harmonic mean elsewhere + const tangents = points.map((_, i) => { + if (i === 0) return slopes[0] + if (i === n - 1) return slopes[n - 2] + const a = slopes[i - 1] + const b = slopes[i] + return a * b <= 0 ? 0 : (2 * a * b) / (a + b) + }) + + let d = `M${points[0][0]},${points[0][1]}` + for (let i = 0; i < n - 1; i++) { + const [x0, y0] = points[i] + const [x1, y1] = points[i + 1] + const h = (x1 - x0) / 3 + d += `C${x0 + h},${y0 + h * tangents[i]},${x1 - h},${y1 - h * tangents[i + 1]},${x1},${y1}` + } + return d +} diff --git a/polylan_submitter/src/locales/en.json b/polylan_submitter/src/locales/en.json index d6c0eb8..88b6c53 100644 --- a/polylan_submitter/src/locales/en.json +++ b/polylan_submitter/src/locales/en.json @@ -302,6 +302,24 @@ "errorPlacingBet": "Error placing bet", "ended": "Ended" }, + "marketCharts": { + "showCharts": "Show charts", + "hideCharts": "Hide charts", + "payoutTitle": "Potential payout if “{option}” wins", + "payout": "Payout", + "stake": "Your stake", + "netGain": "Net gain", + "ifYouBet": "If you bet {n} pts", + "payoutHint": "Based on the current pools. Payouts shift as others bet.", + "historyTitle": "Points bet over time", + "noBets": "No bets yet", + "other": "Other", + "showTable": "Show data table", + "time": "Time", + "option": "Option", + "amount": "Amount", + "optionPool": "Option pool" + }, "puzzleCard": { "byAuthor": "by {author}", "moreTags": "+{n} more",