From 27d3c08a723d0f9d2c0c2a04ddcfdb0503b1aa88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Gremaud?= Date: Fri, 16 Oct 2026 00:59:43 +0200 Subject: [PATCH] feat(games): add steam pages with steam price/reduc --- deploy/docker/entrypoint.sh | 4 + deploy/docker/polylan-submitter.env.dist | 2 + polylan_submitter/games/admin.py | 25 ++- polylan_submitter/games/api.py | 13 +- .../games/management/__init__.py | 0 .../games/management/commands/__init__.py | 0 .../commands/schedule_steam_refresh.py | 13 ++ .../migrations/0010_game_steam_store_info.py | 52 ++++++ polylan_submitter/games/models.py | 11 ++ polylan_submitter/games/schemas.py | 60 +++++++ polylan_submitter/games/services/__init__.py | 0 .../games/services/steam_store.py | 79 +++++++++ polylan_submitter/games/tasks.py | 52 ++++++ polylan_submitter/games/tests.py | 157 ++++++++++++++++++ .../polylan_submitter/settings.py | 6 + polylan_submitter/polylan_submitter/urls.py | 7 + polylan_submitter/src/Games.vue | 55 ++++++ polylan_submitter/src/Home.vue | 11 +- polylan_submitter/src/api/index.ts | 4 +- polylan_submitter/src/api/sdk.gen.ts | 9 +- polylan_submitter/src/api/types.gen.ts | 77 +++++++++ .../src/components/SteamStoreCard.vue | 129 ++++++++++++++ polylan_submitter/src/locales/en.json | 11 ++ polylan_submitter/src/router/index.ts | 8 + 24 files changed, 779 insertions(+), 6 deletions(-) create mode 100644 polylan_submitter/games/management/__init__.py create mode 100644 polylan_submitter/games/management/commands/__init__.py create mode 100644 polylan_submitter/games/management/commands/schedule_steam_refresh.py create mode 100644 polylan_submitter/games/migrations/0010_game_steam_store_info.py create mode 100644 polylan_submitter/games/services/__init__.py create mode 100644 polylan_submitter/games/services/steam_store.py create mode 100644 polylan_submitter/games/tasks.py create mode 100644 polylan_submitter/games/tests.py create mode 100644 polylan_submitter/src/Games.vue create mode 100644 polylan_submitter/src/components/SteamStoreCard.vue diff --git a/deploy/docker/entrypoint.sh b/deploy/docker/entrypoint.sh index 5cec323..24a285e 100644 --- a/deploy/docker/entrypoint.sh +++ b/deploy/docker/entrypoint.sh @@ -16,4 +16,8 @@ if [ "${MIGRATE_ON_START:-true}" = "true" ]; then python manage.py migrate --noinput fi +# Kick off the periodic Steam store refresh (games.tasks). No-op when a refresh +# is already queued, so every replica can safely run it on start. +python manage.py schedule_steam_refresh || echo "Could not schedule Steam store refresh" + exec "$@" diff --git a/deploy/docker/polylan-submitter.env.dist b/deploy/docker/polylan-submitter.env.dist index 78fea24..0f05051 100644 --- a/deploy/docker/polylan-submitter.env.dist +++ b/deploy/docker/polylan-submitter.env.dist @@ -36,5 +36,7 @@ SENTRY_ENVIRONMENT=production SENTRY_DSN= STEAM_API_KEY= +STEAM_STORE_REFRESH_MINUTES=360 +STEAM_STORE_COUNTRY=ch AANDT_API_BASE_URL= SUBMITTER_BASE_URL= diff --git a/polylan_submitter/games/admin.py b/polylan_submitter/games/admin.py index bb906d6..1d0cd59 100644 --- a/polylan_submitter/games/admin.py +++ b/polylan_submitter/games/admin.py @@ -8,6 +8,7 @@ from django_object_actions import ( from submissions.tasks import sync_event_game_scopes_task from .models import EventGame, Game +from .tasks import refresh_steam_games_task class EventGameInline(admin.TabularInline): @@ -19,8 +20,30 @@ class EventGameInline(admin.TabularInline): class GameAdmin(admin.ModelAdmin): list_display = ["name", "steam_app_id", "updated_at"] search_fields = ["name", "steam_app_id"] - readonly_fields = ["created_at", "updated_at"] + readonly_fields = [ + "created_at", + "updated_at", + "steam_price_initial", + "steam_price_final", + "steam_discount_percent", + "steam_currency", + "steam_is_free", + "steam_short_description", + "steam_header_image", + "steam_info_updated_at", + ] inlines = [EventGameInline] + actions = ["refresh_steam_info"] + + @admin.action(description="Refresh Steam store info") + def refresh_steam_info(self, request, queryset): + game_ids = list( + queryset.filter(steam_app_id__isnull=False).values_list("id", flat=True) + ) + refresh_steam_games_task.enqueue(game_ids=game_ids, reschedule=False) + self.message_user( + request, f"Queued Steam store refresh for {len(game_ids)} game(s)." + ) @admin.register(EventGame) diff --git a/polylan_submitter/games/api.py b/polylan_submitter/games/api.py index 1bf0a1d..c49efd8 100644 --- a/polylan_submitter/games/api.py +++ b/polylan_submitter/games/api.py @@ -3,7 +3,8 @@ from typing import List from ninja import Router from .models import Game -from .schemas import GameOut +from .schemas import GameOut, SteamGameOut +from .tasks import schedule_steam_refresh router = Router() event_router = Router() @@ -17,3 +18,13 @@ def list_games(request): @event_router.get("/events/{event_path}/games", response=List[GameOut]) def list_event_games(request, event_path: str): return Game.objects.enabled_for_event(request.event) + + +@event_router.get("/events/{event_path}/games/steam", response=List[SteamGameOut]) +def list_event_steam_games(request, event_path: str): + """The event's Steam games with their store info (price, discount, blurb).""" + # Safety net in case the periodic refresh loop isn't running (no-op otherwise). + schedule_steam_refresh() + return Game.objects.enabled_for_event(request.event).filter( + steam_app_id__isnull=False + ) diff --git a/polylan_submitter/games/management/__init__.py b/polylan_submitter/games/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/polylan_submitter/games/management/commands/__init__.py b/polylan_submitter/games/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/polylan_submitter/games/management/commands/schedule_steam_refresh.py b/polylan_submitter/games/management/commands/schedule_steam_refresh.py new file mode 100644 index 0000000..da2a4a5 --- /dev/null +++ b/polylan_submitter/games/management/commands/schedule_steam_refresh.py @@ -0,0 +1,13 @@ +from django.core.management.base import BaseCommand + +from games.tasks import schedule_steam_refresh + + +class Command(BaseCommand): + help = "Start the periodic Steam store refresh loop (no-op if already running)" + + def handle(self, *args, **options): + if schedule_steam_refresh(): + self.stdout.write(self.style.SUCCESS("Steam store refresh scheduled")) + else: + self.stdout.write("Steam store refresh already scheduled") diff --git a/polylan_submitter/games/migrations/0010_game_steam_store_info.py b/polylan_submitter/games/migrations/0010_game_steam_store_info.py new file mode 100644 index 0000000..a8aece5 --- /dev/null +++ b/polylan_submitter/games/migrations/0010_game_steam_store_info.py @@ -0,0 +1,52 @@ +# Generated by Django 6.0.5 on 2026-10-15 22:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("games", "0009_game_description"), + ] + + operations = [ + migrations.AddField( + model_name="game", + name="steam_currency", + field=models.CharField(blank=True, default="", max_length=3), + ), + migrations.AddField( + model_name="game", + name="steam_discount_percent", + field=models.PositiveSmallIntegerField(default=0), + ), + migrations.AddField( + model_name="game", + name="steam_header_image", + field=models.URLField(blank=True, default="", max_length=500), + ), + migrations.AddField( + model_name="game", + name="steam_info_updated_at", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name="game", + name="steam_is_free", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="game", + name="steam_price_final", + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="game", + name="steam_price_initial", + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="game", + name="steam_short_description", + field=models.TextField(blank=True, default=""), + ), + ] diff --git a/polylan_submitter/games/models.py b/polylan_submitter/games/models.py index bda5c5a..87d7358 100644 --- a/polylan_submitter/games/models.py +++ b/polylan_submitter/games/models.py @@ -16,6 +16,17 @@ class Game(models.Model): image = models.URLField(null=True, blank=True) description = models.TextField(blank=True, default="") + # Steam store info, refreshed in the background (see games.tasks). + # Prices are in cents, in steam_currency. + steam_price_initial = models.PositiveIntegerField(null=True, blank=True) + steam_price_final = models.PositiveIntegerField(null=True, blank=True) + steam_discount_percent = models.PositiveSmallIntegerField(default=0) + steam_currency = models.CharField(max_length=3, blank=True, default="") + steam_is_free = models.BooleanField(default=False) + steam_short_description = models.TextField(blank=True, default="") + steam_header_image = models.URLField(max_length=500, blank=True, default="") + steam_info_updated_at = models.DateTimeField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) diff --git a/polylan_submitter/games/schemas.py b/polylan_submitter/games/schemas.py index cfb015d..ae3c2b0 100644 --- a/polylan_submitter/games/schemas.py +++ b/polylan_submitter/games/schemas.py @@ -1,3 +1,4 @@ +from datetime import datetime from typing import Optional from ninja import Schema @@ -11,3 +12,62 @@ class GameOut(Schema): custom_path: bool image: Optional[str] = None description: str + + @staticmethod + def resolve_description(obj) -> str: + # The admin one wins, Steam games fall back to their store blurb + return obj.description or obj.steam_short_description + + +class SteamGameOut(Schema): + id: int + name: str + steam_app_id: int + store_url: str + header_image: str + short_description: str + is_free: bool + currency: str + price_initial: Optional[int] = None + price_final: Optional[int] = None + discount_percent: int + updated_at: Optional[datetime] = None + + @staticmethod + def resolve_store_url(obj) -> str: + return f"https://store.steampowered.com/app/{obj.steam_app_id}" + + @staticmethod + def resolve_header_image(obj) -> str: + return ( + obj.steam_header_image + or f"https://cdn.akamai.steamstatic.com/steam/apps/{obj.steam_app_id}/header.jpg" + ) + + @staticmethod + def resolve_short_description(obj) -> str: + return obj.steam_short_description + + @staticmethod + def resolve_is_free(obj) -> bool: + return obj.steam_is_free + + @staticmethod + def resolve_currency(obj) -> str: + return obj.steam_currency + + @staticmethod + def resolve_price_initial(obj) -> Optional[int]: + return obj.steam_price_initial + + @staticmethod + def resolve_price_final(obj) -> Optional[int]: + return obj.steam_price_final + + @staticmethod + def resolve_discount_percent(obj) -> int: + return obj.steam_discount_percent + + @staticmethod + def resolve_updated_at(obj) -> Optional[datetime]: + return obj.steam_info_updated_at diff --git a/polylan_submitter/games/services/__init__.py b/polylan_submitter/games/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/polylan_submitter/games/services/steam_store.py b/polylan_submitter/games/services/steam_store.py new file mode 100644 index 0000000..dc26be4 --- /dev/null +++ b/polylan_submitter/games/services/steam_store.py @@ -0,0 +1,79 @@ +import logging + +import httpx +from django.conf import settings +from django.utils import timezone + +from ..models import Game + +logger = logging.getLogger(__name__) + +# Public store endpoint, no API key needed. Rate limited to ~200 calls / 5 min, +# which is plenty for a handful of games every few hours. +APP_DETAILS_URL = "https://store.steampowered.com/api/appdetails" + + +class SteamStoreError(Exception): + """Raised when the Steam store didn't return usable details for an app.""" + + +def fetch_app_details(steam_app_id: int) -> dict: + response = httpx.get( + APP_DETAILS_URL, + params={ + "appids": steam_app_id, + "cc": settings.STEAM_STORE_COUNTRY, + "l": "en", + "filters": "basic,price_overview", + }, + timeout=10, + ) + response.raise_for_status() + + entry = (response.json() or {}).get(str(steam_app_id)) or {} + if not entry.get("success"): + raise SteamStoreError(f"No store details for app {steam_app_id}") + + return entry["data"] + + +def refresh_steam_game(game: Game) -> None: + """Pull the current store info for a Steam game into its steam_* fields.""" + data = fetch_app_details(game.steam_app_id) + price = data.get("price_overview") or {} + + game.steam_is_free = bool(data.get("is_free")) + game.steam_price_initial = price.get("initial") + game.steam_price_final = price.get("final") + game.steam_discount_percent = price.get("discount_percent") or 0 + game.steam_currency = price.get("currency") or "" + game.steam_short_description = data.get("short_description") or "" + game.steam_header_image = data.get("header_image") or "" + game.steam_info_updated_at = timezone.now() + game.save( + update_fields=[ + "steam_is_free", + "steam_price_initial", + "steam_price_final", + "steam_discount_percent", + "steam_currency", + "steam_short_description", + "steam_header_image", + "steam_info_updated_at", + "updated_at", + ] + ) + + +def refresh_steam_games(game_ids: list[int] | None = None) -> None: + """Refresh every Steam game (or just game_ids). A failing game keeps its + previous info and doesn't stop the others.""" + games = Game.objects.filter(steam_app_id__isnull=False) + if game_ids is not None: + games = games.filter(id__in=game_ids) + + for game in games: + try: + refresh_steam_game(game) + except (httpx.HTTPError, ValueError, SteamStoreError) as exc: + logger.warning("Steam store refresh failed for %s: %s", game, exc) diff --git a/polylan_submitter/games/tasks.py b/polylan_submitter/games/tasks.py new file mode 100644 index 0000000..01ba3dd --- /dev/null +++ b/polylan_submitter/games/tasks.py @@ -0,0 +1,52 @@ +from datetime import timedelta + +from django.conf import settings +from django.core.cache import cache +from django.tasks import task +from django.utils import timezone + +from .services.steam_store import refresh_steam_games + +# Set while a periodic refresh is queued, so there's only ever one loop running, +# whatever the number of workers/containers trying to start it. It expires on +# its own if the loop dies (worker killed mid-job, Redis queue flushed, ...), +# letting the next schedule_steam_refresh() start a new one. +STEAM_REFRESH_GUARD_KEY = "games:steam_refresh:scheduled" + + +def steam_refresh_interval() -> timedelta: + return timedelta(minutes=settings.STEAM_STORE_REFRESH_MINUTES) + + +def _guard_timeout() -> int: + return int(steam_refresh_interval().total_seconds() * 2) + + +def schedule_steam_refresh() -> bool: + """Start the periodic refresh loop right away, unless one is already queued. + Safe to call from anywhere, any number of times.""" + if not cache.add(STEAM_REFRESH_GUARD_KEY, 1, timeout=_guard_timeout()): + return False + + refresh_steam_games_task.enqueue() + return True + + +@task +def refresh_steam_games_task( + game_ids: list[int] | None = None, reschedule: bool = True +): + """Refresh Steam store info, then re-enqueue itself every + STEAM_STORE_REFRESH_MINUTES (same self-rescheduling idea as + sync_event_game_scopes_task, no external scheduler needed). + + reschedule=False is a one-off run (e.g. the admin action) that leaves the + periodic loop alone.""" + try: + refresh_steam_games(game_ids) + finally: + if reschedule: + cache.set(STEAM_REFRESH_GUARD_KEY, 1, timeout=_guard_timeout()) + refresh_steam_games_task.using( + run_after=timezone.now() + steam_refresh_interval() + ).enqueue() diff --git a/polylan_submitter/games/tests.py b/polylan_submitter/games/tests.py new file mode 100644 index 0000000..8142214 --- /dev/null +++ b/polylan_submitter/games/tests.py @@ -0,0 +1,157 @@ +from unittest import mock + +import httpx +from django.core.cache import cache +from django.test import TestCase, override_settings + +from accounts.models import CustomUser +from events.models import Event + +from .models import EventGame, Game +from .services.steam_store import refresh_steam_games +from .tasks import refresh_steam_games_task, schedule_steam_refresh + +LOCMEM_CACHE = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}} + + +def steam_response(app_id, data, success=True): + return httpx.Response( + 200, + json={str(app_id): {"success": success, "data": data}}, + request=httpx.Request("GET", "https://store.steampowered.com"), + ) + + +class RefreshSteamGamesTests(TestCase): + def setUp(self): + self.game = Game.objects.create(name="Noita", path="noita", steam_app_id=881100) + + @mock.patch("games.services.steam_store.httpx.get") + def test_stores_price_and_discount(self, get): + get.return_value = steam_response( + 881100, + { + "is_free": False, + "short_description": "Every pixel is simulated.", + "header_image": "https://example.com/header.jpg", + "price_overview": { + "currency": "CHF", + "initial": 1950, + "final": 780, + "discount_percent": 60, + }, + }, + ) + + refresh_steam_games() + + self.game.refresh_from_db() + self.assertEqual(self.game.steam_price_initial, 1950) + self.assertEqual(self.game.steam_price_final, 780) + self.assertEqual(self.game.steam_discount_percent, 60) + self.assertEqual(self.game.steam_currency, "CHF") + self.assertEqual(self.game.steam_short_description, "Every pixel is simulated.") + self.assertIsNotNone(self.game.steam_info_updated_at) + + @mock.patch("games.services.steam_store.httpx.get") + def test_free_game_has_no_price(self, get): + get.return_value = steam_response(881100, {"is_free": True}) + + refresh_steam_games() + + self.game.refresh_from_db() + self.assertTrue(self.game.steam_is_free) + self.assertIsNone(self.game.steam_price_final) + + @mock.patch("games.services.steam_store.httpx.get") + def test_failure_keeps_previous_info(self, get): + Game.objects.filter(pk=self.game.pk).update(steam_price_final=999) + get.side_effect = httpx.ConnectError("down") + + refresh_steam_games() + + self.game.refresh_from_db() + self.assertEqual(self.game.steam_price_final, 999) + + +@override_settings(CACHES=LOCMEM_CACHE) +class ScheduleSteamRefreshTests(TestCase): + def setUp(self): + cache.clear() + + @mock.patch("games.tasks.refresh_steam_games_task") + def test_only_one_loop_is_scheduled(self, task): + self.assertTrue(schedule_steam_refresh()) + self.assertFalse(schedule_steam_refresh()) + task.enqueue.assert_called_once_with() + + @mock.patch("games.tasks.refresh_steam_games") + def test_task_reschedules_itself_even_on_failure(self, refresh): + refresh.side_effect = RuntimeError("boom") + with mock.patch("games.tasks.refresh_steam_games_task") as patched: + with self.assertRaises(RuntimeError): + refresh_steam_games_task.call() + + patched.using.return_value.enqueue.assert_called_once_with() + + @mock.patch("games.tasks.refresh_steam_games") + def test_one_off_run_does_not_reschedule(self, refresh): + with mock.patch("games.tasks.refresh_steam_games_task") as patched: + refresh_steam_games_task.call(game_ids=[1], reschedule=False) + + refresh.assert_called_once_with([1]) + patched.using.assert_not_called() + + +@override_settings(CACHES=LOCMEM_CACHE) +class EventSteamGamesApiTests(TestCase): + def setUp(self): + self.event = Event.objects.create(name="Event", path="event") + noita = Game.objects.create( + name="Noita", path="noita", steam_app_id=881100, steam_price_final=780 + ) + clock = Game.objects.create(name="Clock", path="clock", custom_path=True) + EventGame.objects.create(event=self.event, game=noita) + EventGame.objects.create(event=self.event, game=clock) + user = CustomUser.objects.create_user(username="user", password="x") + self.client.force_login(user) + + @mock.patch("games.api.schedule_steam_refresh") + def test_lists_only_steam_games_with_store_info(self, schedule): + response = self.client.get("/api/events/event/games/steam") + + self.assertEqual(response.status_code, 200) + [game] = response.json() + self.assertEqual(game["name"], "Noita") + self.assertEqual(game["price_final"], 780) + self.assertEqual(game["store_url"], "https://store.steampowered.com/app/881100") + schedule.assert_called_once() + + +class EventGamesDescriptionTests(TestCase): + def setUp(self): + self.event = Event.objects.create(name="Event", path="event") + self.user = CustomUser.objects.create_user(username="user", password="x") + self.client.force_login(self.user) + + def test_steam_description_is_fallback_for_admin_one(self): + steam = Game.objects.create( + name="Noita", + path="noita", + steam_app_id=881100, + steam_short_description="From Steam", + ) + custom = Game.objects.create( + name="Opus", + path="opus", + steam_app_id=558990, + description="From admin", + steam_short_description="From Steam", + ) + EventGame.objects.create(event=self.event, game=steam) + EventGame.objects.create(event=self.event, game=custom) + + response = self.client.get("/api/events/event/games") + + descriptions = {g["name"]: g["description"] for g in response.json()} + self.assertEqual(descriptions, {"Noita": "From Steam", "Opus": "From admin"}) diff --git a/polylan_submitter/polylan_submitter/settings.py b/polylan_submitter/polylan_submitter/settings.py index ab0b546..9c74932 100644 --- a/polylan_submitter/polylan_submitter/settings.py +++ b/polylan_submitter/polylan_submitter/settings.py @@ -231,6 +231,12 @@ LOAD_TEST_LOGIN_ENABLED = ( # Steam API Configuration STEAM_API_KEY = os.environ.get("STEAM_API_KEY") or None # Set via environment variable +# Steam store info (price, discount, blurb) for the event's Steam games, refreshed +# in the background every STEAM_STORE_REFRESH_MINUTES (see games.tasks) +STEAM_STORE_REFRESH_MINUTES = int(os.environ.get("STEAM_STORE_REFRESH_MINUTES") or 360) +# Store region the prices are fetched for (ISO country code, drives the currency) +STEAM_STORE_COUNTRY = os.environ.get("STEAM_STORE_COUNTRY") or "ch" + # Base URL of the legacy PolyLAN site, used to sync AAndT matches/members # (see submissions.services.aandt_sync) AANDT_API_BASE_URL = os.environ.get("AANDT_API_BASE_URL") or "https://polylan.ch" diff --git a/polylan_submitter/polylan_submitter/urls.py b/polylan_submitter/polylan_submitter/urls.py index 14616ad..fc8e468 100644 --- a/polylan_submitter/polylan_submitter/urls.py +++ b/polylan_submitter/polylan_submitter/urls.py @@ -50,6 +50,12 @@ def event_home(request: HttpRequest, event_path: str): return render(request, "app.html") +@login_required +def games_home(request: HttpRequest, event_path: str): + get_object_or_404(Event, path=event_path) + return render(request, "app.html") + + @login_required @require_game_enabled(OPUS_MAGNUM_APP_ID) def opus_magnum_home(request: HttpRequest, event_path: str): @@ -95,6 +101,7 @@ urlpatterns = [ path("cas/login/", SimpleCASLoginView.as_view(), name="cas_ng_login"), path("cas/logout/", SimpleCASLogoutView.as_view(), name="cas_ng_logout"), path("api/", api.urls), + path("/games", games_home, name="games.home"), path("/market", market_home, name="market.home"), path("/opus-magnum", opus_magnum_home, name="opus-magnum.home"), path("/noita", noita_home, name="noita.home"), diff --git a/polylan_submitter/src/Games.vue b/polylan_submitter/src/Games.vue new file mode 100644 index 0000000..9f2d854 --- /dev/null +++ b/polylan_submitter/src/Games.vue @@ -0,0 +1,55 @@ + + + diff --git a/polylan_submitter/src/Home.vue b/polylan_submitter/src/Home.vue index 9b30b97..bfb15f8 100644 --- a/polylan_submitter/src/Home.vue +++ b/polylan_submitter/src/Home.vue @@ -18,6 +18,7 @@ useTitle(computed(() => t("home.title"))); const games = ref(); const loading = ref(true); +const hasSteamGames = computed(() => games.value?.some((game) => game.steam_app_id) ?? false); const imageErrors = ref>(new Set()); const getHeaderImage = (game: GamesApiListEventGamesResponse[number]) => { @@ -83,7 +84,7 @@ onMounted(async () => {

{{ game.name }}

-

{{ game.description }}

+

{{ game.description }}

+ +
+ + + {{ t("home.getTheGames") }} + +
+

{{ t("home.footer") }}

diff --git a/polylan_submitter/src/api/index.ts b/polylan_submitter/src/api/index.ts index 52443ee..727c8cf 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, 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'; -export type { AnimationsApiPuzzleResultsData, AnimationsApiPuzzleResultsResponse, AnimationsApiPuzzleResultsResponses, AnimationsApiResultsData, AnimationsApiResultsResponse, AnimationsApiResultsResponses, AnimationsApiTopSubmissionsData, AnimationsApiTopSubmissionsResponse, AnimationsApiTopSubmissionsResponses, BetHistoryPointSchema, ClientOptions, ClockApiCurrentStateData, ClockApiCurrentStateResponse, ClockApiCurrentStateResponses, ClockApiMyStatsData, ClockApiMyStatsResponse, ClockApiMyStatsResponses, ClockStateOut, 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, 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, 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'; +export type { AnimationsApiPuzzleResultsData, AnimationsApiPuzzleResultsResponse, AnimationsApiPuzzleResultsResponses, AnimationsApiResultsData, AnimationsApiResultsResponse, AnimationsApiResultsResponses, AnimationsApiTopSubmissionsData, AnimationsApiTopSubmissionsResponse, AnimationsApiTopSubmissionsResponses, BetHistoryPointSchema, ClientOptions, ClockApiCurrentStateData, ClockApiCurrentStateResponse, ClockApiCurrentStateResponses, ClockApiMyStatsData, ClockApiMyStatsResponse, ClockApiMyStatsResponses, ClockStateOut, GameOut, GamesApiListEventGamesData, GamesApiListEventGamesResponse, GamesApiListEventGamesResponses, GamesApiListEventSteamGamesData, GamesApiListEventSteamGamesResponse, GamesApiListEventSteamGamesResponses, 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, PolylanSubmitterApiGetUserInfoData, PolylanSubmitterApiGetUserInfoResponse, PolylanSubmitterApiGetUserInfoResponses, PolylanSubmitterApiHealthCheckData, PolylanSubmitterApiHealthCheckResponses, PuzzlePointsFactorOut, PuzzleResponseIn, PuzzleResponseOut, PuzzleResponseRankingOut, PuzzleResultsOut, PuzzleSubmissionsOut, PuzzleSubmissionWithRankOut, RankingSchema, ResolveMarketSchema, ResultsOut, ScopeOut, SteamCollectionItemOut, SteamCollectionOut, SteamGameOut, 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 0c44f6f..2397946 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, 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, 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, GamesApiListEventSteamGamesData, GamesApiListEventSteamGamesResponses, 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, 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 & { /** @@ -196,6 +196,13 @@ export const noitaApiSubmitLogFile = (opti */ export const gamesApiListEventGames = (options: Options) => (options.client ?? client).get({ url: '/api/events/{event_path}/games', ...options }); +/** + * List Event Steam Games + * + * The event's Steam games with their store info (price, discount, blurb). + */ +export const gamesApiListEventSteamGames = (options: Options) => (options.client ?? client).get({ url: '/api/events/{event_path}/games/steam', ...options }); + /** * List My Scopes */ diff --git a/polylan_submitter/src/api/types.gen.ts b/polylan_submitter/src/api/types.gen.ts index e44a97f..a59b657 100644 --- a/polylan_submitter/src/api/types.gen.ts +++ b/polylan_submitter/src/api/types.gen.ts @@ -924,6 +924,60 @@ export type NoitaSubmissionOut = { processed: boolean; }; +/** + * SteamGameOut + */ +export type SteamGameOut = { + /** + * Id + */ + id: number; + /** + * Name + */ + name: string; + /** + * Steam App Id + */ + steam_app_id: number; + /** + * Store Url + */ + store_url: string; + /** + * Header Image + */ + header_image: string; + /** + * Short Description + */ + short_description: string; + /** + * Is Free + */ + is_free: boolean; + /** + * Currency + */ + currency: string; + /** + * Price Initial + */ + price_initial?: number | null; + /** + * Price Final + */ + price_final?: number | null; + /** + * Discount Percent + */ + discount_percent: number; + /** + * Updated At + */ + updated_at?: string | null; +}; + /** * ScopeOut */ @@ -1715,6 +1769,29 @@ export type GamesApiListEventGamesResponses = { export type GamesApiListEventGamesResponse = GamesApiListEventGamesResponses[keyof GamesApiListEventGamesResponses]; +export type GamesApiListEventSteamGamesData = { + body?: never; + path: { + /** + * Event Path + */ + event_path: string; + }; + query?: never; + url: '/api/events/{event_path}/games/steam'; +}; + +export type GamesApiListEventSteamGamesResponses = { + /** + * Response + * + * OK + */ + 200: Array; +}; + +export type GamesApiListEventSteamGamesResponse = GamesApiListEventSteamGamesResponses[keyof GamesApiListEventSteamGamesResponses]; + export type SubmissionsApiListMyScopesData = { body?: never; path: { diff --git a/polylan_submitter/src/components/SteamStoreCard.vue b/polylan_submitter/src/components/SteamStoreCard.vue new file mode 100644 index 0000000..8c6f166 --- /dev/null +++ b/polylan_submitter/src/components/SteamStoreCard.vue @@ -0,0 +1,129 @@ + + + + + diff --git a/polylan_submitter/src/locales/en.json b/polylan_submitter/src/locales/en.json index 98a3211..e4c9ba0 100644 --- a/polylan_submitter/src/locales/en.json +++ b/polylan_submitter/src/locales/en.json @@ -64,8 +64,19 @@ "title": "PolyLAN Submitter", "subtitle": "Choose a game and submit your best solutions", "submitResults": "Submit results", + "getTheGames": "Get the games", "footer": "Select a game above to begin submitting" }, + "games": { + "header": "Games", + "title": "Get the games", + "subtitle": "Don't own them yet? Grab them on Steam", + "empty": "No Steam games for this event", + "buy": "Buy {game}", + "free": "Free to Play", + "viewOnSteam": "View on Steam", + "play": "Play" + }, "clock": { "pageTitle": "Beat the Clock", "header": "Beat the Clock", diff --git a/polylan_submitter/src/router/index.ts b/polylan_submitter/src/router/index.ts index 597e3dc..4ada90b 100644 --- a/polylan_submitter/src/router/index.ts +++ b/polylan_submitter/src/router/index.ts @@ -1,5 +1,6 @@ import { createRouter, createWebHistory, type RouteLocationNormalizedLoaded, type RouteLocationRaw } from "vue-router"; import Clock from "@/Clock.vue"; +import Games from "@/Games.vue"; import Home from "@/Home.vue"; import Market from "@/Market.vue"; import Noita from "@/Noita.vue"; @@ -37,6 +38,13 @@ export const router = createRouter({ component: Home, props: true, }, + { + path: "/:eventPath/games", + name: "games", + component: Games, + props: true, + meta: { titleKey: "games.header", getBackRoute: eventHome }, + }, { path: "/:eventPath/market", name: "market",