Compare commits
18 Commits
9f94fb3974
...
47812ffd09
| Author | SHA1 | Date | |
|---|---|---|---|
| 47812ffd09 | |||
| a5fe8aacaf | |||
| b437210eb3 | |||
| 5584e54b58 | |||
| 35ea54ecea | |||
| 821e453bc0 | |||
| 9fd0122a67 | |||
| e557fe2cda | |||
| 79e7cef3ba | |||
| a264336bd8 | |||
| 42e3571fab | |||
| 43b314bb20 | |||
| f1afb2096f | |||
| 62a81e57ad | |||
| 303b9e1c8a | |||
| f7c7eba4da | |||
| ce30539808 | |||
| 544112b204 |
@ -34,3 +34,6 @@ class CustomUserAdmin(UserAdmin):
|
|||||||
return obj.get_cas_groups_display()
|
return obj.get_cas_groups_display()
|
||||||
|
|
||||||
get_cas_groups_display.short_description = "CAS Groups"
|
get_cas_groups_display.short_description = "CAS Groups"
|
||||||
|
|
||||||
|
def has_delete_permission(self, request, obj=None):
|
||||||
|
return False
|
||||||
|
|||||||
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-05-23 18:11
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("accounts", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="customuser",
|
||||||
|
name="points",
|
||||||
|
field=models.IntegerField(default=1000),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -14,6 +14,9 @@ class CustomUser(AbstractUser):
|
|||||||
# Additional fields that might come from CAS
|
# Additional fields that might come from CAS
|
||||||
cas_attributes = models.JSONField(default=dict, blank=True)
|
cas_attributes = models.JSONField(default=dict, blank=True)
|
||||||
|
|
||||||
|
# Market points balance
|
||||||
|
points = models.IntegerField(default=1000)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.username} ({self.cas_user_id})"
|
return f"{self.username} ({self.cas_user_id})"
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,7 @@ from animations.schemas import (
|
|||||||
WinnerResponseOut,
|
WinnerResponseOut,
|
||||||
WinnerFileOut,
|
WinnerFileOut,
|
||||||
)
|
)
|
||||||
from submissions.models import PuzzleResponse, SteamCollectionItem, SteamCollection
|
from opus_magnum.models import PuzzleResponse, SteamCollectionItem, SteamCollection
|
||||||
|
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
from ninja import ModelSchema, Schema
|
from ninja import ModelSchema, Schema
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from submissions.models import PuzzleResponse
|
from opus_magnum.models import PuzzleResponse
|
||||||
from submissions.schemas import SteamCollectionItemOut
|
from opus_magnum.schemas import SteamCollectionItemOut
|
||||||
|
|
||||||
|
|
||||||
class PuzzleResponseRankingOut(ModelSchema):
|
class PuzzleResponseRankingOut(ModelSchema):
|
||||||
|
|||||||
11
polylan_submitter/games/admin.py
Normal file
11
polylan_submitter/games/admin.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import Game
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Game)
|
||||||
|
class GameAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ["name", "steam_app_id", "enabled", "updated_at"]
|
||||||
|
list_filter = ["enabled"]
|
||||||
|
search_fields = ["name", "steam_app_id"]
|
||||||
|
readonly_fields = ["created_at", "updated_at"]
|
||||||
13
polylan_submitter/games/api.py
Normal file
13
polylan_submitter/games/api.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
|
from ninja import Router
|
||||||
|
|
||||||
|
from .models import Game
|
||||||
|
from .schemas import GameOut
|
||||||
|
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response=List[GameOut])
|
||||||
|
def list_games(request):
|
||||||
|
return Game.objects.filter(enabled=True)
|
||||||
@ -1,6 +1,6 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class SubmissionsConfig(AppConfig):
|
class GamesConfig(AppConfig):
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
name = "submissions"
|
name = "games"
|
||||||
22
polylan_submitter/games/decorators.py
Normal file
22
polylan_submitter/games/decorators.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
|
||||||
|
from .models import Game
|
||||||
|
|
||||||
|
|
||||||
|
def require_game_enabled(steam_app_id: int):
|
||||||
|
def decorator(view_func):
|
||||||
|
@wraps(view_func)
|
||||||
|
def wrapper(request, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
game = Game.objects.get(steam_app_id=steam_app_id)
|
||||||
|
except Game.DoesNotExist:
|
||||||
|
raise PermissionDenied
|
||||||
|
if not game.enabled:
|
||||||
|
raise PermissionDenied
|
||||||
|
return view_func(request, *args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
32
polylan_submitter/games/migrations/0001_initial.py
Normal file
32
polylan_submitter/games/migrations/0001_initial.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Game",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("steam_app_id", models.PositiveIntegerField(unique=True)),
|
||||||
|
("name", models.CharField(max_length=255)),
|
||||||
|
("enabled", models.BooleanField(default=True)),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_at", models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ["name"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
NOITA_APP_ID = 881100
|
||||||
|
OPUS_MAGNUM_APP_ID = 558990
|
||||||
|
|
||||||
|
|
||||||
|
def seed_games(apps, schema_editor):
|
||||||
|
Game = apps.get_model("games", "Game")
|
||||||
|
Game.objects.get_or_create(
|
||||||
|
steam_app_id=NOITA_APP_ID,
|
||||||
|
defaults={"name": "Noita", "enabled": True},
|
||||||
|
)
|
||||||
|
Game.objects.get_or_create(
|
||||||
|
steam_app_id=OPUS_MAGNUM_APP_ID,
|
||||||
|
defaults={"name": "Opus Magnum", "enabled": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def unseed_games(apps, schema_editor):
|
||||||
|
Game = apps.get_model("games", "Game")
|
||||||
|
Game.objects.filter(steam_app_id__in=[NOITA_APP_ID, OPUS_MAGNUM_APP_ID]).delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("games", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(seed_games, reverse_code=unseed_games),
|
||||||
|
]
|
||||||
31
polylan_submitter/games/migrations/0003_game_path.py
Normal file
31
polylan_submitter/games/migrations/0003_game_path.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
NOITA_APP_ID = 881100
|
||||||
|
OPUS_MAGNUM_APP_ID = 558990
|
||||||
|
|
||||||
|
PATHS = {
|
||||||
|
NOITA_APP_ID: "/noita",
|
||||||
|
OPUS_MAGNUM_APP_ID: "/opus-magnum",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def set_paths(apps, schema_editor):
|
||||||
|
Game = apps.get_model("games", "Game")
|
||||||
|
for app_id, path in PATHS.items():
|
||||||
|
Game.objects.filter(steam_app_id=app_id).update(path=path)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("games", "0002_seed_noita_and_opus_magnum"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="game",
|
||||||
|
name="path",
|
||||||
|
field=models.CharField(default="", max_length=100),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.RunPython(set_paths, reverse_code=migrations.RunPython.noop),
|
||||||
|
]
|
||||||
17
polylan_submitter/games/models.py
Normal file
17
polylan_submitter/games/models.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Game(models.Model):
|
||||||
|
steam_app_id = models.PositiveIntegerField(unique=True)
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
path = models.CharField(max_length=100)
|
||||||
|
enabled = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["name"]
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.name} ({self.steam_app_id})"
|
||||||
7
polylan_submitter/games/schemas.py
Normal file
7
polylan_submitter/games/schemas.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from ninja import Schema
|
||||||
|
|
||||||
|
|
||||||
|
class GameOut(Schema):
|
||||||
|
steam_app_id: int
|
||||||
|
name: str
|
||||||
|
path: str
|
||||||
92
polylan_submitter/market/admin.py
Normal file
92
polylan_submitter/market/admin.py
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from market.models import Market, MarketOption, UserBet, UserPointChange
|
||||||
|
|
||||||
|
|
||||||
|
class MarketOptionInline(admin.TabularInline):
|
||||||
|
model = MarketOption
|
||||||
|
extra = 1
|
||||||
|
fields = ["text"]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Market)
|
||||||
|
class MarketAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ["title", "status", "end_date", "created_by", "created_at"]
|
||||||
|
list_filter = ["status", "created_at"]
|
||||||
|
search_fields = ["uuid", "title"]
|
||||||
|
readonly_fields = [
|
||||||
|
"uuid",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"created_by",
|
||||||
|
"winning_option",
|
||||||
|
]
|
||||||
|
inlines = [MarketOptionInline]
|
||||||
|
fieldsets = (
|
||||||
|
("Info", {"fields": ["uuid", "title", "description"]}),
|
||||||
|
("Configuration", {"fields": ["end_date", "multiplier"]}),
|
||||||
|
("Status", {"fields": ["status", "winning_option"]}),
|
||||||
|
("Metadata", {"fields": ["created_by", "created_at", "updated_at"]}),
|
||||||
|
)
|
||||||
|
|
||||||
|
def has_change_permission(self, request, obj=None):
|
||||||
|
# Prevent any changes to resolved markets
|
||||||
|
if obj and obj.status == Market.Status.RESOLVED:
|
||||||
|
return False
|
||||||
|
return super().has_change_permission(request, obj)
|
||||||
|
|
||||||
|
def save_model(self, request, obj, form, change):
|
||||||
|
if not change: # Creating new market
|
||||||
|
obj.created_by = request.user
|
||||||
|
super().save_model(request, obj, form, change)
|
||||||
|
|
||||||
|
@admin.action(description="Publish selected draft markets")
|
||||||
|
def publish_markets(self, request, queryset):
|
||||||
|
updated = queryset.filter(status=Market.Status.DRAFT).update(
|
||||||
|
status=Market.Status.OPEN
|
||||||
|
)
|
||||||
|
self.message_user(request, f"Published {updated} market(s).")
|
||||||
|
|
||||||
|
@admin.action(description="Close selected markets")
|
||||||
|
def close_markets(self, request, queryset):
|
||||||
|
updated = queryset.filter(status=Market.Status.OPEN).update(
|
||||||
|
status=Market.Status.CLOSED
|
||||||
|
)
|
||||||
|
self.message_user(request, f"Closed {updated} market(s).")
|
||||||
|
|
||||||
|
actions = ["publish_markets", "close_markets"]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(MarketOption)
|
||||||
|
class MarketOptionAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ["text", "market"]
|
||||||
|
list_filter = ["market"]
|
||||||
|
search_fields = ["uuid", "text", "market__title"]
|
||||||
|
readonly_fields = ["uuid"]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(UserBet)
|
||||||
|
class UserBetAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ["user", "option", "amount", "created_at"]
|
||||||
|
list_filter = ["user", "created_at", "option__market"]
|
||||||
|
search_fields = ["uuid", "user__username", "option__text"]
|
||||||
|
readonly_fields = ["uuid", "user", "option", "amount", "created_at", "updated_at"]
|
||||||
|
|
||||||
|
def has_add_permission(self, request):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def has_delete_permission(self, request, obj=None):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(UserPointChange)
|
||||||
|
class UserPointChangeAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ["user", "market", "amount", "reason", "created_at"]
|
||||||
|
list_filter = ["user", "reason", "created_at", "market"]
|
||||||
|
search_fields = ["uuid", "user__username", "market__title"]
|
||||||
|
readonly_fields = ["uuid", "created_at", "updated_at"]
|
||||||
|
|
||||||
|
def has_add_permission(self, request):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def has_delete_permission(self, request, obj=None):
|
||||||
|
return False
|
||||||
184
polylan_submitter/market/api.py
Normal file
184
polylan_submitter/market/api.py
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
from typing import List
|
||||||
|
from ninja import Router
|
||||||
|
from ninja.errors import HttpError
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.db.models import Sum, Prefetch
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
from market.models import Market, MarketOption, UserBet, UserPointChange
|
||||||
|
from market.schemas import (
|
||||||
|
MarketListSchema,
|
||||||
|
ResolveMarketSchema,
|
||||||
|
UserBetCreateSchema,
|
||||||
|
UserBetSchema,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
router = Router(tags=["market"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response=List[MarketListSchema])
|
||||||
|
def list_markets(request):
|
||||||
|
"""List all markets (excludes draft markets)."""
|
||||||
|
markets = Market.objects.exclude(status=Market.Status.DRAFT)
|
||||||
|
# Prefetch options with total_bets annotation sorted by total_bets desc, then text asc
|
||||||
|
options_queryset = MarketOption.objects.annotate(
|
||||||
|
total_bets=Coalesce(Sum("user_bets__amount"), 0)
|
||||||
|
).order_by("-total_bets", "text")
|
||||||
|
|
||||||
|
return markets.prefetch_related(Prefetch("options", queryset=options_queryset))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/user/bets", response=List[UserBetSchema])
|
||||||
|
def list_user_bets(request):
|
||||||
|
"""List all bets placed by the current user."""
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
raise HttpError(401, "Authentication required")
|
||||||
|
|
||||||
|
return (
|
||||||
|
UserBet.objects.filter(user=request.user)
|
||||||
|
.select_related("option__market")
|
||||||
|
.prefetch_related("option__market__options")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{market_uuid}/actions/close")
|
||||||
|
def close_market(request, market_uuid: str):
|
||||||
|
"""Close a market. Admin only."""
|
||||||
|
if not request.user.is_staff:
|
||||||
|
raise HttpError(403, "Permission denied")
|
||||||
|
|
||||||
|
market = get_object_or_404(Market, uuid=market_uuid)
|
||||||
|
market.status = Market.Status.CLOSED
|
||||||
|
market.save(update_fields=["status", "updated_at"])
|
||||||
|
return {"status": Market.Status.CLOSED}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{market_uuid}/actions/resolve", response=MarketListSchema)
|
||||||
|
def resolve_market(request, market_uuid: str, payload: ResolveMarketSchema):
|
||||||
|
"""Resolve a market with a winning option. Admin only."""
|
||||||
|
if not request.user.is_staff:
|
||||||
|
raise HttpError(403, "Permission denied")
|
||||||
|
|
||||||
|
market = get_object_or_404(Market, uuid=market_uuid)
|
||||||
|
winning_option = get_object_or_404(MarketOption, uuid=payload.winning_option_uuid)
|
||||||
|
|
||||||
|
if winning_option.market_id != market.id:
|
||||||
|
raise HttpError(400, "Option does not belong to this market")
|
||||||
|
|
||||||
|
market.winning_option = winning_option
|
||||||
|
market.status = Market.Status.RESOLVED
|
||||||
|
market.save(update_fields=["winning_option", "status", "updated_at"])
|
||||||
|
|
||||||
|
# Calculate and distribute winnings
|
||||||
|
all_bets = list(
|
||||||
|
UserBet.objects.filter(option__market=market).select_related("user")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Calculate total pot
|
||||||
|
total_pot = sum(bet.amount for bet in all_bets)
|
||||||
|
if total_pot == 0:
|
||||||
|
return market
|
||||||
|
|
||||||
|
# Separate winning and losing bets
|
||||||
|
winning_bets = [bet for bet in all_bets if bet.option_id == winning_option.id]
|
||||||
|
losing_bets = [bet for bet in all_bets if bet.option_id != winning_option.id]
|
||||||
|
|
||||||
|
total_winning = sum(bet.amount for bet in winning_bets)
|
||||||
|
|
||||||
|
point_changes = []
|
||||||
|
users_to_update = []
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
# Award payouts to winners with multiplier
|
||||||
|
if total_winning > 0:
|
||||||
|
for bet in winning_bets:
|
||||||
|
payout = round(
|
||||||
|
bet.amount / total_winning * total_pot * market.multiplier
|
||||||
|
)
|
||||||
|
bet.user.points += payout
|
||||||
|
users_to_update.append(bet.user)
|
||||||
|
point_changes.append(
|
||||||
|
UserPointChange(
|
||||||
|
user=bet.user,
|
||||||
|
market=market,
|
||||||
|
amount=payout,
|
||||||
|
reason=UserPointChange.Reason.BET_WON,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Record losing bets (points already deducted)
|
||||||
|
for bet in losing_bets:
|
||||||
|
point_changes.append(
|
||||||
|
UserPointChange(
|
||||||
|
user=bet.user,
|
||||||
|
market=market,
|
||||||
|
amount=-bet.amount,
|
||||||
|
reason=UserPointChange.Reason.BET_LOST,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Bulk update users
|
||||||
|
for user in users_to_update:
|
||||||
|
user.save(update_fields=["points"])
|
||||||
|
|
||||||
|
# Bulk create point changes
|
||||||
|
UserPointChange.objects.bulk_create(point_changes)
|
||||||
|
|
||||||
|
return market
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{market_uuid}/bets", response=UserBetSchema)
|
||||||
|
def create_bet(request, market_uuid: str, payload: UserBetCreateSchema):
|
||||||
|
"""Place a bet on a market option."""
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
raise HttpError(401, "Authentication required")
|
||||||
|
|
||||||
|
market = get_object_or_404(Market, uuid=market_uuid)
|
||||||
|
option = get_object_or_404(MarketOption, uuid=payload.option_uuid)
|
||||||
|
|
||||||
|
if option.market_id != market.id:
|
||||||
|
raise HttpError(400, "Option does not belong to this market")
|
||||||
|
|
||||||
|
if market.status != Market.Status.OPEN:
|
||||||
|
raise HttpError(400, "Market is not open for betting")
|
||||||
|
|
||||||
|
# Check if user already has a bet on a different option in this market
|
||||||
|
existing_bet_on_market = (
|
||||||
|
UserBet.objects.filter(user=request.user, option__market=market)
|
||||||
|
.exclude(option=option)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if existing_bet_on_market:
|
||||||
|
raise HttpError(400, "You can only bet on one option per market")
|
||||||
|
|
||||||
|
# Check if user already has a bet on this option
|
||||||
|
existing_bet = UserBet.objects.filter(user=request.user, option=option).first()
|
||||||
|
if existing_bet and payload.amount < existing_bet.amount:
|
||||||
|
raise HttpError(400, "Cannot decrease bet amount. You can only increase it.")
|
||||||
|
|
||||||
|
# Calculate delta (amount to deduct from user's points)
|
||||||
|
delta = payload.amount - (existing_bet.amount if existing_bet else 0)
|
||||||
|
|
||||||
|
# Check if user has enough points
|
||||||
|
if request.user.points < delta:
|
||||||
|
raise HttpError(400, "Insufficient points for this bet")
|
||||||
|
|
||||||
|
user_bet, created = UserBet.objects.update_or_create(
|
||||||
|
user=request.user,
|
||||||
|
option=option,
|
||||||
|
defaults={"amount": payload.amount},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Deduct points and record the change
|
||||||
|
request.user.points -= delta
|
||||||
|
request.user.save(update_fields=["points"])
|
||||||
|
UserPointChange.objects.create(
|
||||||
|
user=request.user,
|
||||||
|
market=market,
|
||||||
|
amount=-delta,
|
||||||
|
reason=UserPointChange.Reason.BET_PLACED,
|
||||||
|
)
|
||||||
|
|
||||||
|
return user_bet
|
||||||
6
polylan_submitter/market/apps.py
Normal file
6
polylan_submitter/market/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class MarketConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "market"
|
||||||
187
polylan_submitter/market/migrations/0001_initial.py
Normal file
187
polylan_submitter/market/migrations/0001_initial.py
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-05-23 15:45
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Market",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"uuid",
|
||||||
|
models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
|
||||||
|
),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_at", models.DateTimeField(auto_now=True)),
|
||||||
|
("title", models.CharField(max_length=255)),
|
||||||
|
("description", models.TextField(blank=True)),
|
||||||
|
(
|
||||||
|
"type",
|
||||||
|
models.CharField(
|
||||||
|
choices=[("yes_no", "Yes/No"), ("multiple", "Multiple Choice")],
|
||||||
|
default="yes_no",
|
||||||
|
max_length=10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"status",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("open", "Open"),
|
||||||
|
("closed", "Closed"),
|
||||||
|
("resolved", "Resolved"),
|
||||||
|
],
|
||||||
|
default="open",
|
||||||
|
max_length=10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("end_date", models.DateTimeField()),
|
||||||
|
(
|
||||||
|
"created_by",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ["-created_at"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="MarketOption",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"uuid",
|
||||||
|
models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
|
||||||
|
),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_at", models.DateTimeField(auto_now=True)),
|
||||||
|
("text", models.CharField(max_length=255)),
|
||||||
|
("position", models.PositiveIntegerField(default=0)),
|
||||||
|
(
|
||||||
|
"market",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="options",
|
||||||
|
to="market.market",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ["position"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="market",
|
||||||
|
name="winning_option",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="market_won",
|
||||||
|
to="market.marketoption",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="UserBet",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"uuid",
|
||||||
|
models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
|
||||||
|
),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_at", models.DateTimeField(auto_now=True)),
|
||||||
|
("amount", models.PositiveIntegerField()),
|
||||||
|
(
|
||||||
|
"option",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="user_bets",
|
||||||
|
to="market.marketoption",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="bets",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="marketoption",
|
||||||
|
index=models.Index(
|
||||||
|
fields=["market", "position"], name="market_mark_market__8679ce_idx"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="marketoption",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
fields=("market", "position"), name="unique_market_option_position"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="market",
|
||||||
|
index=models.Index(
|
||||||
|
fields=["status", "-created_at"], name="market_mark_status_1ef6c3_idx"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="market",
|
||||||
|
index=models.Index(
|
||||||
|
fields=["end_date"], name="market_mark_end_dat_26bec0_idx"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="userbet",
|
||||||
|
index=models.Index(
|
||||||
|
fields=["user", "option"], name="market_user_user_id_5e43d9_idx"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="userbet",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
fields=("user", "option"), name="unique_user_bet_per_option"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
73
polylan_submitter/market/migrations/0002_userpointchange.py
Normal file
73
polylan_submitter/market/migrations/0002_userpointchange.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-05-23 18:11
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("market", "0001_initial"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="UserPointChange",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"uuid",
|
||||||
|
models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
|
||||||
|
),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_at", models.DateTimeField(auto_now=True)),
|
||||||
|
("amount", models.IntegerField()),
|
||||||
|
(
|
||||||
|
"reason",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("bet_placed", "Bet Placed"),
|
||||||
|
("bet_won", "Bet Won"),
|
||||||
|
("bet_lost", "Bet Lost"),
|
||||||
|
],
|
||||||
|
max_length=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"market",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="point_changes",
|
||||||
|
to="market.market",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="point_changes",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ["-created_at"],
|
||||||
|
"indexes": [
|
||||||
|
models.Index(
|
||||||
|
fields=["user", "-created_at"],
|
||||||
|
name="market_user_user_id_631ba9_idx",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-05-23 18:19
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("market", "0002_userpointchange"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="market",
|
||||||
|
name="type",
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-05-23 18:20
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("market", "0003_remove_market_type"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="marketoption",
|
||||||
|
options={"ordering": ["text"]},
|
||||||
|
),
|
||||||
|
migrations.RemoveConstraint(
|
||||||
|
model_name="marketoption",
|
||||||
|
name="unique_market_option_position",
|
||||||
|
),
|
||||||
|
migrations.RemoveIndex(
|
||||||
|
model_name="marketoption",
|
||||||
|
name="market_mark_market__8679ce_idx",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="marketoption",
|
||||||
|
name="position",
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="marketoption",
|
||||||
|
index=models.Index(
|
||||||
|
fields=["market"], name="market_mark_market__67f63b_idx"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="marketoption",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
fields=("market", "text"), name="unique_market_option_text"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-05-23 18:34
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("market", "0004_alter_marketoption_options_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="market",
|
||||||
|
name="multiplier",
|
||||||
|
field=models.FloatField(default=1.0),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-05-23 18:40
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("market", "0005_market_multiplier"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="market",
|
||||||
|
name="status",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("draft", "Draft"),
|
||||||
|
("open", "Open"),
|
||||||
|
("closed", "Closed"),
|
||||||
|
("resolved", "Resolved"),
|
||||||
|
],
|
||||||
|
default="draft",
|
||||||
|
max_length=10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
114
polylan_submitter/market/models.py
Normal file
114
polylan_submitter/market/models.py
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import uuid
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class BaseModel(models.Model):
|
||||||
|
uuid = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
|
class Market(BaseModel):
|
||||||
|
class Status(models.TextChoices):
|
||||||
|
DRAFT = "draft", "Draft"
|
||||||
|
OPEN = "open", "Open"
|
||||||
|
CLOSED = "closed", "Closed"
|
||||||
|
RESOLVED = "resolved", "Resolved"
|
||||||
|
|
||||||
|
title = models.CharField(max_length=255)
|
||||||
|
description = models.TextField(blank=True)
|
||||||
|
status = models.CharField(
|
||||||
|
max_length=10, choices=Status.choices, default=Status.DRAFT
|
||||||
|
)
|
||||||
|
end_date = models.DateTimeField()
|
||||||
|
multiplier = models.FloatField(default=1.0)
|
||||||
|
created_by = models.ForeignKey("accounts.CustomUser", on_delete=models.PROTECT)
|
||||||
|
winning_option = models.ForeignKey(
|
||||||
|
"MarketOption",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="market_won",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["-created_at"]
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["status", "-created_at"]),
|
||||||
|
models.Index(fields=["end_date"]),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
|
||||||
|
class MarketOption(BaseModel):
|
||||||
|
market = models.ForeignKey(Market, on_delete=models.CASCADE, related_name="options")
|
||||||
|
text = models.CharField(max_length=255)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["text"]
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["market", "text"],
|
||||||
|
name="unique_market_option_text",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["market"]),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.market.title} - {self.text}"
|
||||||
|
|
||||||
|
|
||||||
|
class UserBet(BaseModel):
|
||||||
|
user = models.ForeignKey(
|
||||||
|
"accounts.CustomUser", on_delete=models.CASCADE, related_name="bets"
|
||||||
|
)
|
||||||
|
option = models.ForeignKey(
|
||||||
|
MarketOption, on_delete=models.CASCADE, related_name="user_bets"
|
||||||
|
)
|
||||||
|
amount = models.PositiveIntegerField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["user", "option"],
|
||||||
|
name="unique_user_bet_per_option",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["user", "option"]),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user.username} bet {self.amount} on {self.option.text}"
|
||||||
|
|
||||||
|
|
||||||
|
class UserPointChange(BaseModel):
|
||||||
|
class Reason(models.TextChoices):
|
||||||
|
BET_PLACED = "bet_placed", "Bet Placed"
|
||||||
|
BET_WON = "bet_won", "Bet Won"
|
||||||
|
BET_LOST = "bet_lost", "Bet Lost"
|
||||||
|
|
||||||
|
user = models.ForeignKey(
|
||||||
|
"accounts.CustomUser", on_delete=models.CASCADE, related_name="point_changes"
|
||||||
|
)
|
||||||
|
market = models.ForeignKey(
|
||||||
|
Market, on_delete=models.CASCADE, related_name="point_changes"
|
||||||
|
)
|
||||||
|
amount = models.IntegerField()
|
||||||
|
reason = models.CharField(max_length=20, choices=Reason.choices)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["-created_at"]
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["user", "-created_at"]),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user.username} {self.reason}: {self.amount} pts on {self.market.title}"
|
||||||
59
polylan_submitter/market/schemas.py
Normal file
59
polylan_submitter/market/schemas.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import List, Optional, Any
|
||||||
|
from uuid import UUID
|
||||||
|
from ninja import Schema
|
||||||
|
from pydantic import field_serializer, model_validator
|
||||||
|
|
||||||
|
|
||||||
|
class MarketOptionSchema(Schema):
|
||||||
|
uuid: UUID
|
||||||
|
text: str
|
||||||
|
total_bets: int = 0
|
||||||
|
|
||||||
|
@field_serializer("uuid")
|
||||||
|
def serialize_uuid(self, value: UUID) -> str:
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
class MarketListSchema(Schema):
|
||||||
|
uuid: UUID
|
||||||
|
title: str
|
||||||
|
description: str
|
||||||
|
status: str
|
||||||
|
end_date: datetime
|
||||||
|
multiplier: float = 1.0
|
||||||
|
created_at: datetime
|
||||||
|
options: List[MarketOptionSchema]
|
||||||
|
winning_option: Optional[MarketOptionSchema] = None
|
||||||
|
|
||||||
|
@field_serializer("uuid")
|
||||||
|
def serialize_uuid(self, value: UUID) -> str:
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
class ResolveMarketSchema(Schema):
|
||||||
|
winning_option_uuid: str
|
||||||
|
|
||||||
|
|
||||||
|
class UserBetCreateSchema(Schema):
|
||||||
|
option_uuid: str
|
||||||
|
amount: int
|
||||||
|
|
||||||
|
|
||||||
|
class UserBetSchema(Schema):
|
||||||
|
uuid: UUID
|
||||||
|
amount: int
|
||||||
|
created_at: datetime
|
||||||
|
option: MarketOptionSchema
|
||||||
|
market: Optional[MarketListSchema] = None
|
||||||
|
|
||||||
|
@field_serializer("uuid")
|
||||||
|
def serialize_uuid(self, value: UUID) -> str:
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
@model_validator(mode="before")
|
||||||
|
@classmethod
|
||||||
|
def resolve_market_from_option(cls, data: Any) -> Any:
|
||||||
|
if hasattr(data, "option") and hasattr(data.option, "market"):
|
||||||
|
data.market = data.option.market
|
||||||
|
return data
|
||||||
8
polylan_submitter/market/views.py
Normal file
8
polylan_submitter/market/views.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.http import HttpRequest
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def market_home(request: HttpRequest):
|
||||||
|
return render(request, "market.html", {})
|
||||||
@ -12,18 +12,22 @@ from django.db.models import (
|
|||||||
)
|
)
|
||||||
from ninja import Router, File
|
from ninja import Router, File
|
||||||
from ninja.files import UploadedFile
|
from ninja.files import UploadedFile
|
||||||
|
from ninja.decorators import decorate_view
|
||||||
|
|
||||||
from noita.schemas import ResultsOut, LeaderboardOut
|
from noita.schemas import ResultsOut, LeaderboardOut
|
||||||
from noita.services.objectives import parse_objectives_and_store
|
from noita.services.objectives import parse_objectives_and_store
|
||||||
|
from games.decorators import require_game_enabled
|
||||||
|
|
||||||
from .models import LogfileSubmission, Objectiv, ObjectivPoint, DeathCounter
|
from .models import LogfileSubmission, Objectiv, ObjectivPoint, DeathCounter
|
||||||
from .schemas import NoitaSubmissionOut
|
from .schemas import NoitaSubmissionOut
|
||||||
|
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
|
NOITA_APP_ID = 881100
|
||||||
|
|
||||||
|
|
||||||
@router.get("results", response=ResultsOut)
|
@router.get("results", response=ResultsOut)
|
||||||
|
@decorate_view(require_game_enabled(NOITA_APP_ID))
|
||||||
def get_results(request: HttpRequest):
|
def get_results(request: HttpRequest):
|
||||||
cache_key = f"api:noita:results:{request.user.id}"
|
cache_key = f"api:noita:results:{request.user.id}"
|
||||||
cached_data = cache.get(cache_key)
|
cached_data = cache.get(cache_key)
|
||||||
@ -127,6 +131,7 @@ def get_results(request: HttpRequest):
|
|||||||
|
|
||||||
|
|
||||||
@router.get("leaderboard", response=LeaderboardOut)
|
@router.get("leaderboard", response=LeaderboardOut)
|
||||||
|
@decorate_view(require_game_enabled(NOITA_APP_ID))
|
||||||
def get_leaderboard(request: HttpRequest):
|
def get_leaderboard(request: HttpRequest):
|
||||||
"""
|
"""
|
||||||
Get the global leaderboard for all users ranked by total score.
|
Get the global leaderboard for all users ranked by total score.
|
||||||
@ -232,6 +237,7 @@ def get_leaderboard(request: HttpRequest):
|
|||||||
|
|
||||||
|
|
||||||
@router.post("submit", response={200: NoitaSubmissionOut, 400: dict})
|
@router.post("submit", response={200: NoitaSubmissionOut, 400: dict})
|
||||||
|
@decorate_view(require_game_enabled(NOITA_APP_ID))
|
||||||
def submit_log_file(request: HttpRequest, file: UploadedFile = File(...)):
|
def submit_log_file(request: HttpRequest, file: UploadedFile = File(...)):
|
||||||
"""
|
"""
|
||||||
Submit a Noita run file (log file, screenshot, or video).
|
Submit a Noita run file (log file, screenshot, or video).
|
||||||
|
|||||||
@ -72,7 +72,6 @@ POINTS = {
|
|||||||
"NOLLA": 10,
|
"NOLLA": 10,
|
||||||
"CHAOTIC_TRANSMUTATION": 10,
|
"CHAOTIC_TRANSMUTATION": 10,
|
||||||
"DUPLICATE": 5,
|
"DUPLICATE": 5,
|
||||||
"OMEGA": 10,
|
|
||||||
"BURST_2": 10,
|
"BURST_2": 10,
|
||||||
"BURST_3": 15,
|
"BURST_3": 15,
|
||||||
"BURST_4": 20,
|
"BURST_4": 20,
|
||||||
|
|||||||
6
polylan_submitter/openapi-ts.config.ts
Normal file
6
polylan_submitter/openapi-ts.config.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { defineConfig } from '@hey-api/openapi-ts';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
input: `http://localhost:7777/api/openapi.json`,
|
||||||
|
output: 'src/api/',
|
||||||
|
});
|
||||||
0
polylan_submitter/opus_magnum/__init__.py
Normal file
0
polylan_submitter/opus_magnum/__init__.py
Normal file
@ -1,7 +1,7 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from submissions.models import (
|
from opus_magnum.models import (
|
||||||
SteamAPIKey,
|
SteamAPIKey,
|
||||||
SteamCollection,
|
SteamCollection,
|
||||||
SteamCollectionItem,
|
SteamCollectionItem,
|
||||||
@ -9,7 +9,9 @@ from django.utils import timezone
|
|||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from submissions.utils import verify_and_validate_ocr_date_for_submission
|
from games.decorators import require_game_enabled
|
||||||
|
from opus_magnum.utils import verify_and_validate_ocr_date_for_submission
|
||||||
|
from ninja.decorators import decorate_view
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Submission,
|
Submission,
|
||||||
@ -28,9 +30,11 @@ from .schemas import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
|
OPUS_MAGNUM_APP_ID = 558990
|
||||||
|
|
||||||
|
|
||||||
@router.get("/puzzles", response=List[SteamCollectionItemOut])
|
@router.get("/puzzles", response=List[SteamCollectionItemOut])
|
||||||
|
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||||
def list_puzzles(request):
|
def list_puzzles(request):
|
||||||
"""Get list of available puzzles"""
|
"""Get list of available puzzles"""
|
||||||
return SteamCollectionItem.objects.select_related("collection").filter(
|
return SteamCollectionItem.objects.select_related("collection").filter(
|
||||||
@ -39,6 +43,7 @@ def list_puzzles(request):
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/collection", response=SteamCollectionOut)
|
@router.get("/collection", response=SteamCollectionOut)
|
||||||
|
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||||
def get_collection(request):
|
def get_collection(request):
|
||||||
"""Get the active collection details"""
|
"""Get the active collection details"""
|
||||||
collection = get_object_or_404(SteamCollection, is_active=True)
|
collection = get_object_or_404(SteamCollection, is_active=True)
|
||||||
@ -46,6 +51,7 @@ def get_collection(request):
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/submissions", response=List[SubmissionOut])
|
@router.get("/submissions", response=List[SubmissionOut])
|
||||||
|
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||||
@paginate
|
@paginate
|
||||||
def list_submissions(request):
|
def list_submissions(request):
|
||||||
"""Get paginated list of submissions"""
|
"""Get paginated list of submissions"""
|
||||||
@ -55,6 +61,7 @@ def list_submissions(request):
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/submissions/{submission_id}", response=SubmissionOut)
|
@router.get("/submissions/{submission_id}", response=SubmissionOut)
|
||||||
|
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||||
def get_submission(request, submission_id: str):
|
def get_submission(request, submission_id: str):
|
||||||
"""Get detailed submission by ID"""
|
"""Get detailed submission by ID"""
|
||||||
return get_object_or_404(
|
return get_object_or_404(
|
||||||
@ -66,6 +73,7 @@ def get_submission(request, submission_id: str):
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/submissions", response=SubmissionOut)
|
@router.post("/submissions", response=SubmissionOut)
|
||||||
|
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||||
def create_submission(
|
def create_submission(
|
||||||
request, data: SubmissionIn, files: List[UploadedFile] = File(...)
|
request, data: SubmissionIn, files: List[UploadedFile] = File(...)
|
||||||
):
|
):
|
||||||
@ -198,6 +206,7 @@ def create_submission(
|
|||||||
|
|
||||||
|
|
||||||
@router.put("/responses/{response_id}/validate", response=PuzzleResponseOut)
|
@router.put("/responses/{response_id}/validate", response=PuzzleResponseOut)
|
||||||
|
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||||
def validate_response(request, response_id: int, data: ValidationIn):
|
def validate_response(request, response_id: int, data: ValidationIn):
|
||||||
"""Manually validate a puzzle response"""
|
"""Manually validate a puzzle response"""
|
||||||
|
|
||||||
@ -233,6 +242,7 @@ def validate_response(request, response_id: int, data: ValidationIn):
|
|||||||
|
|
||||||
|
|
||||||
@router.put("/responses/{response_id}/validate/auto", response=PuzzleResponseOut)
|
@router.put("/responses/{response_id}/validate/auto", response=PuzzleResponseOut)
|
||||||
|
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||||
def validate_auto(request, response_id: int):
|
def validate_auto(request, response_id: int):
|
||||||
"""Try to auto validate a puzzle response"""
|
"""Try to auto validate a puzzle response"""
|
||||||
|
|
||||||
@ -248,6 +258,7 @@ def validate_auto(request, response_id: int):
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/responses/needs-validation", response=List[PuzzleResponseOut])
|
@router.get("/responses/needs-validation", response=List[PuzzleResponseOut])
|
||||||
|
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||||
def list_responses_needing_validation(request):
|
def list_responses_needing_validation(request):
|
||||||
"""Get all responses that need manual validation"""
|
"""Get all responses that need manual validation"""
|
||||||
|
|
||||||
@ -263,6 +274,7 @@ def list_responses_needing_validation(request):
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/submissions/{submission_id}/validate", response=SubmissionOut)
|
@router.post("/submissions/{submission_id}/validate", response=SubmissionOut)
|
||||||
|
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||||
def validate_submission(request, submission_id: str):
|
def validate_submission(request, submission_id: str):
|
||||||
"""Mark entire submission as validated"""
|
"""Mark entire submission as validated"""
|
||||||
|
|
||||||
@ -291,6 +303,7 @@ def validate_submission(request, submission_id: str):
|
|||||||
|
|
||||||
|
|
||||||
@router.delete("/submissions/{submission_id}")
|
@router.delete("/submissions/{submission_id}")
|
||||||
|
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||||
def delete_submission(request, submission_id: str):
|
def delete_submission(request, submission_id: str):
|
||||||
"""Delete a submission (admin only)"""
|
"""Delete a submission (admin only)"""
|
||||||
|
|
||||||
@ -307,6 +320,7 @@ def delete_submission(request, submission_id: str):
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/stats")
|
@router.get("/stats")
|
||||||
|
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||||
def get_stats(request):
|
def get_stats(request):
|
||||||
"""Get submission statistics"""
|
"""Get submission statistics"""
|
||||||
|
|
||||||
6
polylan_submitter/opus_magnum/apps.py
Normal file
6
polylan_submitter/opus_magnum/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class OpusMagnumConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "opus_magnum"
|
||||||
@ -3,8 +3,8 @@ Django management command to fetch Steam Workshop collections
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
from submissions.utils import create_or_update_collection
|
from opus_magnum.utils import create_or_update_collection
|
||||||
from submissions.models import SteamAPIKey, SteamCollection
|
from opus_magnum.models import SteamAPIKey, SteamCollection
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
@ -34,7 +34,7 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Check if collection already exists
|
# Check if collection already exists
|
||||||
from submissions.utils import SteamCollectionFetcher
|
from opus_magnum.utils import SteamCollectionFetcher
|
||||||
|
|
||||||
fetcher = SteamCollectionFetcher(api_key.api_key)
|
fetcher = SteamCollectionFetcher(api_key.api_key)
|
||||||
collection_id = fetcher.extract_collection_id(url)
|
collection_id = fetcher.extract_collection_id(url)
|
||||||
@ -1,6 +1,6 @@
|
|||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from submissions.utils import verify_and_validate_ocr_date_for_submission
|
from opus_magnum.utils import verify_and_validate_ocr_date_for_submission
|
||||||
from submissions.models import SubmissionFile
|
from opus_magnum.models import SubmissionFile
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
@ -205,7 +205,7 @@ class Migration(migrations.Migration):
|
|||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
related_name="items",
|
related_name="items",
|
||||||
to="submissions.steamcollection",
|
to="opus_magnum.steamcollection",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -5,7 +5,7 @@ from django.db import migrations
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("submissions", "0001_initial"),
|
("opus_magnum", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@ -5,7 +5,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("submissions", "0002_delete_collection"),
|
("opus_magnum", "0002_delete_collection"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@ -1,7 +1,7 @@
|
|||||||
# Generated by Django 5.2.7 on 2025-10-29 01:32
|
# Generated by Django 5.2.7 on 2025-10-29 01:32
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import submissions.models
|
import opus_magnum.models
|
||||||
import uuid
|
import uuid
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
@ -9,7 +9,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("submissions", "0003_steamapikey"),
|
("opus_magnum", "0003_steamapikey"),
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -160,7 +160,7 @@ class Migration(migrations.Migration):
|
|||||||
help_text="The puzzle this response is for",
|
help_text="The puzzle this response is for",
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
related_name="responses",
|
related_name="responses",
|
||||||
to="submissions.steamcollectionitem",
|
to="opus_magnum.steamcollectionitem",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@ -168,7 +168,7 @@ class Migration(migrations.Migration):
|
|||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
related_name="responses",
|
related_name="responses",
|
||||||
to="submissions.submission",
|
to="opus_magnum.submission",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -195,7 +195,7 @@ class Migration(migrations.Migration):
|
|||||||
"file",
|
"file",
|
||||||
models.FileField(
|
models.FileField(
|
||||||
help_text="Uploaded file (image/gif)",
|
help_text="Uploaded file (image/gif)",
|
||||||
upload_to=submissions.models.submission_file_upload_path,
|
upload_to=opus_magnum.models.submission_file_upload_path,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@ -239,7 +239,7 @@ class Migration(migrations.Migration):
|
|||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
related_name="files",
|
related_name="files",
|
||||||
to="submissions.puzzleresponse",
|
to="opus_magnum.puzzleresponse",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -5,7 +5,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("submissions", "0004_submission_puzzleresponse_submissionfile"),
|
("opus_magnum", "0004_submission_puzzleresponse_submissionfile"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@ -5,7 +5,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("submissions", "0005_alter_submission_notes"),
|
("opus_magnum", "0005_alter_submission_notes"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@ -5,7 +5,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("submissions", "0006_remove_puzzleresponse_ocr_confidence_score_and_more"),
|
("opus_magnum", "0006_remove_puzzleresponse_ocr_confidence_score_and_more"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@ -5,7 +5,7 @@ from django.db import migrations
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("submissions", "0007_submission_manual_validation_requested"),
|
("opus_magnum", "0007_submission_manual_validation_requested"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@ -7,7 +7,7 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("animations", "0001_initial"),
|
("animations", "0001_initial"),
|
||||||
("submissions", "0008_alter_puzzleresponse_unique_together"),
|
("opus_magnum", "0008_alter_puzzleresponse_unique_together"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@ -5,7 +5,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("submissions", "0009_steamcollectionitem_points_factor"),
|
("opus_magnum", "0009_steamcollectionitem_points_factor"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@ -5,7 +5,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("submissions", "0010_alter_puzzleresponse_validated_area_and_more"),
|
("opus_magnum", "0010_alter_puzzleresponse_validated_area_and_more"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@ -6,7 +6,7 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
(
|
(
|
||||||
"submissions",
|
"opus_magnum",
|
||||||
"0011_alter_puzzleresponse_area_alter_puzzleresponse_cost_and_more",
|
"0011_alter_puzzleresponse_area_alter_puzzleresponse_cost_and_more",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@ -7,7 +7,7 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("animations", "0002_puzzlepointsvalue"),
|
("animations", "0002_puzzlepointsvalue"),
|
||||||
("submissions", "0012_alter_puzzleresponse_validated_area_and_more"),
|
("opus_magnum", "0012_alter_puzzleresponse_validated_area_and_more"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@ -5,7 +5,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("submissions", "0013_steamcollectionitem_points_value"),
|
("opus_magnum", "0013_steamcollectionitem_points_value"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
# Data migration: Copy all data from submissions app tables to opus_magnum app tables
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_data_forward(apps, schema_editor):
|
||||||
|
"""Copy data from submissions_* tables to opus_magnum_* tables"""
|
||||||
|
# Use raw SQL to copy data while preserving all fields
|
||||||
|
with schema_editor.connection.cursor() as cursor:
|
||||||
|
# Copy SteamAPIKey
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO opus_magnum_steamapikey
|
||||||
|
SELECT * FROM submissions_steamapikey
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Copy SteamCollection
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO opus_magnum_steamcollection
|
||||||
|
SELECT * FROM submissions_steamcollection
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Copy SteamCollectionItem
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO opus_magnum_steamcollectionitem
|
||||||
|
SELECT * FROM submissions_steamcollectionitem
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Copy Submission
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO opus_magnum_submission
|
||||||
|
SELECT * FROM submissions_submission
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Copy PuzzleResponse
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO opus_magnum_puzzleresponse
|
||||||
|
SELECT * FROM submissions_puzzleresponse
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Copy SubmissionFile
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO opus_magnum_submissionfile
|
||||||
|
SELECT * FROM submissions_submissionfile
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_data_backward(apps, schema_editor):
|
||||||
|
"""Delete all data from opus_magnum_* tables"""
|
||||||
|
with schema_editor.connection.cursor() as cursor:
|
||||||
|
# Delete in reverse order of foreign key dependencies
|
||||||
|
cursor.execute("DELETE FROM opus_magnum_submissionfile")
|
||||||
|
cursor.execute("DELETE FROM opus_magnum_puzzleresponse")
|
||||||
|
cursor.execute("DELETE FROM opus_magnum_submission")
|
||||||
|
cursor.execute("DELETE FROM opus_magnum_steamcollectionitem")
|
||||||
|
cursor.execute("DELETE FROM opus_magnum_steamcollection")
|
||||||
|
cursor.execute("DELETE FROM opus_magnum_steamapikey")
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("opus_magnum", "0014_steamcollection_accepting_submissions"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(migrate_data_forward, migrate_data_backward),
|
||||||
|
]
|
||||||
@ -221,6 +221,7 @@ class UserInfoOut(Schema):
|
|||||||
first_name: Optional[str] = None
|
first_name: Optional[str] = None
|
||||||
last_name: Optional[str] = None
|
last_name: Optional[str] = None
|
||||||
email: Optional[str] = None
|
email: Optional[str] = None
|
||||||
|
points: int = 0
|
||||||
is_authenticated: bool
|
is_authenticated: bool
|
||||||
is_staff: bool
|
is_staff: bool
|
||||||
is_superuser: bool
|
is_superuser: bool
|
||||||
1
polylan_submitter/opus_magnum/tests.py
Normal file
1
polylan_submitter/opus_magnum/tests.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Create your tests here.
|
||||||
@ -4,7 +4,7 @@ Utilities for fetching Steam Workshop collection data using Steam Web API
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
import requests
|
import requests
|
||||||
from submissions.models import SteamCollection, SteamCollectionItem, SubmissionFile
|
from opus_magnum.models import SteamCollection, SteamCollectionItem, SubmissionFile
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -7,7 +7,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc -b && vite build",
|
"build": "vue-tsc -b && vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"schema": "openapi-ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.16",
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
@ -21,6 +22,7 @@
|
|||||||
"vue": "^3.5.22"
|
"vue": "^3.5.22"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@hey-api/openapi-ts": "^0.97.2",
|
||||||
"@mdi/font": "^7.4.47",
|
"@mdi/font": "^7.4.47",
|
||||||
"@types/node": "^24.6.0",
|
"@types/node": "^24.6.0",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
|
|||||||
@ -36,6 +36,9 @@ importers:
|
|||||||
specifier: ^3.5.22
|
specifier: ^3.5.22
|
||||||
version: 3.5.22(typescript@5.9.3)
|
version: 3.5.22(typescript@5.9.3)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@hey-api/openapi-ts':
|
||||||
|
specifier: ^0.97.2
|
||||||
|
version: 0.97.2(typescript@5.9.3)
|
||||||
'@mdi/font':
|
'@mdi/font':
|
||||||
specifier: ^7.4.47
|
specifier: ^7.4.47
|
||||||
version: 7.4.47
|
version: 7.4.47
|
||||||
@ -236,6 +239,31 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@hey-api/codegen-core@0.8.1':
|
||||||
|
resolution: {integrity: sha512-Iciv2vUCJTW9lWM/ROvyZLblmcbYJHPuXfzb1SzeDVVn4xEXu2ilLU1pq3fn+09FZ/Y0P7VyvRE47UDU6om8xA==}
|
||||||
|
engines: {node: '>=22.13.0'}
|
||||||
|
|
||||||
|
'@hey-api/json-schema-ref-parser@1.4.2':
|
||||||
|
resolution: {integrity: sha512-ZhCFSKI2ipZHEbgmtUHdyddvRU3wJ4elgCfYUC7T7hZa4EivSrVflTQf2w+v3TuaYxR1Y2V2kq3otqTttrrK8Q==}
|
||||||
|
engines: {node: '>=22.13.0'}
|
||||||
|
|
||||||
|
'@hey-api/openapi-ts@0.97.2':
|
||||||
|
resolution: {integrity: sha512-nA+y0/I5O9loQMeJKumi6BQ40/Y71N0hIMmXZ/I7rh8jEOzYxSxmf5a4TBEI2Ap4RAfZyh7RJzJfVzT98KUYQQ==}
|
||||||
|
engines: {node: '>=22.13.0'}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=5.5.3 || >=6.0.0 || 6.0.1-rc'
|
||||||
|
|
||||||
|
'@hey-api/shared@0.4.4':
|
||||||
|
resolution: {integrity: sha512-UZgaQNEdo/OSGLeNXhSv0VQTHQQm5Q2mHOuoYhFPJkNvLVrz7KZtGdKR8O4QPrhyblshxY+caJli08WKM0gREg==}
|
||||||
|
engines: {node: '>=22.13.0'}
|
||||||
|
|
||||||
|
'@hey-api/spec-types@0.2.0':
|
||||||
|
resolution: {integrity: sha512-ibQ8Is7evMavzr8GNyJCcTg975d8DpaMUyLmOrQ85UBdy1l6t1KuRAwgChAbesJsIlNV6gjmlXruWyegDX18Fg==}
|
||||||
|
|
||||||
|
'@hey-api/types@0.1.4':
|
||||||
|
resolution: {integrity: sha512-thWfawrDIP7wSI9ioT13I5soaaqB5vAPIiZmgD8PbeEVKNrkonc0N/Sjj97ezl7oQgusZmaNphGdMKipPO6IBg==}
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.13':
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||||
|
|
||||||
@ -252,6 +280,13 @@ packages:
|
|||||||
'@jridgewell/trace-mapping@0.3.31':
|
'@jridgewell/trace-mapping@0.3.31':
|
||||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||||
|
|
||||||
|
'@jsdevtools/ono@7.1.3':
|
||||||
|
resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==}
|
||||||
|
|
||||||
|
'@lukeed/ms@2.0.2':
|
||||||
|
resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
'@mdi/font@7.4.47':
|
'@mdi/font@7.4.47':
|
||||||
resolution: {integrity: sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==}
|
resolution: {integrity: sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==}
|
||||||
|
|
||||||
@ -471,6 +506,9 @@ packages:
|
|||||||
'@types/estree@1.0.8':
|
'@types/estree@1.0.8':
|
||||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||||
|
|
||||||
|
'@types/json-schema@7.0.15':
|
||||||
|
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||||
|
|
||||||
'@types/node@24.9.2':
|
'@types/node@24.9.2':
|
||||||
resolution: {integrity: sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==}
|
resolution: {integrity: sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==}
|
||||||
|
|
||||||
@ -566,16 +604,54 @@ packages:
|
|||||||
alien-signals@3.0.3:
|
alien-signals@3.0.3:
|
||||||
resolution: {integrity: sha512-2JXjom6R7ZwrISpUphLhf4htUq1aKRCennTJ6u9kFfr3sLmC9+I4CxxVi+McoFnIg+p1HnVrfLT/iCt4Dlz//Q==}
|
resolution: {integrity: sha512-2JXjom6R7ZwrISpUphLhf4htUq1aKRCennTJ6u9kFfr3sLmC9+I4CxxVi+McoFnIg+p1HnVrfLT/iCt4Dlz//Q==}
|
||||||
|
|
||||||
|
ansi-colors@4.1.3:
|
||||||
|
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
argparse@2.0.1:
|
||||||
|
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||||
|
|
||||||
birpc@2.6.1:
|
birpc@2.6.1:
|
||||||
resolution: {integrity: sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==}
|
resolution: {integrity: sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==}
|
||||||
|
|
||||||
bmp-js@0.1.0:
|
bmp-js@0.1.0:
|
||||||
resolution: {integrity: sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==}
|
resolution: {integrity: sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==}
|
||||||
|
|
||||||
|
bundle-name@4.1.0:
|
||||||
|
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
c12@3.3.4:
|
||||||
|
resolution: {integrity: sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==}
|
||||||
|
peerDependencies:
|
||||||
|
magicast: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
magicast:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
chokidar@5.0.0:
|
||||||
|
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
|
||||||
|
engines: {node: '>= 20.19.0'}
|
||||||
|
|
||||||
|
color-support@1.1.3:
|
||||||
|
resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
commander@14.0.3:
|
||||||
|
resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
|
confbox@0.2.4:
|
||||||
|
resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==}
|
||||||
|
|
||||||
copy-anything@4.0.5:
|
copy-anything@4.0.5:
|
||||||
resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==}
|
resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
cross-spawn@7.0.6:
|
||||||
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
csstype@3.1.3:
|
csstype@3.1.3:
|
||||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||||
|
|
||||||
@ -585,10 +661,32 @@ packages:
|
|||||||
dayjs@1.11.20:
|
dayjs@1.11.20:
|
||||||
resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==}
|
resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==}
|
||||||
|
|
||||||
|
default-browser-id@5.0.1:
|
||||||
|
resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
default-browser@5.5.0:
|
||||||
|
resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
define-lazy-prop@3.0.0:
|
||||||
|
resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
defu@6.1.7:
|
||||||
|
resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
|
||||||
|
|
||||||
|
destr@2.0.5:
|
||||||
|
resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==}
|
||||||
|
|
||||||
detect-libc@2.1.2:
|
detect-libc@2.1.2:
|
||||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
dotenv@17.4.2:
|
||||||
|
resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
enhanced-resolve@5.18.3:
|
enhanced-resolve@5.18.3:
|
||||||
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
|
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
@ -605,6 +703,9 @@ packages:
|
|||||||
estree-walker@2.0.2:
|
estree-walker@2.0.2:
|
||||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||||
|
|
||||||
|
exsolve@1.0.8:
|
||||||
|
resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
|
||||||
|
|
||||||
fdir@6.5.0:
|
fdir@6.5.0:
|
||||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
@ -619,6 +720,13 @@ packages:
|
|||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
|
get-tsconfig@4.14.0:
|
||||||
|
resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==}
|
||||||
|
|
||||||
|
giget@3.2.0:
|
||||||
|
resolution: {integrity: sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
graceful-fs@4.2.11:
|
graceful-fs@4.2.11:
|
||||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||||
|
|
||||||
@ -632,9 +740,23 @@ packages:
|
|||||||
resolution: {integrity: sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==}
|
resolution: {integrity: sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
|
is-docker@3.0.0:
|
||||||
|
resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
|
||||||
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
is-electron@2.2.2:
|
is-electron@2.2.2:
|
||||||
resolution: {integrity: sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==}
|
resolution: {integrity: sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==}
|
||||||
|
|
||||||
|
is-in-ssh@1.0.0:
|
||||||
|
resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
|
is-inside-container@1.0.0:
|
||||||
|
resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==}
|
||||||
|
engines: {node: '>=14.16'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
is-url@1.2.4:
|
is-url@1.2.4:
|
||||||
resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==}
|
resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==}
|
||||||
|
|
||||||
@ -642,10 +764,21 @@ packages:
|
|||||||
resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==}
|
resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
is-wsl@3.1.1:
|
||||||
|
resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
|
isexe@2.0.0:
|
||||||
|
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||||
|
|
||||||
jiti@2.6.1:
|
jiti@2.6.1:
|
||||||
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
js-yaml@4.1.1:
|
||||||
|
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
lightningcss-android-arm64@1.30.2:
|
lightningcss-android-arm64@1.30.2:
|
||||||
resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
|
resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
@ -739,6 +872,13 @@ packages:
|
|||||||
encoding:
|
encoding:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
ohash@2.0.11:
|
||||||
|
resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
|
||||||
|
|
||||||
|
open@11.0.0:
|
||||||
|
resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
opencollective-postinstall@2.0.3:
|
opencollective-postinstall@2.0.3:
|
||||||
resolution: {integrity: sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==}
|
resolution: {integrity: sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -746,9 +886,19 @@ packages:
|
|||||||
path-browserify@1.0.1:
|
path-browserify@1.0.1:
|
||||||
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
||||||
|
|
||||||
|
path-key@3.1.1:
|
||||||
|
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
pathe@2.0.3:
|
||||||
|
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||||
|
|
||||||
perfect-debounce@1.0.0:
|
perfect-debounce@1.0.0:
|
||||||
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
|
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
|
||||||
|
|
||||||
|
perfect-debounce@2.1.0:
|
||||||
|
resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==}
|
||||||
|
|
||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
@ -765,13 +915,30 @@ packages:
|
|||||||
typescript:
|
typescript:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
pkg-types@2.3.1:
|
||||||
|
resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==}
|
||||||
|
|
||||||
postcss@8.5.6:
|
postcss@8.5.6:
|
||||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
|
powershell-utils@0.1.0:
|
||||||
|
resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
|
rc9@3.0.1:
|
||||||
|
resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==}
|
||||||
|
|
||||||
|
readdirp@5.0.0:
|
||||||
|
resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
|
||||||
|
engines: {node: '>= 20.19.0'}
|
||||||
|
|
||||||
regenerator-runtime@0.13.11:
|
regenerator-runtime@0.13.11:
|
||||||
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
|
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
|
||||||
|
|
||||||
|
resolve-pkg-maps@1.0.0:
|
||||||
|
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||||
|
|
||||||
rfdc@1.4.1:
|
rfdc@1.4.1:
|
||||||
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
||||||
|
|
||||||
@ -780,6 +947,23 @@ packages:
|
|||||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
run-applescript@7.1.0:
|
||||||
|
resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
semver@7.7.4:
|
||||||
|
resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
shebang-command@2.0.0:
|
||||||
|
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
shebang-regex@3.0.0:
|
||||||
|
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
source-map-js@1.2.1:
|
source-map-js@1.2.1:
|
||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@ -886,6 +1070,15 @@ packages:
|
|||||||
whatwg-url@5.0.0:
|
whatwg-url@5.0.0:
|
||||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||||
|
|
||||||
|
which@2.0.2:
|
||||||
|
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
wsl-utils@0.3.1:
|
||||||
|
resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
yaml@2.8.4:
|
yaml@2.8.4:
|
||||||
resolution: {integrity: sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==}
|
resolution: {integrity: sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==}
|
||||||
engines: {node: '>= 14.6'}
|
engines: {node: '>= 14.6'}
|
||||||
@ -987,6 +1180,56 @@ snapshots:
|
|||||||
'@esbuild/win32-x64@0.25.11':
|
'@esbuild/win32-x64@0.25.11':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@hey-api/codegen-core@0.8.1':
|
||||||
|
dependencies:
|
||||||
|
'@hey-api/types': 0.1.4
|
||||||
|
ansi-colors: 4.1.3
|
||||||
|
c12: 3.3.4
|
||||||
|
color-support: 1.1.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- magicast
|
||||||
|
|
||||||
|
'@hey-api/json-schema-ref-parser@1.4.2':
|
||||||
|
dependencies:
|
||||||
|
'@jsdevtools/ono': 7.1.3
|
||||||
|
'@types/json-schema': 7.0.15
|
||||||
|
js-yaml: 4.1.1
|
||||||
|
|
||||||
|
'@hey-api/openapi-ts@0.97.2(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@hey-api/codegen-core': 0.8.1
|
||||||
|
'@hey-api/json-schema-ref-parser': 1.4.2
|
||||||
|
'@hey-api/shared': 0.4.4
|
||||||
|
'@hey-api/spec-types': 0.2.0
|
||||||
|
'@hey-api/types': 0.1.4
|
||||||
|
'@lukeed/ms': 2.0.2
|
||||||
|
ansi-colors: 4.1.3
|
||||||
|
color-support: 1.1.3
|
||||||
|
commander: 14.0.3
|
||||||
|
get-tsconfig: 4.14.0
|
||||||
|
typescript: 5.9.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- magicast
|
||||||
|
|
||||||
|
'@hey-api/shared@0.4.4':
|
||||||
|
dependencies:
|
||||||
|
'@hey-api/codegen-core': 0.8.1
|
||||||
|
'@hey-api/json-schema-ref-parser': 1.4.2
|
||||||
|
'@hey-api/spec-types': 0.2.0
|
||||||
|
'@hey-api/types': 0.1.4
|
||||||
|
ansi-colors: 4.1.3
|
||||||
|
cross-spawn: 7.0.6
|
||||||
|
open: 11.0.0
|
||||||
|
semver: 7.7.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- magicast
|
||||||
|
|
||||||
|
'@hey-api/spec-types@0.2.0':
|
||||||
|
dependencies:
|
||||||
|
'@hey-api/types': 0.1.4
|
||||||
|
|
||||||
|
'@hey-api/types@0.1.4': {}
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.13':
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
@ -1006,6 +1249,10 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
'@jsdevtools/ono@7.1.3': {}
|
||||||
|
|
||||||
|
'@lukeed/ms@2.0.2': {}
|
||||||
|
|
||||||
'@mdi/font@7.4.47': {}
|
'@mdi/font@7.4.47': {}
|
||||||
|
|
||||||
'@rolldown/pluginutils@1.0.0-beta.29': {}
|
'@rolldown/pluginutils@1.0.0-beta.29': {}
|
||||||
@ -1153,6 +1400,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
|
|
||||||
|
'@types/json-schema@7.0.15': {}
|
||||||
|
|
||||||
'@types/node@24.9.2':
|
'@types/node@24.9.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.16.0
|
undici-types: 7.16.0
|
||||||
@ -1281,22 +1530,76 @@ snapshots:
|
|||||||
|
|
||||||
alien-signals@3.0.3: {}
|
alien-signals@3.0.3: {}
|
||||||
|
|
||||||
|
ansi-colors@4.1.3: {}
|
||||||
|
|
||||||
|
argparse@2.0.1: {}
|
||||||
|
|
||||||
birpc@2.6.1: {}
|
birpc@2.6.1: {}
|
||||||
|
|
||||||
bmp-js@0.1.0: {}
|
bmp-js@0.1.0: {}
|
||||||
|
|
||||||
|
bundle-name@4.1.0:
|
||||||
|
dependencies:
|
||||||
|
run-applescript: 7.1.0
|
||||||
|
|
||||||
|
c12@3.3.4:
|
||||||
|
dependencies:
|
||||||
|
chokidar: 5.0.0
|
||||||
|
confbox: 0.2.4
|
||||||
|
defu: 6.1.7
|
||||||
|
dotenv: 17.4.2
|
||||||
|
exsolve: 1.0.8
|
||||||
|
giget: 3.2.0
|
||||||
|
jiti: 2.6.1
|
||||||
|
ohash: 2.0.11
|
||||||
|
pathe: 2.0.3
|
||||||
|
perfect-debounce: 2.1.0
|
||||||
|
pkg-types: 2.3.1
|
||||||
|
rc9: 3.0.1
|
||||||
|
|
||||||
|
chokidar@5.0.0:
|
||||||
|
dependencies:
|
||||||
|
readdirp: 5.0.0
|
||||||
|
|
||||||
|
color-support@1.1.3: {}
|
||||||
|
|
||||||
|
commander@14.0.3: {}
|
||||||
|
|
||||||
|
confbox@0.2.4: {}
|
||||||
|
|
||||||
copy-anything@4.0.5:
|
copy-anything@4.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-what: 5.5.0
|
is-what: 5.5.0
|
||||||
|
|
||||||
|
cross-spawn@7.0.6:
|
||||||
|
dependencies:
|
||||||
|
path-key: 3.1.1
|
||||||
|
shebang-command: 2.0.0
|
||||||
|
which: 2.0.2
|
||||||
|
|
||||||
csstype@3.1.3: {}
|
csstype@3.1.3: {}
|
||||||
|
|
||||||
daisyui@5.3.10: {}
|
daisyui@5.3.10: {}
|
||||||
|
|
||||||
dayjs@1.11.20: {}
|
dayjs@1.11.20: {}
|
||||||
|
|
||||||
|
default-browser-id@5.0.1: {}
|
||||||
|
|
||||||
|
default-browser@5.5.0:
|
||||||
|
dependencies:
|
||||||
|
bundle-name: 4.1.0
|
||||||
|
default-browser-id: 5.0.1
|
||||||
|
|
||||||
|
define-lazy-prop@3.0.0: {}
|
||||||
|
|
||||||
|
defu@6.1.7: {}
|
||||||
|
|
||||||
|
destr@2.0.5: {}
|
||||||
|
|
||||||
detect-libc@2.1.2: {}
|
detect-libc@2.1.2: {}
|
||||||
|
|
||||||
|
dotenv@17.4.2: {}
|
||||||
|
|
||||||
enhanced-resolve@5.18.3:
|
enhanced-resolve@5.18.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
@ -1335,6 +1638,8 @@ snapshots:
|
|||||||
|
|
||||||
estree-walker@2.0.2: {}
|
estree-walker@2.0.2: {}
|
||||||
|
|
||||||
|
exsolve@1.0.8: {}
|
||||||
|
|
||||||
fdir@6.5.0(picomatch@4.0.3):
|
fdir@6.5.0(picomatch@4.0.3):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.3
|
||||||
@ -1342,6 +1647,12 @@ snapshots:
|
|||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
get-tsconfig@4.14.0:
|
||||||
|
dependencies:
|
||||||
|
resolve-pkg-maps: 1.0.0
|
||||||
|
|
||||||
|
giget@3.2.0: {}
|
||||||
|
|
||||||
graceful-fs@4.2.11: {}
|
graceful-fs@4.2.11: {}
|
||||||
|
|
||||||
hookable@5.5.3: {}
|
hookable@5.5.3: {}
|
||||||
@ -1350,14 +1661,32 @@ snapshots:
|
|||||||
|
|
||||||
install@0.13.0: {}
|
install@0.13.0: {}
|
||||||
|
|
||||||
|
is-docker@3.0.0: {}
|
||||||
|
|
||||||
is-electron@2.2.2: {}
|
is-electron@2.2.2: {}
|
||||||
|
|
||||||
|
is-in-ssh@1.0.0: {}
|
||||||
|
|
||||||
|
is-inside-container@1.0.0:
|
||||||
|
dependencies:
|
||||||
|
is-docker: 3.0.0
|
||||||
|
|
||||||
is-url@1.2.4: {}
|
is-url@1.2.4: {}
|
||||||
|
|
||||||
is-what@5.5.0: {}
|
is-what@5.5.0: {}
|
||||||
|
|
||||||
|
is-wsl@3.1.1:
|
||||||
|
dependencies:
|
||||||
|
is-inside-container: 1.0.0
|
||||||
|
|
||||||
|
isexe@2.0.0: {}
|
||||||
|
|
||||||
jiti@2.6.1: {}
|
jiti@2.6.1: {}
|
||||||
|
|
||||||
|
js-yaml@4.1.1:
|
||||||
|
dependencies:
|
||||||
|
argparse: 2.0.1
|
||||||
|
|
||||||
lightningcss-android-arm64@1.30.2:
|
lightningcss-android-arm64@1.30.2:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -1421,12 +1750,29 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
whatwg-url: 5.0.0
|
whatwg-url: 5.0.0
|
||||||
|
|
||||||
|
ohash@2.0.11: {}
|
||||||
|
|
||||||
|
open@11.0.0:
|
||||||
|
dependencies:
|
||||||
|
default-browser: 5.5.0
|
||||||
|
define-lazy-prop: 3.0.0
|
||||||
|
is-in-ssh: 1.0.0
|
||||||
|
is-inside-container: 1.0.0
|
||||||
|
powershell-utils: 0.1.0
|
||||||
|
wsl-utils: 0.3.1
|
||||||
|
|
||||||
opencollective-postinstall@2.0.3: {}
|
opencollective-postinstall@2.0.3: {}
|
||||||
|
|
||||||
path-browserify@1.0.1: {}
|
path-browserify@1.0.1: {}
|
||||||
|
|
||||||
|
path-key@3.1.1: {}
|
||||||
|
|
||||||
|
pathe@2.0.3: {}
|
||||||
|
|
||||||
perfect-debounce@1.0.0: {}
|
perfect-debounce@1.0.0: {}
|
||||||
|
|
||||||
|
perfect-debounce@2.1.0: {}
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
picomatch@4.0.3: {}
|
picomatch@4.0.3: {}
|
||||||
@ -1438,14 +1784,31 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
||||||
|
pkg-types@2.3.1:
|
||||||
|
dependencies:
|
||||||
|
confbox: 0.2.4
|
||||||
|
exsolve: 1.0.8
|
||||||
|
pathe: 2.0.3
|
||||||
|
|
||||||
postcss@8.5.6:
|
postcss@8.5.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
nanoid: 3.3.11
|
nanoid: 3.3.11
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
powershell-utils@0.1.0: {}
|
||||||
|
|
||||||
|
rc9@3.0.1:
|
||||||
|
dependencies:
|
||||||
|
defu: 6.1.7
|
||||||
|
destr: 2.0.5
|
||||||
|
|
||||||
|
readdirp@5.0.0: {}
|
||||||
|
|
||||||
regenerator-runtime@0.13.11: {}
|
regenerator-runtime@0.13.11: {}
|
||||||
|
|
||||||
|
resolve-pkg-maps@1.0.0: {}
|
||||||
|
|
||||||
rfdc@1.4.1: {}
|
rfdc@1.4.1: {}
|
||||||
|
|
||||||
rollup@4.52.5:
|
rollup@4.52.5:
|
||||||
@ -1476,6 +1839,16 @@ snapshots:
|
|||||||
'@rollup/rollup-win32-x64-msvc': 4.52.5
|
'@rollup/rollup-win32-x64-msvc': 4.52.5
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
run-applescript@7.1.0: {}
|
||||||
|
|
||||||
|
semver@7.7.4: {}
|
||||||
|
|
||||||
|
shebang-command@2.0.0:
|
||||||
|
dependencies:
|
||||||
|
shebang-regex: 3.0.0
|
||||||
|
|
||||||
|
shebang-regex@3.0.0: {}
|
||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
speakingurl@14.0.1: {}
|
speakingurl@14.0.1: {}
|
||||||
@ -1558,6 +1931,15 @@ snapshots:
|
|||||||
tr46: 0.0.3
|
tr46: 0.0.3
|
||||||
webidl-conversions: 3.0.1
|
webidl-conversions: 3.0.1
|
||||||
|
|
||||||
|
which@2.0.2:
|
||||||
|
dependencies:
|
||||||
|
isexe: 2.0.0
|
||||||
|
|
||||||
|
wsl-utils@0.3.1:
|
||||||
|
dependencies:
|
||||||
|
is-wsl: 3.1.1
|
||||||
|
powershell-utils: 0.1.0
|
||||||
|
|
||||||
yaml@2.8.4:
|
yaml@2.8.4:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
from ninja import NinjaAPI
|
from ninja import NinjaAPI
|
||||||
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from submissions.api import router as submissions_router
|
from opus_magnum.api import router as submissions_router
|
||||||
from submissions.schemas import UserInfoOut
|
from opus_magnum.schemas import UserInfoOut
|
||||||
from animations.api import router as results_router
|
from animations.api import router as results_router
|
||||||
from noita.api import router as noita_router
|
from noita.api import router as noita_router
|
||||||
|
from games.api import router as games_router
|
||||||
|
from market.api import router as market_router
|
||||||
|
|
||||||
# Create the main API instance
|
# Create the main API instance
|
||||||
api = NinjaAPI(
|
api = NinjaAPI(
|
||||||
@ -30,10 +33,14 @@ The Noita Submission API allows clients to upload the result of the log file of
|
|||||||
# Add authentication for protected endpoints
|
# Add authentication for protected endpoints
|
||||||
# api.auth = django_auth # Uncomment if you want global auth
|
# api.auth = django_auth # Uncomment if you want global auth
|
||||||
|
|
||||||
# Include the submissions router
|
# Include the opus_magnum router
|
||||||
api.add_router("/submissions/", submissions_router, tags=["submissions"])
|
api.add_router("/opus-magnum/", submissions_router, tags=["opus-magnum"])
|
||||||
api.add_router("/results/", results_router, tags=["results"])
|
api.add_router("/results/", results_router, tags=["results"])
|
||||||
api.add_router("/noita/", noita_router, tags=["noita"])
|
api.add_router("/noita/", noita_router, tags=["noita"])
|
||||||
|
api.add_router("/games/", games_router, tags=["games"])
|
||||||
|
|
||||||
|
if settings.MARKET_ENABLED:
|
||||||
|
api.add_router("/market/", market_router)
|
||||||
|
|
||||||
|
|
||||||
# Health check endpoint
|
# Health check endpoint
|
||||||
@ -63,18 +70,8 @@ def get_user_info(request):
|
|||||||
user = request.user
|
user = request.user
|
||||||
|
|
||||||
if user.is_authenticated:
|
if user.is_authenticated:
|
||||||
return {
|
return user
|
||||||
"id": user.id,
|
|
||||||
"username": user.username,
|
|
||||||
"first_name": user.first_name,
|
|
||||||
"last_name": user.last_name,
|
|
||||||
"email": user.email,
|
|
||||||
"is_authenticated": True,
|
|
||||||
"is_staff": user.is_staff,
|
|
||||||
"is_superuser": user.is_superuser,
|
|
||||||
"cas_groups": getattr(user, "cas_groups", []),
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
return {
|
return {
|
||||||
"is_authenticated": False,
|
"is_authenticated": False,
|
||||||
"is_staff": False,
|
"is_staff": False,
|
||||||
|
|||||||
@ -41,8 +41,10 @@ INSTALLED_APPS = [
|
|||||||
"django_vite",
|
"django_vite",
|
||||||
"accounts",
|
"accounts",
|
||||||
"animations",
|
"animations",
|
||||||
"submissions",
|
"opus_magnum",
|
||||||
"noita",
|
"noita",
|
||||||
|
"games",
|
||||||
|
"market",
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
@ -167,6 +169,9 @@ ALLOWED_SUBMISSION_TYPES = [
|
|||||||
"video/webm",
|
"video/webm",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Market app settings
|
||||||
|
MARKET_ENABLED = os.environ.get("MARKET_ENABLED", "true").lower() == "true"
|
||||||
|
|
||||||
# Authentication backends
|
# Authentication backends
|
||||||
AUTHENTICATION_BACKENDS = [
|
AUTHENTICATION_BACKENDS = [
|
||||||
"django.contrib.auth.backends.ModelBackend",
|
"django.contrib.auth.backends.ModelBackend",
|
||||||
@ -193,7 +198,7 @@ STATICFILES_DIRS = [
|
|||||||
from polylan_submitter.settingsLocal import * # noqa
|
from polylan_submitter.settingsLocal import * # noqa
|
||||||
|
|
||||||
|
|
||||||
import sentry_sdk
|
import sentry_sdk # noqa
|
||||||
|
|
||||||
sentry_sdk.init(
|
sentry_sdk.init(
|
||||||
dsn="https://cc62a4ce3f3470890b43accf02cc6d8c@sentry2.polylan.ch/12",
|
dsn="https://cc62a4ce3f3470890b43accf02cc6d8c@sentry2.polylan.ch/12",
|
||||||
|
|||||||
@ -23,17 +23,31 @@ from django.contrib.auth.decorators import login_required
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
from simple_cas_views import SimpleCASLoginView, SimpleCASLogoutView
|
from simple_cas_views import SimpleCASLoginView, SimpleCASLogoutView
|
||||||
|
from games.decorators import require_game_enabled
|
||||||
|
from market.views import market_home
|
||||||
from .api import api
|
from .api import api
|
||||||
|
|
||||||
|
NOITA_APP_ID = 881100
|
||||||
|
OPUS_MAGNUM_APP_ID = 558990
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def home(request: HttpRequest):
|
def home(request: HttpRequest):
|
||||||
return render(request, "home.html", {})
|
from django.conf import settings
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"home.html",
|
||||||
|
{
|
||||||
|
"market_enabled": settings.MARKET_ENABLED,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@require_game_enabled(OPUS_MAGNUM_APP_ID)
|
||||||
def opus_magnum_home(request: HttpRequest):
|
def opus_magnum_home(request: HttpRequest):
|
||||||
from submissions.models import SteamCollection
|
from opus_magnum.models import SteamCollection
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
@ -45,6 +59,7 @@ def opus_magnum_home(request: HttpRequest):
|
|||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@require_game_enabled(NOITA_APP_ID)
|
||||||
def noita_home(request: HttpRequest):
|
def noita_home(request: HttpRequest):
|
||||||
return render(request, "noita.html", {})
|
return render(request, "noita.html", {})
|
||||||
|
|
||||||
@ -54,6 +69,7 @@ urlpatterns = [
|
|||||||
path("cas/login/", SimpleCASLoginView.as_view(), name="cas_ng_login"),
|
path("cas/login/", SimpleCASLoginView.as_view(), name="cas_ng_login"),
|
||||||
path("cas/logout/", SimpleCASLogoutView.as_view(), name="cas_ng_logout"),
|
path("cas/logout/", SimpleCASLogoutView.as_view(), name="cas_ng_logout"),
|
||||||
path("api/", api.urls),
|
path("api/", api.urls),
|
||||||
|
path("market", market_home, name="market.home"),
|
||||||
path("opus-magnum", opus_magnum_home, name="opus-magnum.home"),
|
path("opus-magnum", opus_magnum_home, name="opus-magnum.home"),
|
||||||
path("noita", noita_home, name="noita.home"),
|
path("noita", noita_home, name="noita.home"),
|
||||||
path("", home, name="home"),
|
path("", home, name="home"),
|
||||||
|
|||||||
@ -1,23 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
|
import { gamesApiListGames } from "./api";
|
||||||
|
import type { GamesApiListGamesResponse } from "./api/types.gen";
|
||||||
|
|
||||||
const games = computed(() => [
|
interface Props {
|
||||||
{
|
marketEnabled?: string | boolean;
|
||||||
id: "opus-magnum",
|
}
|
||||||
title: "Opus Magnum",
|
|
||||||
description: "Submit your best Opus Magnum puzzle solutions",
|
|
||||||
appId: 558990,
|
|
||||||
path: "/opus-magnum",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "noita",
|
|
||||||
title: "Noita",
|
|
||||||
description: "Submit your greatest Noita achievements",
|
|
||||||
appId: 881100,
|
|
||||||
path: "/noita",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
marketEnabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const games = ref<GamesApiListGamesResponse | undefined>();
|
||||||
|
const loading = ref(true);
|
||||||
const imageErrors = ref<Set<number>>(new Set());
|
const imageErrors = ref<Set<number>>(new Set());
|
||||||
|
|
||||||
const getHeaderImage = (appId: number) => {
|
const getHeaderImage = (appId: number) => {
|
||||||
@ -31,6 +26,22 @@ const onImageError = (appId: number) => {
|
|||||||
const navigate = (path: string) => {
|
const navigate = (path: string) => {
|
||||||
window.location.href = path;
|
window.location.href = path;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isMarketEnabled = () => {
|
||||||
|
if (typeof props.marketEnabled === 'string') {
|
||||||
|
return props.marketEnabled === 'true';
|
||||||
|
}
|
||||||
|
return Boolean(props.marketEnabled);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// Fetch games list
|
||||||
|
const response = await gamesApiListGames();
|
||||||
|
if (response.data) {
|
||||||
|
games.value = response.data;
|
||||||
|
}
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -44,13 +55,38 @@ const navigate = (path: string) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading -->
|
||||||
|
<div v-if="loading" class="flex justify-center py-20">
|
||||||
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Cards Grid -->
|
<!-- Cards Grid -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
<div v-else class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
<div v-for="game in games" :key="game.id" @click="navigate(game.path)"
|
<!-- Market Card -->
|
||||||
|
<div v-if="isMarketEnabled()" @click="navigate('/market')"
|
||||||
|
class="card card-xl bg-base-200 shadow-xl hover:shadow-2xl transition-all cursor-pointer transform hover:-translate-y-2 hover:scale-[1.05] hover:bg-base-100 overflow-hidden">
|
||||||
|
<figure class="relative h-60 bg-gradient-to-br from-purple-600 to-blue-600 flex items-center justify-center">
|
||||||
|
<i class="mdi mdi-chart-box text-6xl text-white opacity-80"></i>
|
||||||
|
<div class="absolute inset-0 bg-black/30 group-hover:bg-black/20 transition-colors"></div>
|
||||||
|
</figure>
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-2xl">Market</h2>
|
||||||
|
<p class="text-base-content/70">Place your bets and compete</p>
|
||||||
|
<div class="card-actions justify-end mt-4">
|
||||||
|
<button class="btn btn-primary">
|
||||||
|
<i class="mdi mdi-arrow-right mr-2"></i>
|
||||||
|
Place bets
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Game Cards -->
|
||||||
|
<div v-for="game in games" :key="game.steam_app_id" @click="navigate(game.path)"
|
||||||
class="card card-xl bg-base-200 shadow-xl hover:shadow-2xl transition-all cursor-pointer transform hover:-translate-y-2 hover:scale-[1.05] hover:bg-base-100 overflow-hidden">
|
class="card card-xl bg-base-200 shadow-xl hover:shadow-2xl transition-all cursor-pointer transform hover:-translate-y-2 hover:scale-[1.05] hover:bg-base-100 overflow-hidden">
|
||||||
<figure class="relative h-60 bg-base-300 overflow-hidden">
|
<figure class="relative h-60 bg-base-300 overflow-hidden">
|
||||||
<img v-if="!imageErrors.has(game.appId)" :src="getHeaderImage(game.appId)" :alt="game.title"
|
<img v-if="!imageErrors.has(game.steam_app_id)" :src="getHeaderImage(game.steam_app_id)" :alt="game.name"
|
||||||
@error="onImageError(game.appId)" class="w-full h-full object-cover" />
|
@error="onImageError(game.steam_app_id)" class="w-full h-full object-cover" />
|
||||||
<div v-else
|
<div v-else
|
||||||
class="w-full h-full bg-gradient-to-br from-blue-600 to-blue-400 flex items-center justify-center text-white">
|
class="w-full h-full bg-gradient-to-br from-blue-600 to-blue-400 flex items-center justify-center text-white">
|
||||||
<i class="mdi mdi-gamepad-variant text-5xl"></i>
|
<i class="mdi mdi-gamepad-variant text-5xl"></i>
|
||||||
@ -58,8 +94,7 @@ const navigate = (path: string) => {
|
|||||||
<div class="absolute inset-0 bg-black/30 group-hover:bg-black/20 transition-colors"></div>
|
<div class="absolute inset-0 bg-black/30 group-hover:bg-black/20 transition-colors"></div>
|
||||||
</figure>
|
</figure>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title text-2xl">{{ game.title }}</h2>
|
<h2 class="card-title text-2xl">{{ game.name }}</h2>
|
||||||
<p class="text-base-content/70">{{ game.description }}</p>
|
|
||||||
<div class="card-actions justify-end mt-4">
|
<div class="card-actions justify-end mt-4">
|
||||||
<button class="btn btn-primary">
|
<button class="btn btn-primary">
|
||||||
<i class="mdi mdi-arrow-right mr-2"></i>
|
<i class="mdi mdi-arrow-right mr-2"></i>
|
||||||
@ -75,7 +110,5 @@ const navigate = (path: string) => {
|
|||||||
<p>Select a game above to begin submitting</p>
|
<p>Select a game above to begin submitting</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
106
polylan_submitter/src/Market.vue
Normal file
106
polylan_submitter/src/Market.vue
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted } from "vue";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useMarketStore } from "./stores/market";
|
||||||
|
import MarketCard from "./components/MarketCard.vue";
|
||||||
|
import UserBets from "./components/UserBets.vue";
|
||||||
|
|
||||||
|
const marketStore = useMarketStore();
|
||||||
|
const { markets, userInfo, isLoading } = storeToRefs(marketStore);
|
||||||
|
|
||||||
|
const goHome = () => {
|
||||||
|
window.location.href = "/";
|
||||||
|
};
|
||||||
|
|
||||||
|
const reloadPage = async () => {
|
||||||
|
await marketStore.refreshPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
marketStore.initializeMarketPage();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-base-200">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="navbar bg-base-100 shadow-lg">
|
||||||
|
<div class="container min-w-3/4 mx-auto w-full flex items-center gap-4">
|
||||||
|
<button @click="goHome" class="btn btn-primary btn-sm">
|
||||||
|
<i class="mdi mdi-arrow-left"></i>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
<h1 class="text-xl font-bold">Market</h1>
|
||||||
|
<div class="flex-1"></div>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div v-if="userInfo?.is_authenticated" class="flex items-center gap-2">
|
||||||
|
<div class="text-sm">
|
||||||
|
<span class="font-medium">{{ userInfo.username }}</span>
|
||||||
|
<span v-if="userInfo.is_superuser" class="badge badge-warning badge-xs ml-1">Admin</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-sm text-base-content/70">Not logged in</div>
|
||||||
|
<a href="/api/docs" class="btn btn-xs">API docs</a>
|
||||||
|
<a href="/admin" class="btn btn-xs btn-warning">Admin panel</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="container min-w-3/4 mx-auto px-4 py-8">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h2 class="text-3xl font-bold mb-2">Market</h2>
|
||||||
|
<p class="text-base-content/70">Place your bets on upcoming events</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading -->
|
||||||
|
<div v-if="isLoading" class="flex justify-center py-20">
|
||||||
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<template v-else>
|
||||||
|
<div class="space-y-8">
|
||||||
|
<!-- My Bets Section -->
|
||||||
|
<div v-if="userInfo?.is_authenticated">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h3 class="text-2xl font-bold flex items-center gap-2">
|
||||||
|
<i class="mdi mdi-heart text-error"></i>
|
||||||
|
My Bets
|
||||||
|
</h3>
|
||||||
|
<div class="text-lg font-semibold">
|
||||||
|
<span class="text-primary">{{ userInfo.points }}</span>
|
||||||
|
<span class="text-base-content/60 ml-1">pts</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<UserBets :markets="markets" @refresh="reloadPage" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- All Markets Section -->
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h3 class="text-2xl font-bold flex items-center gap-2">
|
||||||
|
<i class="mdi mdi-list"></i>
|
||||||
|
All Markets
|
||||||
|
</h3>
|
||||||
|
<a v-if="userInfo?.is_superuser" href="/admin/market/market/add/" class="btn btn-sm btn-primary">
|
||||||
|
<i class="mdi mdi-plus"></i>
|
||||||
|
Create Market
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="markets.length === 0" class="alert">
|
||||||
|
<i class="mdi mdi-information mr-2"></i>
|
||||||
|
<span>No markets available</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<MarketCard v-for="market in markets" :key="market.uuid" :market="market" @refresh="reloadPage" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -1,7 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from "vue";
|
import { ref, computed, onMounted } from "vue";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import RankBadge from "@/components/RankBadge.vue";
|
import RankBadge from "@/components/RankBadge.vue";
|
||||||
|
import { useNoitaStore, type Objective } from "@/stores/noita";
|
||||||
import {
|
import {
|
||||||
createColumnHelper,
|
createColumnHelper,
|
||||||
useVueTable,
|
useVueTable,
|
||||||
@ -12,32 +14,11 @@ import {
|
|||||||
type SortingState,
|
type SortingState,
|
||||||
} from "@tanstack/vue-table";
|
} from "@tanstack/vue-table";
|
||||||
|
|
||||||
interface Objective {
|
const noitaStore = useNoitaStore();
|
||||||
objectiv_id: string;
|
const { userInfo, objectives, leaderboard, isLoadingLeaderboard, isUploading } = storeToRefs(noitaStore);
|
||||||
display_string: string;
|
|
||||||
first_seen_at: string | null;
|
|
||||||
count: number;
|
|
||||||
max_count: number;
|
|
||||||
seed: string | null;
|
|
||||||
points_per_objectiv: number;
|
|
||||||
total_points: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userInfo = ref({
|
|
||||||
username: "Player",
|
|
||||||
rank: null as number | null,
|
|
||||||
score: 0,
|
|
||||||
runsSubmitted: 0,
|
|
||||||
deathsCount: 0,
|
|
||||||
isStaff: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const uploadedFiles = ref<File[]>([]);
|
const uploadedFiles = ref<File[]>([]);
|
||||||
const isUploading = ref(false);
|
|
||||||
const isDragover = ref(false);
|
const isDragover = ref(false);
|
||||||
const objectives = ref<Objective[]>([]);
|
|
||||||
const isLoadingLeaderboard = ref(false);
|
|
||||||
const leaderboard = ref<any[]>([]);
|
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<Objective>();
|
const columnHelper = createColumnHelper<Objective>();
|
||||||
const sorting = ref<SortingState>([]);
|
const sorting = ref<SortingState>([]);
|
||||||
@ -153,40 +134,13 @@ const handleDrop = (event: DragEvent) => {
|
|||||||
const submitRun = async () => {
|
const submitRun = async () => {
|
||||||
if (uploadedFiles.value.length === 0) return;
|
if (uploadedFiles.value.length === 0) return;
|
||||||
|
|
||||||
isUploading.value = true;
|
|
||||||
try {
|
try {
|
||||||
for (const file of uploadedFiles.value) {
|
await noitaStore.submitRun(uploadedFiles.value);
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("file", file);
|
|
||||||
|
|
||||||
const response = await fetch("/api/noita/submit", {
|
|
||||||
method: "POST",
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json();
|
|
||||||
alert(`Error submitting ${file.name}: ${error.detail || "Unknown error"}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
console.log("Submission successful:", result);
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadedFiles.value = [];
|
uploadedFiles.value = [];
|
||||||
alert("Run submitted successfully!");
|
alert("Run submitted successfully!");
|
||||||
|
|
||||||
// Refresh objectives, score, and rank after successful submission
|
|
||||||
await Promise.all([
|
|
||||||
fetchUserResults(),
|
|
||||||
fetchLeaderboard(),
|
|
||||||
]);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error submitting run:", error);
|
console.error("Error submitting run:", error);
|
||||||
alert("Error submitting run. Please try again.");
|
alert("Error submitting run. Please try again.");
|
||||||
} finally {
|
|
||||||
isUploading.value = false;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -194,94 +148,18 @@ const goHome = () => {
|
|||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchUserResults = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch("/api/noita/results");
|
|
||||||
if (!response.ok) throw new Error("Failed to fetch results");
|
|
||||||
|
|
||||||
const results = await response.json();
|
|
||||||
userInfo.value.score = results.total_score;
|
|
||||||
userInfo.value.deathsCount = results.deaths_count;
|
|
||||||
userInfo.value.runsSubmitted = results.objectives.length;
|
|
||||||
objectives.value = results.objectives;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching results:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchLeaderboard = async () => {
|
|
||||||
isLoadingLeaderboard.value = true;
|
|
||||||
try {
|
|
||||||
const response = await fetch("/api/noita/leaderboard");
|
|
||||||
if (!response.ok) throw new Error("Failed to fetch leaderboard");
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
leaderboard.value = data.leaderboard;
|
|
||||||
|
|
||||||
// Find current user's rank
|
|
||||||
const userRank = leaderboard.value.find(
|
|
||||||
(entry: any) => entry.username === userInfo.value.username
|
|
||||||
);
|
|
||||||
|
|
||||||
if (userRank) {
|
|
||||||
userInfo.value.rank = userRank.rank;
|
|
||||||
userInfo.value.score = userRank.total_score;
|
|
||||||
userInfo.value.deathsCount = userRank.deaths_count;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching leaderboard:", error);
|
|
||||||
} finally {
|
|
||||||
isLoadingLeaderboard.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearCache = async () => {
|
const clearCache = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/cache/clear", {
|
await noitaStore.clearCache();
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
alert("Cache cleared successfully!");
|
alert("Cache cleared successfully!");
|
||||||
// Refresh data after clearing cache
|
|
||||||
await Promise.all([
|
|
||||||
fetchUserResults(),
|
|
||||||
fetchLeaderboard(),
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
const error = await response.json();
|
|
||||||
alert(`Error clearing cache: ${error.detail || "Unknown error"}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error clearing cache:", error);
|
console.error("Error clearing cache:", error);
|
||||||
alert("Error clearing cache. Please try again.");
|
alert("Error clearing cache. Please try again.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadUserData = async () => {
|
|
||||||
// Get user info first
|
|
||||||
try {
|
|
||||||
const response = await fetch("/api/user");
|
|
||||||
if (response.ok) {
|
|
||||||
const user = await response.json();
|
|
||||||
if (user.is_authenticated) {
|
|
||||||
userInfo.value.username = user.username;
|
|
||||||
userInfo.value.isStaff = user.is_staff || false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching user info:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch results and leaderboard
|
|
||||||
await Promise.all([
|
|
||||||
fetchUserResults(),
|
|
||||||
fetchLeaderboard(),
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadUserData();
|
noitaStore.loadUserData();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,8 @@ import Results from "@/components/Results.vue";
|
|||||||
import Winners from "@/components/Winners.vue";
|
import Winners from "@/components/Winners.vue";
|
||||||
import PuzzleResults from "@/components/PuzzleResults.vue";
|
import PuzzleResults from "@/components/PuzzleResults.vue";
|
||||||
import TopUsersLeaderboard from "@/components/TopUsersLeaderboard.vue";
|
import TopUsersLeaderboard from "@/components/TopUsersLeaderboard.vue";
|
||||||
import { apiService, errorHelpers } from "@/services/apiService";
|
import { polylanSubmitterApiGetUserInfo, opusMagnumApiGetCollection } from "@/api";
|
||||||
|
import { errorHelpers } from "@/services/apiService";
|
||||||
import { usePuzzlesStore } from "@/stores/puzzles";
|
import { usePuzzlesStore } from "@/stores/puzzles";
|
||||||
import { useSubmissionsStore } from "@/stores/submissions";
|
import { useSubmissionsStore } from "@/stores/submissions";
|
||||||
import type { PuzzleResponse, UserInfo, SteamCollection } from "@/types";
|
import type { PuzzleResponse, UserInfo, SteamCollection } from "@/types";
|
||||||
@ -66,9 +67,9 @@ async function initialize() {
|
|||||||
|
|
||||||
// Load user info
|
// Load user info
|
||||||
console.log("Loading user info...");
|
console.log("Loading user info...");
|
||||||
const userResponse = await apiService.getUserInfo();
|
const userResponse = await polylanSubmitterApiGetUserInfo();
|
||||||
if (userResponse.data) {
|
if (userResponse.data) {
|
||||||
userInfo.value = userResponse.data;
|
userInfo.value = userResponse.data as UserInfo;
|
||||||
console.log("User info loaded:", userResponse.data);
|
console.log("User info loaded:", userResponse.data);
|
||||||
} else if (userResponse.error) {
|
} else if (userResponse.error) {
|
||||||
console.warn("User info error:", userResponse.error);
|
console.warn("User info error:", userResponse.error);
|
||||||
@ -76,9 +77,9 @@ async function initialize() {
|
|||||||
|
|
||||||
// Load collection data
|
// Load collection data
|
||||||
console.log("Loading collection...");
|
console.log("Loading collection...");
|
||||||
const collectionResponse = await apiService.getCollection();
|
const collectionResponse = await opusMagnumApiGetCollection();
|
||||||
if (collectionResponse.data) {
|
if (collectionResponse.data) {
|
||||||
collection.value = collectionResponse.data;
|
collection.value = collectionResponse.data as SteamCollection;
|
||||||
console.log("Collection loaded:", collectionResponse.data);
|
console.log("Collection loaded:", collectionResponse.data);
|
||||||
} else if (collectionResponse.error) {
|
} else if (collectionResponse.error) {
|
||||||
console.warn("Collection error:", collectionResponse.error);
|
console.warn("Collection error:", collectionResponse.error);
|
||||||
|
|||||||
16
polylan_submitter/src/api/client.gen.ts
Normal file
16
polylan_submitter/src/api/client.gen.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
|
import { type ClientOptions, type Config, createClient, createConfig } from './client';
|
||||||
|
import type { ClientOptions as ClientOptions2 } from './types.gen';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `createClientConfig()` function will be called on client initialization
|
||||||
|
* and the returned object will become the client's initial configuration.
|
||||||
|
*
|
||||||
|
* You may want to initialize your client this way instead of calling
|
||||||
|
* `setConfig()`. This is useful for example if you're using Next.js
|
||||||
|
* to ensure your client always has the correct values.
|
||||||
|
*/
|
||||||
|
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (override?: Config<ClientOptions & T>) => Config<Required<ClientOptions> & T>;
|
||||||
|
|
||||||
|
export const client = createClient(createConfig<ClientOptions2>());
|
||||||
277
polylan_submitter/src/api/client/client.gen.ts
Normal file
277
polylan_submitter/src/api/client/client.gen.ts
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
|
import { createSseClient } from '../core/serverSentEvents.gen';
|
||||||
|
import type { HttpMethod } from '../core/types.gen';
|
||||||
|
import { getValidRequestBody } from '../core/utils.gen';
|
||||||
|
import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen';
|
||||||
|
import {
|
||||||
|
buildUrl,
|
||||||
|
createConfig,
|
||||||
|
createInterceptors,
|
||||||
|
getParseAs,
|
||||||
|
mergeConfigs,
|
||||||
|
mergeHeaders,
|
||||||
|
setAuthParams,
|
||||||
|
} from './utils.gen';
|
||||||
|
|
||||||
|
type ReqInit = Omit<RequestInit, 'body' | 'headers'> & {
|
||||||
|
body?: any;
|
||||||
|
headers: ReturnType<typeof mergeHeaders>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createClient = (config: Config = {}): Client => {
|
||||||
|
let _config = mergeConfigs(createConfig(), config);
|
||||||
|
|
||||||
|
const getConfig = (): Config => ({ ..._config });
|
||||||
|
|
||||||
|
const setConfig = (config: Config): Config => {
|
||||||
|
_config = mergeConfigs(_config, config);
|
||||||
|
return getConfig();
|
||||||
|
};
|
||||||
|
|
||||||
|
const interceptors = createInterceptors<Request, Response, unknown, ResolvedRequestOptions>();
|
||||||
|
|
||||||
|
const beforeRequest = async <
|
||||||
|
TData = unknown,
|
||||||
|
TResponseStyle extends 'data' | 'fields' = 'fields',
|
||||||
|
ThrowOnError extends boolean = boolean,
|
||||||
|
Url extends string = string,
|
||||||
|
>(
|
||||||
|
options: RequestOptions<TData, TResponseStyle, ThrowOnError, Url>,
|
||||||
|
) => {
|
||||||
|
const opts = {
|
||||||
|
..._config,
|
||||||
|
...options,
|
||||||
|
fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
|
||||||
|
headers: mergeHeaders(_config.headers, options.headers),
|
||||||
|
serializedBody: undefined as string | undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (opts.security) {
|
||||||
|
await setAuthParams(opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.requestValidator) {
|
||||||
|
await opts.requestValidator(opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.body !== undefined && opts.bodySerializer) {
|
||||||
|
opts.serializedBody = opts.bodySerializer(opts.body) as string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove Content-Type header if body is empty to avoid sending invalid requests
|
||||||
|
if (opts.body === undefined || opts.serializedBody === '') {
|
||||||
|
opts.headers.delete('Content-Type');
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedOpts = opts as typeof opts &
|
||||||
|
ResolvedRequestOptions<TResponseStyle, ThrowOnError, Url>;
|
||||||
|
const url = buildUrl(resolvedOpts);
|
||||||
|
|
||||||
|
return { opts: resolvedOpts, url };
|
||||||
|
};
|
||||||
|
|
||||||
|
const request: Client['request'] = async (options) => {
|
||||||
|
const throwOnError = options.throwOnError ?? _config.throwOnError;
|
||||||
|
const responseStyle = options.responseStyle ?? _config.responseStyle;
|
||||||
|
|
||||||
|
let request: Request | undefined;
|
||||||
|
let response: Response | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { opts, url } = await beforeRequest(options);
|
||||||
|
const requestInit: ReqInit = {
|
||||||
|
redirect: 'follow',
|
||||||
|
...opts,
|
||||||
|
body: getValidRequestBody(opts),
|
||||||
|
};
|
||||||
|
|
||||||
|
request = new Request(url, requestInit);
|
||||||
|
|
||||||
|
for (const fn of interceptors.request.fns) {
|
||||||
|
if (fn) {
|
||||||
|
request = await fn(request, opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch must be assigned here, otherwise it would throw the error:
|
||||||
|
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
|
||||||
|
const _fetch = opts.fetch!;
|
||||||
|
|
||||||
|
response = await _fetch(request);
|
||||||
|
|
||||||
|
for (const fn of interceptors.response.fns) {
|
||||||
|
if (fn) {
|
||||||
|
response = await fn(response, request, opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
request,
|
||||||
|
response,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const parseAs =
|
||||||
|
(opts.parseAs === 'auto'
|
||||||
|
? getParseAs(response.headers.get('Content-Type'))
|
||||||
|
: opts.parseAs) ?? 'json';
|
||||||
|
|
||||||
|
if (response.status === 204 || response.headers.get('Content-Length') === '0') {
|
||||||
|
let emptyData: any;
|
||||||
|
switch (parseAs) {
|
||||||
|
case 'arrayBuffer':
|
||||||
|
case 'blob':
|
||||||
|
case 'text':
|
||||||
|
emptyData = await response[parseAs]();
|
||||||
|
break;
|
||||||
|
case 'formData':
|
||||||
|
emptyData = new FormData();
|
||||||
|
break;
|
||||||
|
case 'stream':
|
||||||
|
emptyData = response.body;
|
||||||
|
break;
|
||||||
|
case 'json':
|
||||||
|
default:
|
||||||
|
emptyData = {};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return opts.responseStyle === 'data'
|
||||||
|
? emptyData
|
||||||
|
: {
|
||||||
|
data: emptyData,
|
||||||
|
...result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let data: any;
|
||||||
|
switch (parseAs) {
|
||||||
|
case 'arrayBuffer':
|
||||||
|
case 'blob':
|
||||||
|
case 'formData':
|
||||||
|
case 'text':
|
||||||
|
data = await response[parseAs]();
|
||||||
|
break;
|
||||||
|
case 'json': {
|
||||||
|
// Some servers return 200 with no Content-Length and empty body.
|
||||||
|
// response.json() would throw; read as text and parse if non-empty.
|
||||||
|
const text = await response.text();
|
||||||
|
data = text ? JSON.parse(text) : {};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'stream':
|
||||||
|
return opts.responseStyle === 'data'
|
||||||
|
? response.body
|
||||||
|
: {
|
||||||
|
data: response.body,
|
||||||
|
...result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parseAs === 'json') {
|
||||||
|
if (opts.responseValidator) {
|
||||||
|
await opts.responseValidator(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.responseTransformer) {
|
||||||
|
data = await opts.responseTransformer(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts.responseStyle === 'data'
|
||||||
|
? data
|
||||||
|
: {
|
||||||
|
data,
|
||||||
|
...result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const textError = await response.text();
|
||||||
|
let jsonError: unknown;
|
||||||
|
|
||||||
|
try {
|
||||||
|
jsonError = JSON.parse(textError);
|
||||||
|
} catch {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
|
||||||
|
throw jsonError ?? textError;
|
||||||
|
} catch (error) {
|
||||||
|
let finalError = error;
|
||||||
|
|
||||||
|
for (const fn of interceptors.error.fns) {
|
||||||
|
if (fn) {
|
||||||
|
finalError = await fn(finalError, response, request, options as ResolvedRequestOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finalError = finalError || {};
|
||||||
|
|
||||||
|
if (throwOnError) {
|
||||||
|
throw finalError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: we probably want to return error and improve types
|
||||||
|
return responseStyle === 'data'
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
error: finalError,
|
||||||
|
request,
|
||||||
|
response,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeMethodFn = (method: Uppercase<HttpMethod>) => (options: RequestOptions) =>
|
||||||
|
request({ ...options, method });
|
||||||
|
|
||||||
|
const makeSseFn = (method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
|
||||||
|
const { opts, url } = await beforeRequest(options);
|
||||||
|
return createSseClient({
|
||||||
|
...opts,
|
||||||
|
body: opts.body as BodyInit | null | undefined,
|
||||||
|
method,
|
||||||
|
onRequest: async (url, init) => {
|
||||||
|
let request = new Request(url, init);
|
||||||
|
for (const fn of interceptors.request.fns) {
|
||||||
|
if (fn) {
|
||||||
|
request = await fn(request, opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return request;
|
||||||
|
},
|
||||||
|
serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const _buildUrl: Client['buildUrl'] = (options) => buildUrl({ ..._config, ...options });
|
||||||
|
|
||||||
|
return {
|
||||||
|
buildUrl: _buildUrl,
|
||||||
|
connect: makeMethodFn('CONNECT'),
|
||||||
|
delete: makeMethodFn('DELETE'),
|
||||||
|
get: makeMethodFn('GET'),
|
||||||
|
getConfig,
|
||||||
|
head: makeMethodFn('HEAD'),
|
||||||
|
interceptors,
|
||||||
|
options: makeMethodFn('OPTIONS'),
|
||||||
|
patch: makeMethodFn('PATCH'),
|
||||||
|
post: makeMethodFn('POST'),
|
||||||
|
put: makeMethodFn('PUT'),
|
||||||
|
request,
|
||||||
|
setConfig,
|
||||||
|
sse: {
|
||||||
|
connect: makeSseFn('CONNECT'),
|
||||||
|
delete: makeSseFn('DELETE'),
|
||||||
|
get: makeSseFn('GET'),
|
||||||
|
head: makeSseFn('HEAD'),
|
||||||
|
options: makeSseFn('OPTIONS'),
|
||||||
|
patch: makeSseFn('PATCH'),
|
||||||
|
post: makeSseFn('POST'),
|
||||||
|
put: makeSseFn('PUT'),
|
||||||
|
trace: makeSseFn('TRACE'),
|
||||||
|
},
|
||||||
|
trace: makeMethodFn('TRACE'),
|
||||||
|
} as Client;
|
||||||
|
};
|
||||||
25
polylan_submitter/src/api/client/index.ts
Normal file
25
polylan_submitter/src/api/client/index.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
|
export type { Auth } from '../core/auth.gen';
|
||||||
|
export type { QuerySerializerOptions } from '../core/bodySerializer.gen';
|
||||||
|
export {
|
||||||
|
formDataBodySerializer,
|
||||||
|
jsonBodySerializer,
|
||||||
|
urlSearchParamsBodySerializer,
|
||||||
|
} from '../core/bodySerializer.gen';
|
||||||
|
export { buildClientParams } from '../core/params.gen';
|
||||||
|
export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
|
||||||
|
export { createClient } from './client.gen';
|
||||||
|
export type {
|
||||||
|
Client,
|
||||||
|
ClientOptions,
|
||||||
|
Config,
|
||||||
|
CreateClientConfig,
|
||||||
|
Options,
|
||||||
|
RequestOptions,
|
||||||
|
RequestResult,
|
||||||
|
ResolvedRequestOptions,
|
||||||
|
ResponseStyle,
|
||||||
|
TDataShape,
|
||||||
|
} from './types.gen';
|
||||||
|
export { createConfig, mergeHeaders } from './utils.gen';
|
||||||
217
polylan_submitter/src/api/client/types.gen.ts
Normal file
217
polylan_submitter/src/api/client/types.gen.ts
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
|
import type { Auth } from '../core/auth.gen';
|
||||||
|
import type {
|
||||||
|
ServerSentEventsOptions,
|
||||||
|
ServerSentEventsResult,
|
||||||
|
} from '../core/serverSentEvents.gen';
|
||||||
|
import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen';
|
||||||
|
import type { Middleware } from './utils.gen';
|
||||||
|
|
||||||
|
export type ResponseStyle = 'data' | 'fields';
|
||||||
|
|
||||||
|
export interface Config<T extends ClientOptions = ClientOptions>
|
||||||
|
extends Omit<RequestInit, 'body' | 'headers' | 'method'>, CoreConfig {
|
||||||
|
/**
|
||||||
|
* Base URL for all requests made by this client.
|
||||||
|
*/
|
||||||
|
baseUrl?: T['baseUrl'];
|
||||||
|
/**
|
||||||
|
* Fetch API implementation. You can use this option to provide a custom
|
||||||
|
* fetch instance.
|
||||||
|
*
|
||||||
|
* @default globalThis.fetch
|
||||||
|
*/
|
||||||
|
fetch?: typeof fetch;
|
||||||
|
/**
|
||||||
|
* Please don't use the Fetch client for Next.js applications. The `next`
|
||||||
|
* options won't have any effect.
|
||||||
|
*
|
||||||
|
* Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead.
|
||||||
|
*/
|
||||||
|
next?: never;
|
||||||
|
/**
|
||||||
|
* Return the response data parsed in a specified format. By default, `auto`
|
||||||
|
* will infer the appropriate method from the `Content-Type` response header.
|
||||||
|
* You can override this behavior with any of the {@link Body} methods.
|
||||||
|
* Select `stream` if you don't want to parse response data at all.
|
||||||
|
*
|
||||||
|
* @default 'auto'
|
||||||
|
*/
|
||||||
|
parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text';
|
||||||
|
/**
|
||||||
|
* Should we return only data or multiple fields (data, error, response, etc.)?
|
||||||
|
*
|
||||||
|
* @default 'fields'
|
||||||
|
*/
|
||||||
|
responseStyle?: ResponseStyle;
|
||||||
|
/**
|
||||||
|
* Throw an error instead of returning it in the response?
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
throwOnError?: T['throwOnError'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RequestOptions<
|
||||||
|
TData = unknown,
|
||||||
|
TResponseStyle extends ResponseStyle = 'fields',
|
||||||
|
ThrowOnError extends boolean = boolean,
|
||||||
|
Url extends string = string,
|
||||||
|
>
|
||||||
|
extends
|
||||||
|
Config<{
|
||||||
|
responseStyle: TResponseStyle;
|
||||||
|
throwOnError: ThrowOnError;
|
||||||
|
}>,
|
||||||
|
Pick<
|
||||||
|
ServerSentEventsOptions<TData>,
|
||||||
|
| 'onRequest'
|
||||||
|
| 'onSseError'
|
||||||
|
| 'onSseEvent'
|
||||||
|
| 'sseDefaultRetryDelay'
|
||||||
|
| 'sseMaxRetryAttempts'
|
||||||
|
| 'sseMaxRetryDelay'
|
||||||
|
> {
|
||||||
|
/**
|
||||||
|
* Any body that you want to add to your request.
|
||||||
|
*
|
||||||
|
* {@link https://developer.mozilla.org/docs/Web/API/fetch#body}
|
||||||
|
*/
|
||||||
|
body?: unknown;
|
||||||
|
path?: Record<string, unknown>;
|
||||||
|
query?: Record<string, unknown>;
|
||||||
|
/**
|
||||||
|
* Security mechanism(s) to use for the request.
|
||||||
|
*/
|
||||||
|
security?: ReadonlyArray<Auth>;
|
||||||
|
url: Url;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResolvedRequestOptions<
|
||||||
|
TResponseStyle extends ResponseStyle = 'fields',
|
||||||
|
ThrowOnError extends boolean = boolean,
|
||||||
|
Url extends string = string,
|
||||||
|
> extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> {
|
||||||
|
headers: Headers;
|
||||||
|
serializedBody?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RequestResult<
|
||||||
|
TData = unknown,
|
||||||
|
TError = unknown,
|
||||||
|
ThrowOnError extends boolean = boolean,
|
||||||
|
TResponseStyle extends ResponseStyle = 'fields',
|
||||||
|
> = ThrowOnError extends true
|
||||||
|
? Promise<
|
||||||
|
TResponseStyle extends 'data'
|
||||||
|
? TData extends Record<string, unknown>
|
||||||
|
? TData[keyof TData]
|
||||||
|
: TData
|
||||||
|
: {
|
||||||
|
data: TData extends Record<string, unknown> ? TData[keyof TData] : TData;
|
||||||
|
request: Request;
|
||||||
|
response: Response;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
: Promise<
|
||||||
|
TResponseStyle extends 'data'
|
||||||
|
? (TData extends Record<string, unknown> ? TData[keyof TData] : TData) | undefined
|
||||||
|
: (
|
||||||
|
| {
|
||||||
|
data: TData extends Record<string, unknown> ? TData[keyof TData] : TData;
|
||||||
|
error: undefined;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
data: undefined;
|
||||||
|
error: TError extends Record<string, unknown> ? TError[keyof TError] : TError;
|
||||||
|
}
|
||||||
|
) & {
|
||||||
|
/** request may be undefined, because error may be from building the request object itself */
|
||||||
|
request?: Request;
|
||||||
|
/** response may be undefined, because error may be from building the request object itself or from a network error */
|
||||||
|
response?: Response;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface ClientOptions {
|
||||||
|
baseUrl?: string;
|
||||||
|
responseStyle?: ResponseStyle;
|
||||||
|
throwOnError?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type MethodFn = <
|
||||||
|
TData = unknown,
|
||||||
|
TError = unknown,
|
||||||
|
ThrowOnError extends boolean = false,
|
||||||
|
TResponseStyle extends ResponseStyle = 'fields',
|
||||||
|
>(
|
||||||
|
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>,
|
||||||
|
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
|
||||||
|
|
||||||
|
type SseFn = <
|
||||||
|
TData = unknown,
|
||||||
|
TError = unknown,
|
||||||
|
ThrowOnError extends boolean = false,
|
||||||
|
TResponseStyle extends ResponseStyle = 'fields',
|
||||||
|
>(
|
||||||
|
options: Omit<RequestOptions<never, TResponseStyle, ThrowOnError>, 'method'>,
|
||||||
|
) => Promise<ServerSentEventsResult<TData, TError>>;
|
||||||
|
|
||||||
|
type RequestFn = <
|
||||||
|
TData = unknown,
|
||||||
|
TError = unknown,
|
||||||
|
ThrowOnError extends boolean = false,
|
||||||
|
TResponseStyle extends ResponseStyle = 'fields',
|
||||||
|
>(
|
||||||
|
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'> &
|
||||||
|
Pick<Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>, 'method'>,
|
||||||
|
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
|
||||||
|
|
||||||
|
type BuildUrlFn = <
|
||||||
|
TData extends {
|
||||||
|
body?: unknown;
|
||||||
|
path?: Record<string, unknown>;
|
||||||
|
query?: Record<string, unknown>;
|
||||||
|
url: string;
|
||||||
|
},
|
||||||
|
>(
|
||||||
|
options: TData & Options<TData>,
|
||||||
|
) => string;
|
||||||
|
|
||||||
|
export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn, SseFn> & {
|
||||||
|
interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `createClientConfig()` function will be called on client initialization
|
||||||
|
* and the returned object will become the client's initial configuration.
|
||||||
|
*
|
||||||
|
* You may want to initialize your client this way instead of calling
|
||||||
|
* `setConfig()`. This is useful for example if you're using Next.js
|
||||||
|
* to ensure your client always has the correct values.
|
||||||
|
*/
|
||||||
|
export type CreateClientConfig<T extends ClientOptions = ClientOptions> = (
|
||||||
|
override?: Config<ClientOptions & T>,
|
||||||
|
) => Config<Required<ClientOptions> & T>;
|
||||||
|
|
||||||
|
export interface TDataShape {
|
||||||
|
body?: unknown;
|
||||||
|
headers?: unknown;
|
||||||
|
path?: unknown;
|
||||||
|
query?: unknown;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>;
|
||||||
|
|
||||||
|
export type Options<
|
||||||
|
TData extends TDataShape = TDataShape,
|
||||||
|
ThrowOnError extends boolean = boolean,
|
||||||
|
TResponse = unknown,
|
||||||
|
TResponseStyle extends ResponseStyle = 'fields',
|
||||||
|
> = OmitKeys<
|
||||||
|
RequestOptions<TResponse, TResponseStyle, ThrowOnError>,
|
||||||
|
'body' | 'path' | 'query' | 'url'
|
||||||
|
> &
|
||||||
|
([TData] extends [never] ? unknown : Omit<TData, 'url'>);
|
||||||
316
polylan_submitter/src/api/client/utils.gen.ts
Normal file
316
polylan_submitter/src/api/client/utils.gen.ts
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
|
import { getAuthToken } from '../core/auth.gen';
|
||||||
|
import type { QuerySerializerOptions } from '../core/bodySerializer.gen';
|
||||||
|
import { jsonBodySerializer } from '../core/bodySerializer.gen';
|
||||||
|
import {
|
||||||
|
serializeArrayParam,
|
||||||
|
serializeObjectParam,
|
||||||
|
serializePrimitiveParam,
|
||||||
|
} from '../core/pathSerializer.gen';
|
||||||
|
import { getUrl } from '../core/utils.gen';
|
||||||
|
import type { Client, ClientOptions, Config, RequestOptions } from './types.gen';
|
||||||
|
|
||||||
|
export const createQuerySerializer = <T = unknown>({
|
||||||
|
parameters = {},
|
||||||
|
...args
|
||||||
|
}: QuerySerializerOptions = {}) => {
|
||||||
|
const querySerializer = (queryParams: T) => {
|
||||||
|
const search: string[] = [];
|
||||||
|
if (queryParams && typeof queryParams === 'object') {
|
||||||
|
for (const name in queryParams) {
|
||||||
|
const value = queryParams[name];
|
||||||
|
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = parameters[name] || args;
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
const serializedArray = serializeArrayParam({
|
||||||
|
allowReserved: options.allowReserved,
|
||||||
|
explode: true,
|
||||||
|
name,
|
||||||
|
style: 'form',
|
||||||
|
value,
|
||||||
|
...options.array,
|
||||||
|
});
|
||||||
|
if (serializedArray) search.push(serializedArray);
|
||||||
|
} else if (typeof value === 'object') {
|
||||||
|
const serializedObject = serializeObjectParam({
|
||||||
|
allowReserved: options.allowReserved,
|
||||||
|
explode: true,
|
||||||
|
name,
|
||||||
|
style: 'deepObject',
|
||||||
|
value: value as Record<string, unknown>,
|
||||||
|
...options.object,
|
||||||
|
});
|
||||||
|
if (serializedObject) search.push(serializedObject);
|
||||||
|
} else {
|
||||||
|
const serializedPrimitive = serializePrimitiveParam({
|
||||||
|
allowReserved: options.allowReserved,
|
||||||
|
name,
|
||||||
|
value: value as string,
|
||||||
|
});
|
||||||
|
if (serializedPrimitive) search.push(serializedPrimitive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return search.join('&');
|
||||||
|
};
|
||||||
|
return querySerializer;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Infers parseAs value from provided Content-Type header.
|
||||||
|
*/
|
||||||
|
export const getParseAs = (contentType: string | null): Exclude<Config['parseAs'], 'auto'> => {
|
||||||
|
if (!contentType) {
|
||||||
|
// If no Content-Type header is provided, the best we can do is return the raw response body,
|
||||||
|
// which is effectively the same as the 'stream' option.
|
||||||
|
return 'stream';
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanContent = contentType.split(';')[0]?.trim();
|
||||||
|
|
||||||
|
if (!cleanContent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleanContent.startsWith('application/json') || cleanContent.endsWith('+json')) {
|
||||||
|
return 'json';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleanContent === 'multipart/form-data') {
|
||||||
|
return 'formData';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
['application/', 'audio/', 'image/', 'video/'].some((type) => cleanContent.startsWith(type))
|
||||||
|
) {
|
||||||
|
return 'blob';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleanContent.startsWith('text/')) {
|
||||||
|
return 'text';
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkForExistence = (
|
||||||
|
options: Pick<RequestOptions, 'auth' | 'query'> & {
|
||||||
|
headers: Headers;
|
||||||
|
},
|
||||||
|
name?: string,
|
||||||
|
): boolean => {
|
||||||
|
if (!name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
options.headers.has(name) ||
|
||||||
|
options.query?.[name] ||
|
||||||
|
options.headers.get('Cookie')?.includes(`${name}=`)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function setAuthParams(
|
||||||
|
options: Pick<RequestOptions, 'auth' | 'query' | 'security'> & {
|
||||||
|
headers: Headers;
|
||||||
|
},
|
||||||
|
): Promise<void> {
|
||||||
|
for (const auth of options.security ?? []) {
|
||||||
|
if (checkForExistence(options, auth.name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await getAuthToken(auth, options.auth);
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = auth.name ?? 'Authorization';
|
||||||
|
|
||||||
|
switch (auth.in) {
|
||||||
|
case 'query':
|
||||||
|
if (!options.query) {
|
||||||
|
options.query = {};
|
||||||
|
}
|
||||||
|
options.query[name] = token;
|
||||||
|
break;
|
||||||
|
case 'cookie':
|
||||||
|
options.headers.append('Cookie', `${name}=${token}`);
|
||||||
|
break;
|
||||||
|
case 'header':
|
||||||
|
default:
|
||||||
|
options.headers.set(name, token);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const buildUrl: Client['buildUrl'] = (options) =>
|
||||||
|
getUrl({
|
||||||
|
baseUrl: options.baseUrl as string,
|
||||||
|
path: options.path,
|
||||||
|
query: options.query,
|
||||||
|
querySerializer:
|
||||||
|
typeof options.querySerializer === 'function'
|
||||||
|
? options.querySerializer
|
||||||
|
: createQuerySerializer(options.querySerializer),
|
||||||
|
url: options.url,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mergeConfigs = (a: Config, b: Config): Config => {
|
||||||
|
const config = { ...a, ...b };
|
||||||
|
if (config.baseUrl?.endsWith('/')) {
|
||||||
|
config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
|
||||||
|
}
|
||||||
|
config.headers = mergeHeaders(a.headers, b.headers);
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|
||||||
|
const headersEntries = (headers: Headers): Array<[string, string]> => {
|
||||||
|
const entries: Array<[string, string]> = [];
|
||||||
|
headers.forEach((value, key) => {
|
||||||
|
entries.push([key, value]);
|
||||||
|
});
|
||||||
|
return entries;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mergeHeaders = (
|
||||||
|
...headers: Array<Required<Config>['headers'] | undefined>
|
||||||
|
): Headers => {
|
||||||
|
const mergedHeaders = new Headers();
|
||||||
|
for (const header of headers) {
|
||||||
|
if (!header) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header);
|
||||||
|
|
||||||
|
for (const [key, value] of iterator) {
|
||||||
|
if (value === null) {
|
||||||
|
mergedHeaders.delete(key);
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
for (const v of value) {
|
||||||
|
mergedHeaders.append(key, v as string);
|
||||||
|
}
|
||||||
|
} else if (value !== undefined) {
|
||||||
|
// assume object headers are meant to be JSON stringified, i.e., their
|
||||||
|
// content value in OpenAPI specification is 'application/json'
|
||||||
|
mergedHeaders.set(
|
||||||
|
key,
|
||||||
|
typeof value === 'object' ? JSON.stringify(value) : (value as string),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mergedHeaders;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ErrInterceptor<Err, Res, Req, Options> = (
|
||||||
|
error: Err,
|
||||||
|
/** response may be undefined due to a network error where no response object is produced */
|
||||||
|
response: Res | undefined,
|
||||||
|
/** request may be undefined, because error may be from building the request object itself */
|
||||||
|
request: Req | undefined,
|
||||||
|
options: Options,
|
||||||
|
) => Err | Promise<Err>;
|
||||||
|
|
||||||
|
type ReqInterceptor<Req, Options> = (request: Req, options: Options) => Req | Promise<Req>;
|
||||||
|
|
||||||
|
type ResInterceptor<Res, Req, Options> = (
|
||||||
|
response: Res,
|
||||||
|
request: Req,
|
||||||
|
options: Options,
|
||||||
|
) => Res | Promise<Res>;
|
||||||
|
|
||||||
|
class Interceptors<Interceptor> {
|
||||||
|
fns: Array<Interceptor | null> = [];
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.fns = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
eject(id: number | Interceptor): void {
|
||||||
|
const index = this.getInterceptorIndex(id);
|
||||||
|
if (this.fns[index]) {
|
||||||
|
this.fns[index] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exists(id: number | Interceptor): boolean {
|
||||||
|
const index = this.getInterceptorIndex(id);
|
||||||
|
return Boolean(this.fns[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
getInterceptorIndex(id: number | Interceptor): number {
|
||||||
|
if (typeof id === 'number') {
|
||||||
|
return this.fns[id] ? id : -1;
|
||||||
|
}
|
||||||
|
return this.fns.indexOf(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false {
|
||||||
|
const index = this.getInterceptorIndex(id);
|
||||||
|
if (this.fns[index]) {
|
||||||
|
this.fns[index] = fn;
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
use(fn: Interceptor): number {
|
||||||
|
this.fns.push(fn);
|
||||||
|
return this.fns.length - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Middleware<Req, Res, Err, Options> {
|
||||||
|
error: Interceptors<ErrInterceptor<Err, Res, Req, Options>>;
|
||||||
|
request: Interceptors<ReqInterceptor<Req, Options>>;
|
||||||
|
response: Interceptors<ResInterceptor<Res, Req, Options>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createInterceptors = <Req, Res, Err, Options>(): Middleware<
|
||||||
|
Req,
|
||||||
|
Res,
|
||||||
|
Err,
|
||||||
|
Options
|
||||||
|
> => ({
|
||||||
|
error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(),
|
||||||
|
request: new Interceptors<ReqInterceptor<Req, Options>>(),
|
||||||
|
response: new Interceptors<ResInterceptor<Res, Req, Options>>(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultQuerySerializer = createQuerySerializer({
|
||||||
|
allowReserved: false,
|
||||||
|
array: {
|
||||||
|
explode: true,
|
||||||
|
style: 'form',
|
||||||
|
},
|
||||||
|
object: {
|
||||||
|
explode: true,
|
||||||
|
style: 'deepObject',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultHeaders = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createConfig = <T extends ClientOptions = ClientOptions>(
|
||||||
|
override: Config<Omit<ClientOptions, keyof T> & T> = {},
|
||||||
|
): Config<Omit<ClientOptions, keyof T> & T> => ({
|
||||||
|
...jsonBodySerializer,
|
||||||
|
headers: defaultHeaders,
|
||||||
|
parseAs: 'auto',
|
||||||
|
querySerializer: defaultQuerySerializer,
|
||||||
|
...override,
|
||||||
|
});
|
||||||
41
polylan_submitter/src/api/core/auth.gen.ts
Normal file
41
polylan_submitter/src/api/core/auth.gen.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
|
export type AuthToken = string | undefined;
|
||||||
|
|
||||||
|
export interface Auth {
|
||||||
|
/**
|
||||||
|
* Which part of the request do we use to send the auth?
|
||||||
|
*
|
||||||
|
* @default 'header'
|
||||||
|
*/
|
||||||
|
in?: 'header' | 'query' | 'cookie';
|
||||||
|
/**
|
||||||
|
* Header or query parameter name.
|
||||||
|
*
|
||||||
|
* @default 'Authorization'
|
||||||
|
*/
|
||||||
|
name?: string;
|
||||||
|
scheme?: 'basic' | 'bearer';
|
||||||
|
type: 'apiKey' | 'http';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAuthToken = async (
|
||||||
|
auth: Auth,
|
||||||
|
callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken,
|
||||||
|
): Promise<string | undefined> => {
|
||||||
|
const token = typeof callback === 'function' ? await callback(auth) : callback;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth.scheme === 'bearer') {
|
||||||
|
return `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth.scheme === 'basic') {
|
||||||
|
return `Basic ${btoa(token)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return token;
|
||||||
|
};
|
||||||
82
polylan_submitter/src/api/core/bodySerializer.gen.ts
Normal file
82
polylan_submitter/src/api/core/bodySerializer.gen.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
|
import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen';
|
||||||
|
|
||||||
|
export type QuerySerializer = (query: Record<string, unknown>) => string;
|
||||||
|
|
||||||
|
export type BodySerializer = (body: unknown) => unknown;
|
||||||
|
|
||||||
|
type QuerySerializerOptionsObject = {
|
||||||
|
allowReserved?: boolean;
|
||||||
|
array?: Partial<SerializerOptions<ArrayStyle>>;
|
||||||
|
object?: Partial<SerializerOptions<ObjectStyle>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QuerySerializerOptions = QuerySerializerOptionsObject & {
|
||||||
|
/**
|
||||||
|
* Per-parameter serialization overrides. When provided, these settings
|
||||||
|
* override the global array/object settings for specific parameter names.
|
||||||
|
*/
|
||||||
|
parameters?: Record<string, QuerySerializerOptionsObject>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => {
|
||||||
|
if (typeof value === 'string' || value instanceof Blob) {
|
||||||
|
data.append(key, value);
|
||||||
|
} else if (value instanceof Date) {
|
||||||
|
data.append(key, value.toISOString());
|
||||||
|
} else {
|
||||||
|
data.append(key, JSON.stringify(value));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
data.append(key, value);
|
||||||
|
} else {
|
||||||
|
data.append(key, JSON.stringify(value));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formDataBodySerializer = {
|
||||||
|
bodySerializer: (body: unknown): FormData => {
|
||||||
|
const data = new FormData();
|
||||||
|
|
||||||
|
Object.entries(body as Record<string, unknown>).forEach(([key, value]) => {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach((v) => serializeFormDataPair(data, key, v));
|
||||||
|
} else {
|
||||||
|
serializeFormDataPair(data, key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const jsonBodySerializer = {
|
||||||
|
bodySerializer: (body: unknown): string =>
|
||||||
|
JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const urlSearchParamsBodySerializer = {
|
||||||
|
bodySerializer: (body: unknown): string => {
|
||||||
|
const data = new URLSearchParams();
|
||||||
|
|
||||||
|
Object.entries(body as Record<string, unknown>).forEach(([key, value]) => {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach((v) => serializeUrlSearchParamsPair(data, key, v));
|
||||||
|
} else {
|
||||||
|
serializeUrlSearchParamsPair(data, key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return data.toString();
|
||||||
|
},
|
||||||
|
};
|
||||||
169
polylan_submitter/src/api/core/params.gen.ts
Normal file
169
polylan_submitter/src/api/core/params.gen.ts
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
|
type Slot = 'body' | 'headers' | 'path' | 'query';
|
||||||
|
|
||||||
|
export type Field =
|
||||||
|
| {
|
||||||
|
in: Exclude<Slot, 'body'>;
|
||||||
|
/**
|
||||||
|
* Field name. This is the name we want the user to see and use.
|
||||||
|
*/
|
||||||
|
key: string;
|
||||||
|
/**
|
||||||
|
* Field mapped name. This is the name we want to use in the request.
|
||||||
|
* If omitted, we use the same value as `key`.
|
||||||
|
*/
|
||||||
|
map?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
in: Extract<Slot, 'body'>;
|
||||||
|
/**
|
||||||
|
* Key isn't required for bodies.
|
||||||
|
*/
|
||||||
|
key?: string;
|
||||||
|
map?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
/**
|
||||||
|
* Field name. This is the name we want the user to see and use.
|
||||||
|
*/
|
||||||
|
key: string;
|
||||||
|
/**
|
||||||
|
* Field mapped name. This is the name we want to use in the request.
|
||||||
|
* If `in` is omitted, `map` aliases `key` to the transport layer.
|
||||||
|
*/
|
||||||
|
map: Slot;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Fields {
|
||||||
|
allowExtra?: Partial<Record<Slot, boolean>>;
|
||||||
|
args?: ReadonlyArray<Field>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FieldsConfig = ReadonlyArray<Field | Fields>;
|
||||||
|
|
||||||
|
const extraPrefixesMap: Record<string, Slot> = {
|
||||||
|
$body_: 'body',
|
||||||
|
$headers_: 'headers',
|
||||||
|
$path_: 'path',
|
||||||
|
$query_: 'query',
|
||||||
|
};
|
||||||
|
const extraPrefixes = Object.entries(extraPrefixesMap);
|
||||||
|
|
||||||
|
type KeyMap = Map<
|
||||||
|
string,
|
||||||
|
| {
|
||||||
|
in: Slot;
|
||||||
|
map?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
in?: never;
|
||||||
|
map: Slot;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
|
||||||
|
if (!map) {
|
||||||
|
map = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const config of fields) {
|
||||||
|
if ('in' in config) {
|
||||||
|
if (config.key) {
|
||||||
|
map.set(config.key, {
|
||||||
|
in: config.in,
|
||||||
|
map: config.map,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if ('key' in config) {
|
||||||
|
map.set(config.key, {
|
||||||
|
map: config.map,
|
||||||
|
});
|
||||||
|
} else if (config.args) {
|
||||||
|
buildKeyMap(config.args, map);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Params {
|
||||||
|
body: unknown;
|
||||||
|
headers: Record<string, unknown>;
|
||||||
|
path: Record<string, unknown>;
|
||||||
|
query: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stripEmptySlots = (params: Params) => {
|
||||||
|
for (const [slot, value] of Object.entries(params)) {
|
||||||
|
if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) {
|
||||||
|
delete params[slot as Slot];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildClientParams = (args: ReadonlyArray<unknown>, fields: FieldsConfig) => {
|
||||||
|
const params: Params = {
|
||||||
|
body: {},
|
||||||
|
headers: {},
|
||||||
|
path: {},
|
||||||
|
query: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const map = buildKeyMap(fields);
|
||||||
|
|
||||||
|
let config: FieldsConfig[number] | undefined;
|
||||||
|
|
||||||
|
for (const [index, arg] of args.entries()) {
|
||||||
|
if (fields[index]) {
|
||||||
|
config = fields[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('in' in config) {
|
||||||
|
if (config.key) {
|
||||||
|
const field = map.get(config.key)!;
|
||||||
|
const name = field.map || config.key;
|
||||||
|
if (field.in) {
|
||||||
|
(params[field.in] as Record<string, unknown>)[name] = arg;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
params.body = arg;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const [key, value] of Object.entries(arg ?? {})) {
|
||||||
|
const field = map.get(key);
|
||||||
|
|
||||||
|
if (field) {
|
||||||
|
if (field.in) {
|
||||||
|
const name = field.map || key;
|
||||||
|
(params[field.in] as Record<string, unknown>)[name] = value;
|
||||||
|
} else {
|
||||||
|
params[field.map] = value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix));
|
||||||
|
|
||||||
|
if (extra) {
|
||||||
|
const [prefix, slot] = extra;
|
||||||
|
(params[slot] as Record<string, unknown>)[key.slice(prefix.length)] = value;
|
||||||
|
} else if ('allowExtra' in config && config.allowExtra) {
|
||||||
|
for (const [slot, allowed] of Object.entries(config.allowExtra)) {
|
||||||
|
if (allowed) {
|
||||||
|
(params[slot as Slot] as Record<string, unknown>)[key] = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stripEmptySlots(params);
|
||||||
|
|
||||||
|
return params;
|
||||||
|
};
|
||||||
171
polylan_submitter/src/api/core/pathSerializer.gen.ts
Normal file
171
polylan_submitter/src/api/core/pathSerializer.gen.ts
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
|
interface SerializeOptions<T> extends SerializePrimitiveOptions, SerializerOptions<T> {}
|
||||||
|
|
||||||
|
interface SerializePrimitiveOptions {
|
||||||
|
allowReserved?: boolean;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SerializerOptions<T> {
|
||||||
|
/**
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
explode: boolean;
|
||||||
|
style: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited';
|
||||||
|
export type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
|
||||||
|
type MatrixStyle = 'label' | 'matrix' | 'simple';
|
||||||
|
export type ObjectStyle = 'form' | 'deepObject';
|
||||||
|
type ObjectSeparatorStyle = ObjectStyle | MatrixStyle;
|
||||||
|
|
||||||
|
interface SerializePrimitiveParam extends SerializePrimitiveOptions {
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const separatorArrayExplode = (style: ArraySeparatorStyle) => {
|
||||||
|
switch (style) {
|
||||||
|
case 'label':
|
||||||
|
return '.';
|
||||||
|
case 'matrix':
|
||||||
|
return ';';
|
||||||
|
case 'simple':
|
||||||
|
return ',';
|
||||||
|
default:
|
||||||
|
return '&';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => {
|
||||||
|
switch (style) {
|
||||||
|
case 'form':
|
||||||
|
return ',';
|
||||||
|
case 'pipeDelimited':
|
||||||
|
return '|';
|
||||||
|
case 'spaceDelimited':
|
||||||
|
return '%20';
|
||||||
|
default:
|
||||||
|
return ',';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const separatorObjectExplode = (style: ObjectSeparatorStyle) => {
|
||||||
|
switch (style) {
|
||||||
|
case 'label':
|
||||||
|
return '.';
|
||||||
|
case 'matrix':
|
||||||
|
return ';';
|
||||||
|
case 'simple':
|
||||||
|
return ',';
|
||||||
|
default:
|
||||||
|
return '&';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const serializeArrayParam = ({
|
||||||
|
allowReserved,
|
||||||
|
explode,
|
||||||
|
name,
|
||||||
|
style,
|
||||||
|
value,
|
||||||
|
}: SerializeOptions<ArraySeparatorStyle> & {
|
||||||
|
value: unknown[];
|
||||||
|
}) => {
|
||||||
|
if (!explode) {
|
||||||
|
const joinedValues = (
|
||||||
|
allowReserved ? value : value.map((v) => encodeURIComponent(v as string))
|
||||||
|
).join(separatorArrayNoExplode(style));
|
||||||
|
switch (style) {
|
||||||
|
case 'label':
|
||||||
|
return `.${joinedValues}`;
|
||||||
|
case 'matrix':
|
||||||
|
return `;${name}=${joinedValues}`;
|
||||||
|
case 'simple':
|
||||||
|
return joinedValues;
|
||||||
|
default:
|
||||||
|
return `${name}=${joinedValues}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const separator = separatorArrayExplode(style);
|
||||||
|
const joinedValues = value
|
||||||
|
.map((v) => {
|
||||||
|
if (style === 'label' || style === 'simple') {
|
||||||
|
return allowReserved ? v : encodeURIComponent(v as string);
|
||||||
|
}
|
||||||
|
|
||||||
|
return serializePrimitiveParam({
|
||||||
|
allowReserved,
|
||||||
|
name,
|
||||||
|
value: v as string,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.join(separator);
|
||||||
|
return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const serializePrimitiveParam = ({
|
||||||
|
allowReserved,
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
}: SerializePrimitiveParam) => {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
throw new Error(
|
||||||
|
'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const serializeObjectParam = ({
|
||||||
|
allowReserved,
|
||||||
|
explode,
|
||||||
|
name,
|
||||||
|
style,
|
||||||
|
value,
|
||||||
|
valueOnly,
|
||||||
|
}: SerializeOptions<ObjectSeparatorStyle> & {
|
||||||
|
value: Record<string, unknown> | Date;
|
||||||
|
valueOnly?: boolean;
|
||||||
|
}) => {
|
||||||
|
if (value instanceof Date) {
|
||||||
|
return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (style !== 'deepObject' && !explode) {
|
||||||
|
let values: string[] = [];
|
||||||
|
Object.entries(value).forEach(([key, v]) => {
|
||||||
|
values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)];
|
||||||
|
});
|
||||||
|
const joinedValues = values.join(',');
|
||||||
|
switch (style) {
|
||||||
|
case 'form':
|
||||||
|
return `${name}=${joinedValues}`;
|
||||||
|
case 'label':
|
||||||
|
return `.${joinedValues}`;
|
||||||
|
case 'matrix':
|
||||||
|
return `;${name}=${joinedValues}`;
|
||||||
|
default:
|
||||||
|
return joinedValues;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const separator = separatorObjectExplode(style);
|
||||||
|
const joinedValues = Object.entries(value)
|
||||||
|
.map(([key, v]) =>
|
||||||
|
serializePrimitiveParam({
|
||||||
|
allowReserved,
|
||||||
|
name: style === 'deepObject' ? `${name}[${key}]` : key,
|
||||||
|
value: v as string,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.join(separator);
|
||||||
|
return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues;
|
||||||
|
};
|
||||||
117
polylan_submitter/src/api/core/queryKeySerializer.gen.ts
Normal file
117
polylan_submitter/src/api/core/queryKeySerializer.gen.ts
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON-friendly union that mirrors what Pinia Colada can hash.
|
||||||
|
*/
|
||||||
|
export type JsonValue =
|
||||||
|
| null
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| JsonValue[]
|
||||||
|
| { [key: string]: JsonValue };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
|
||||||
|
*/
|
||||||
|
export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
|
||||||
|
if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (typeof value === 'bigint') {
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
if (value instanceof Date) {
|
||||||
|
return value.toISOString();
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely stringifies a value and parses it back into a JsonValue.
|
||||||
|
*/
|
||||||
|
export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
|
||||||
|
try {
|
||||||
|
const json = JSON.stringify(input, queryKeyJsonReplacer);
|
||||||
|
if (json === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return JSON.parse(json) as JsonValue;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects plain objects (including objects with a null prototype).
|
||||||
|
*/
|
||||||
|
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
|
||||||
|
if (value === null || typeof value !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const prototype = Object.getPrototypeOf(value as object);
|
||||||
|
return prototype === Object.prototype || prototype === null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns URLSearchParams into a sorted JSON object for deterministic keys.
|
||||||
|
*/
|
||||||
|
const serializeSearchParams = (params: URLSearchParams): JsonValue => {
|
||||||
|
const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b));
|
||||||
|
const result: Record<string, JsonValue> = {};
|
||||||
|
|
||||||
|
for (const [key, value] of entries) {
|
||||||
|
const existing = result[key];
|
||||||
|
if (existing === undefined) {
|
||||||
|
result[key] = value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(existing)) {
|
||||||
|
(existing as string[]).push(value);
|
||||||
|
} else {
|
||||||
|
result[key] = [existing, value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes any accepted value into a JSON-friendly shape for query keys.
|
||||||
|
*/
|
||||||
|
export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => {
|
||||||
|
if (value === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'bigint') {
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value instanceof Date) {
|
||||||
|
return value.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return stringifyToJsonValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) {
|
||||||
|
return serializeSearchParams(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlainObject(value)) {
|
||||||
|
return stringifyToJsonValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
242
polylan_submitter/src/api/core/serverSentEvents.gen.ts
Normal file
242
polylan_submitter/src/api/core/serverSentEvents.gen.ts
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
|
import type { Config } from './types.gen';
|
||||||
|
|
||||||
|
export type ServerSentEventsOptions<TData = unknown> = Omit<RequestInit, 'method'> &
|
||||||
|
Pick<Config, 'method' | 'responseTransformer' | 'responseValidator'> & {
|
||||||
|
/**
|
||||||
|
* Fetch API implementation. You can use this option to provide a custom
|
||||||
|
* fetch instance.
|
||||||
|
*
|
||||||
|
* @default globalThis.fetch
|
||||||
|
*/
|
||||||
|
fetch?: typeof fetch;
|
||||||
|
/**
|
||||||
|
* Implementing clients can call request interceptors inside this hook.
|
||||||
|
*/
|
||||||
|
onRequest?: (url: string, init: RequestInit) => Promise<Request>;
|
||||||
|
/**
|
||||||
|
* Callback invoked when a network or parsing error occurs during streaming.
|
||||||
|
*
|
||||||
|
* This option applies only if the endpoint returns a stream of events.
|
||||||
|
*
|
||||||
|
* @param error The error that occurred.
|
||||||
|
*/
|
||||||
|
onSseError?: (error: unknown) => void;
|
||||||
|
/**
|
||||||
|
* Callback invoked when an event is streamed from the server.
|
||||||
|
*
|
||||||
|
* This option applies only if the endpoint returns a stream of events.
|
||||||
|
*
|
||||||
|
* @param event Event streamed from the server.
|
||||||
|
* @returns Nothing (void).
|
||||||
|
*/
|
||||||
|
onSseEvent?: (event: StreamEvent<TData>) => void;
|
||||||
|
serializedBody?: RequestInit['body'];
|
||||||
|
/**
|
||||||
|
* Default retry delay in milliseconds.
|
||||||
|
*
|
||||||
|
* This option applies only if the endpoint returns a stream of events.
|
||||||
|
*
|
||||||
|
* @default 3000
|
||||||
|
*/
|
||||||
|
sseDefaultRetryDelay?: number;
|
||||||
|
/**
|
||||||
|
* Maximum number of retry attempts before giving up.
|
||||||
|
*/
|
||||||
|
sseMaxRetryAttempts?: number;
|
||||||
|
/**
|
||||||
|
* Maximum retry delay in milliseconds.
|
||||||
|
*
|
||||||
|
* Applies only when exponential backoff is used.
|
||||||
|
*
|
||||||
|
* This option applies only if the endpoint returns a stream of events.
|
||||||
|
*
|
||||||
|
* @default 30000
|
||||||
|
*/
|
||||||
|
sseMaxRetryDelay?: number;
|
||||||
|
/**
|
||||||
|
* Optional sleep function for retry backoff.
|
||||||
|
*
|
||||||
|
* Defaults to using `setTimeout`.
|
||||||
|
*/
|
||||||
|
sseSleepFn?: (ms: number) => Promise<void>;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface StreamEvent<TData = unknown> {
|
||||||
|
data: TData;
|
||||||
|
event?: string;
|
||||||
|
id?: string;
|
||||||
|
retry?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ServerSentEventsResult<TData = unknown, TReturn = void, TNext = unknown> = {
|
||||||
|
stream: AsyncGenerator<
|
||||||
|
TData extends Record<string, unknown> ? TData[keyof TData] : TData,
|
||||||
|
TReturn,
|
||||||
|
TNext
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createSseClient<TData = unknown>({
|
||||||
|
onRequest,
|
||||||
|
onSseError,
|
||||||
|
onSseEvent,
|
||||||
|
responseTransformer,
|
||||||
|
responseValidator,
|
||||||
|
sseDefaultRetryDelay,
|
||||||
|
sseMaxRetryAttempts,
|
||||||
|
sseMaxRetryDelay,
|
||||||
|
sseSleepFn,
|
||||||
|
url,
|
||||||
|
...options
|
||||||
|
}: ServerSentEventsOptions): ServerSentEventsResult<TData> {
|
||||||
|
let lastEventId: string | undefined;
|
||||||
|
|
||||||
|
const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
|
||||||
|
|
||||||
|
const createStream = async function* () {
|
||||||
|
let retryDelay: number = sseDefaultRetryDelay ?? 3000;
|
||||||
|
let attempt = 0;
|
||||||
|
const signal = options.signal ?? new AbortController().signal;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (signal.aborted) break;
|
||||||
|
|
||||||
|
attempt++;
|
||||||
|
|
||||||
|
const headers =
|
||||||
|
options.headers instanceof Headers
|
||||||
|
? options.headers
|
||||||
|
: new Headers(options.headers as Record<string, string> | undefined);
|
||||||
|
|
||||||
|
if (lastEventId !== undefined) {
|
||||||
|
headers.set('Last-Event-ID', lastEventId);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const requestInit: RequestInit = {
|
||||||
|
redirect: 'follow',
|
||||||
|
...options,
|
||||||
|
body: options.serializedBody,
|
||||||
|
headers,
|
||||||
|
signal,
|
||||||
|
};
|
||||||
|
let request = new Request(url, requestInit);
|
||||||
|
if (onRequest) {
|
||||||
|
request = await onRequest(url, requestInit);
|
||||||
|
}
|
||||||
|
// fetch must be assigned here, otherwise it would throw the error:
|
||||||
|
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
|
||||||
|
const _fetch = options.fetch ?? globalThis.fetch;
|
||||||
|
const response = await _fetch(request);
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`);
|
||||||
|
|
||||||
|
if (!response.body) throw new Error('No body in SSE response');
|
||||||
|
|
||||||
|
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
|
||||||
|
|
||||||
|
let buffer = '';
|
||||||
|
|
||||||
|
const abortHandler = () => {
|
||||||
|
try {
|
||||||
|
reader.cancel();
|
||||||
|
} catch {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
signal.addEventListener('abort', abortHandler);
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
buffer += value;
|
||||||
|
buffer = buffer.replace(/\r\n?/g, '\n'); // normalize line endings
|
||||||
|
|
||||||
|
const chunks = buffer.split('\n\n');
|
||||||
|
buffer = chunks.pop() ?? '';
|
||||||
|
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
const lines = chunk.split('\n');
|
||||||
|
const dataLines: Array<string> = [];
|
||||||
|
let eventName: string | undefined;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('data:')) {
|
||||||
|
dataLines.push(line.replace(/^data:\s*/, ''));
|
||||||
|
} else if (line.startsWith('event:')) {
|
||||||
|
eventName = line.replace(/^event:\s*/, '');
|
||||||
|
} else if (line.startsWith('id:')) {
|
||||||
|
lastEventId = line.replace(/^id:\s*/, '');
|
||||||
|
} else if (line.startsWith('retry:')) {
|
||||||
|
const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10);
|
||||||
|
if (!Number.isNaN(parsed)) {
|
||||||
|
retryDelay = parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let data: unknown;
|
||||||
|
let parsedJson = false;
|
||||||
|
|
||||||
|
if (dataLines.length) {
|
||||||
|
const rawData = dataLines.join('\n');
|
||||||
|
try {
|
||||||
|
data = JSON.parse(rawData);
|
||||||
|
parsedJson = true;
|
||||||
|
} catch {
|
||||||
|
data = rawData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedJson) {
|
||||||
|
if (responseValidator) {
|
||||||
|
await responseValidator(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseTransformer) {
|
||||||
|
data = await responseTransformer(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSseEvent?.({
|
||||||
|
data,
|
||||||
|
event: eventName,
|
||||||
|
id: lastEventId,
|
||||||
|
retry: retryDelay,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (dataLines.length) {
|
||||||
|
yield data as any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
signal.removeEventListener('abort', abortHandler);
|
||||||
|
reader.releaseLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
break; // exit loop on normal completion
|
||||||
|
} catch (error) {
|
||||||
|
// connection failed or aborted; retry after delay
|
||||||
|
onSseError?.(error);
|
||||||
|
|
||||||
|
if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) {
|
||||||
|
break; // stop after firing error
|
||||||
|
}
|
||||||
|
|
||||||
|
// exponential backoff: double retry each attempt, cap at 30s
|
||||||
|
const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000);
|
||||||
|
await sleep(backoff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stream = createStream();
|
||||||
|
|
||||||
|
return { stream };
|
||||||
|
}
|
||||||
104
polylan_submitter/src/api/core/types.gen.ts
Normal file
104
polylan_submitter/src/api/core/types.gen.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
|
import type { Auth, AuthToken } from './auth.gen';
|
||||||
|
import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen';
|
||||||
|
|
||||||
|
export type HttpMethod =
|
||||||
|
| 'connect'
|
||||||
|
| 'delete'
|
||||||
|
| 'get'
|
||||||
|
| 'head'
|
||||||
|
| 'options'
|
||||||
|
| 'patch'
|
||||||
|
| 'post'
|
||||||
|
| 'put'
|
||||||
|
| 'trace';
|
||||||
|
|
||||||
|
export type Client<
|
||||||
|
RequestFn = never,
|
||||||
|
Config = unknown,
|
||||||
|
MethodFn = never,
|
||||||
|
BuildUrlFn = never,
|
||||||
|
SseFn = never,
|
||||||
|
> = {
|
||||||
|
/**
|
||||||
|
* Returns the final request URL.
|
||||||
|
*/
|
||||||
|
buildUrl: BuildUrlFn;
|
||||||
|
getConfig: () => Config;
|
||||||
|
request: RequestFn;
|
||||||
|
setConfig: (config: Config) => Config;
|
||||||
|
} & {
|
||||||
|
[K in HttpMethod]: MethodFn;
|
||||||
|
} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } });
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
/**
|
||||||
|
* Auth token or a function returning auth token. The resolved value will be
|
||||||
|
* added to the request payload as defined by its `security` array.
|
||||||
|
*/
|
||||||
|
auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken;
|
||||||
|
/**
|
||||||
|
* A function for serializing request body parameter. By default,
|
||||||
|
* {@link JSON.stringify()} will be used.
|
||||||
|
*/
|
||||||
|
bodySerializer?: BodySerializer | null;
|
||||||
|
/**
|
||||||
|
* An object containing any HTTP headers that you want to pre-populate your
|
||||||
|
* `Headers` object with.
|
||||||
|
*
|
||||||
|
* {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more}
|
||||||
|
*/
|
||||||
|
headers?:
|
||||||
|
| RequestInit['headers']
|
||||||
|
| Record<
|
||||||
|
string,
|
||||||
|
string | number | boolean | (string | number | boolean)[] | null | undefined | unknown
|
||||||
|
>;
|
||||||
|
/**
|
||||||
|
* The request method.
|
||||||
|
*
|
||||||
|
* {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more}
|
||||||
|
*/
|
||||||
|
method?: Uppercase<HttpMethod>;
|
||||||
|
/**
|
||||||
|
* A function for serializing request query parameters. By default, arrays
|
||||||
|
* will be exploded in form style, objects will be exploded in deepObject
|
||||||
|
* style, and reserved characters are percent-encoded.
|
||||||
|
*
|
||||||
|
* This method will have no effect if the native `paramsSerializer()` Axios
|
||||||
|
* API function is used.
|
||||||
|
*
|
||||||
|
* {@link https://swagger.io/docs/specification/serialization/#query View examples}
|
||||||
|
*/
|
||||||
|
querySerializer?: QuerySerializer | QuerySerializerOptions;
|
||||||
|
/**
|
||||||
|
* A function validating request data. This is useful if you want to ensure
|
||||||
|
* the request conforms to the desired shape, so it can be safely sent to
|
||||||
|
* the server.
|
||||||
|
*/
|
||||||
|
requestValidator?: (data: unknown) => Promise<unknown>;
|
||||||
|
/**
|
||||||
|
* A function transforming response data before it's returned. This is useful
|
||||||
|
* for post-processing data, e.g., converting ISO strings into Date objects.
|
||||||
|
*/
|
||||||
|
responseTransformer?: (data: unknown) => Promise<unknown>;
|
||||||
|
/**
|
||||||
|
* A function validating response data. This is useful if you want to ensure
|
||||||
|
* the response conforms to the desired shape, so it can be safely passed to
|
||||||
|
* the transformers and returned to the user.
|
||||||
|
*/
|
||||||
|
responseValidator?: (data: unknown) => Promise<unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
|
||||||
|
? true
|
||||||
|
: [T] extends [never | undefined]
|
||||||
|
? [undefined] extends [T]
|
||||||
|
? false
|
||||||
|
: true
|
||||||
|
: false;
|
||||||
|
|
||||||
|
export type OmitNever<T extends Record<string, unknown>> = {
|
||||||
|
[K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true ? never : K]: T[K];
|
||||||
|
};
|
||||||
140
polylan_submitter/src/api/core/utils.gen.ts
Normal file
140
polylan_submitter/src/api/core/utils.gen.ts
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
|
import type { BodySerializer, QuerySerializer } from './bodySerializer.gen';
|
||||||
|
import {
|
||||||
|
type ArraySeparatorStyle,
|
||||||
|
serializeArrayParam,
|
||||||
|
serializeObjectParam,
|
||||||
|
serializePrimitiveParam,
|
||||||
|
} from './pathSerializer.gen';
|
||||||
|
|
||||||
|
export interface PathSerializer {
|
||||||
|
path: Record<string, unknown>;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PATH_PARAM_RE = /\{[^{}]+\}/g;
|
||||||
|
|
||||||
|
export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
|
||||||
|
let url = _url;
|
||||||
|
const matches = _url.match(PATH_PARAM_RE);
|
||||||
|
if (matches) {
|
||||||
|
for (const match of matches) {
|
||||||
|
let explode = false;
|
||||||
|
let name = match.substring(1, match.length - 1);
|
||||||
|
let style: ArraySeparatorStyle = 'simple';
|
||||||
|
|
||||||
|
if (name.endsWith('*')) {
|
||||||
|
explode = true;
|
||||||
|
name = name.substring(0, name.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.startsWith('.')) {
|
||||||
|
name = name.substring(1);
|
||||||
|
style = 'label';
|
||||||
|
} else if (name.startsWith(';')) {
|
||||||
|
name = name.substring(1);
|
||||||
|
style = 'matrix';
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = path[name];
|
||||||
|
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
url = url.replace(match, serializeArrayParam({ explode, name, style, value }));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
url = url.replace(
|
||||||
|
match,
|
||||||
|
serializeObjectParam({
|
||||||
|
explode,
|
||||||
|
name,
|
||||||
|
style,
|
||||||
|
value: value as Record<string, unknown>,
|
||||||
|
valueOnly: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (style === 'matrix') {
|
||||||
|
url = url.replace(
|
||||||
|
match,
|
||||||
|
`;${serializePrimitiveParam({
|
||||||
|
name,
|
||||||
|
value: value as string,
|
||||||
|
})}`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const replaceValue = encodeURIComponent(
|
||||||
|
style === 'label' ? `.${value as string}` : (value as string),
|
||||||
|
);
|
||||||
|
url = url.replace(match, replaceValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUrl = ({
|
||||||
|
baseUrl,
|
||||||
|
path,
|
||||||
|
query,
|
||||||
|
querySerializer,
|
||||||
|
url: _url,
|
||||||
|
}: {
|
||||||
|
baseUrl?: string;
|
||||||
|
path?: Record<string, unknown>;
|
||||||
|
query?: Record<string, unknown>;
|
||||||
|
querySerializer: QuerySerializer;
|
||||||
|
url: string;
|
||||||
|
}) => {
|
||||||
|
const pathUrl = _url.startsWith('/') ? _url : `/${_url}`;
|
||||||
|
let url = (baseUrl ?? '') + pathUrl;
|
||||||
|
if (path) {
|
||||||
|
url = defaultPathSerializer({ path, url });
|
||||||
|
}
|
||||||
|
let search = query ? querySerializer(query) : '';
|
||||||
|
if (search.startsWith('?')) {
|
||||||
|
search = search.substring(1);
|
||||||
|
}
|
||||||
|
if (search) {
|
||||||
|
url += `?${search}`;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getValidRequestBody(options: {
|
||||||
|
body?: unknown;
|
||||||
|
bodySerializer?: BodySerializer | null;
|
||||||
|
serializedBody?: unknown;
|
||||||
|
}) {
|
||||||
|
const hasBody = options.body !== undefined;
|
||||||
|
const isSerializedBody = hasBody && options.bodySerializer;
|
||||||
|
|
||||||
|
if (isSerializedBody) {
|
||||||
|
if ('serializedBody' in options) {
|
||||||
|
const hasSerializedBody =
|
||||||
|
options.serializedBody !== undefined && options.serializedBody !== '';
|
||||||
|
|
||||||
|
return hasSerializedBody ? options.serializedBody : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// not all clients implement a serializedBody property (i.e., client-axios)
|
||||||
|
return options.body !== '' ? options.body : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// plain/text body
|
||||||
|
if (hasBody) {
|
||||||
|
return options.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
// no body was provided
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
4
polylan_submitter/src/api/index.ts
Normal file
4
polylan_submitter/src/api/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
|
export { animationsApiPuzzleResults, animationsApiResults, animationsApiTopSubmissions, gamesApiListGames, marketApiCloseMarket, marketApiCreateBet, marketApiListMarkets, marketApiListUserBets, marketApiResolveMarket, noitaApiGetLeaderboard, noitaApiGetResults, noitaApiSubmitLogFile, type Options, opusMagnumApiCreateSubmission, opusMagnumApiDeleteSubmission, opusMagnumApiGetCollection, opusMagnumApiGetStats, opusMagnumApiGetSubmission, opusMagnumApiListPuzzles, opusMagnumApiListResponsesNeedingValidation, opusMagnumApiListSubmissions, opusMagnumApiValidateAuto, opusMagnumApiValidateResponse, opusMagnumApiValidateSubmission, polylanSubmitterApiClearCache, polylanSubmitterApiGetUserInfo, polylanSubmitterApiHealthCheck } from './sdk.gen';
|
||||||
|
export type { AnimationsApiPuzzleResultsData, AnimationsApiPuzzleResultsResponse, AnimationsApiPuzzleResultsResponses, AnimationsApiResultsData, AnimationsApiResultsResponse, AnimationsApiResultsResponses, AnimationsApiTopSubmissionsData, AnimationsApiTopSubmissionsResponse, AnimationsApiTopSubmissionsResponses, ClientOptions, GameOut, GamesApiListGamesData, GamesApiListGamesResponse, GamesApiListGamesResponses, Input, LeaderboardEntryOut, LeaderboardOut, MarketApiCloseMarketData, MarketApiCloseMarketResponses, MarketApiCreateBetData, MarketApiCreateBetResponse, MarketApiCreateBetResponses, 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, PolylanSubmitterApiClearCacheData, PolylanSubmitterApiClearCacheResponses, PolylanSubmitterApiGetUserInfoData, PolylanSubmitterApiGetUserInfoResponse, PolylanSubmitterApiGetUserInfoResponses, PolylanSubmitterApiHealthCheckData, PolylanSubmitterApiHealthCheckResponses, PuzzlePointsFactorOut, PuzzleResponseIn, PuzzleResponseOut, PuzzleResponseRankingOut, PuzzleResultsOut, PuzzleSubmissionsOut, PuzzleSubmissionWithRankOut, RankingSchema, ResolveMarketSchema, ResultsOut, SteamCollectionItemOut, SteamCollectionOut, SubmissionFileOut, SubmissionIn, SubmissionOut, TournamentPuzzleResultsOut, TournamentSubmissionsOut, UserBetCreateSchema, UserBetSchema, UserDisplayOut, UserInfoOut, ValidationIn, WinnerFileOut, WinnerResponseOut } from './types.gen';
|
||||||
241
polylan_submitter/src/api/sdk.gen.ts
Normal file
241
polylan_submitter/src/api/sdk.gen.ts
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
|
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, GamesApiListGamesData, GamesApiListGamesResponses, MarketApiCloseMarketData, MarketApiCloseMarketResponses, MarketApiCreateBetData, MarketApiCreateBetResponses, 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, PolylanSubmitterApiClearCacheData, PolylanSubmitterApiClearCacheResponses, PolylanSubmitterApiGetUserInfoData, PolylanSubmitterApiGetUserInfoResponses, PolylanSubmitterApiHealthCheckData, PolylanSubmitterApiHealthCheckResponses } from './types.gen';
|
||||||
|
|
||||||
|
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, TResponse = unknown> = Options2<TData, ThrowOnError, TResponse> & {
|
||||||
|
/**
|
||||||
|
* You can provide a client instance returned by `createClient()` instead of
|
||||||
|
* individual options. This might be also useful if you want to implement a
|
||||||
|
* custom client.
|
||||||
|
*/
|
||||||
|
client?: Client;
|
||||||
|
/**
|
||||||
|
* You can pass arbitrary values through the `meta` object. This can be
|
||||||
|
* used to access values that aren't defined as part of the SDK function.
|
||||||
|
*/
|
||||||
|
meta?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Health Check
|
||||||
|
*
|
||||||
|
* Health check endpoint
|
||||||
|
*/
|
||||||
|
export const polylanSubmitterApiHealthCheck = <ThrowOnError extends boolean = false>(options?: Options<PolylanSubmitterApiHealthCheckData, ThrowOnError>) => (options?.client ?? client).get<PolylanSubmitterApiHealthCheckResponses, unknown, ThrowOnError>({ url: '/api/health', ...options });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear Cache
|
||||||
|
*
|
||||||
|
* Clear all API caches (admin only)
|
||||||
|
*/
|
||||||
|
export const polylanSubmitterApiClearCache = <ThrowOnError extends boolean = false>(options?: Options<PolylanSubmitterApiClearCacheData, ThrowOnError>) => (options?.client ?? client).post<PolylanSubmitterApiClearCacheResponses, unknown, ThrowOnError>({ url: '/api/cache/clear', ...options });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get User Info
|
||||||
|
*
|
||||||
|
* Get current user information
|
||||||
|
*/
|
||||||
|
export const polylanSubmitterApiGetUserInfo = <ThrowOnError extends boolean = false>(options?: Options<PolylanSubmitterApiGetUserInfoData, ThrowOnError>) => (options?.client ?? client).get<PolylanSubmitterApiGetUserInfoResponses, unknown, ThrowOnError>({ url: '/api/user', ...options });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List Puzzles
|
||||||
|
*
|
||||||
|
* Get list of available puzzles
|
||||||
|
*/
|
||||||
|
export const opusMagnumApiListPuzzles = <ThrowOnError extends boolean = false>(options?: Options<OpusMagnumApiListPuzzlesData, ThrowOnError>) => (options?.client ?? client).get<OpusMagnumApiListPuzzlesResponses, unknown, ThrowOnError>({ url: '/api/opus-magnum/puzzles', ...options });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Collection
|
||||||
|
*
|
||||||
|
* Get the active collection details
|
||||||
|
*/
|
||||||
|
export const opusMagnumApiGetCollection = <ThrowOnError extends boolean = false>(options?: Options<OpusMagnumApiGetCollectionData, ThrowOnError>) => (options?.client ?? client).get<OpusMagnumApiGetCollectionResponses, unknown, ThrowOnError>({ url: '/api/opus-magnum/collection', ...options });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List Submissions
|
||||||
|
*
|
||||||
|
* Get paginated list of submissions
|
||||||
|
*/
|
||||||
|
export const opusMagnumApiListSubmissions = <ThrowOnError extends boolean = false>(options?: Options<OpusMagnumApiListSubmissionsData, ThrowOnError>) => (options?.client ?? client).get<OpusMagnumApiListSubmissionsResponses, unknown, ThrowOnError>({ url: '/api/opus-magnum/submissions', ...options });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create Submission
|
||||||
|
*
|
||||||
|
* Create a new submission with multiple puzzle responses
|
||||||
|
*/
|
||||||
|
export const opusMagnumApiCreateSubmission = <ThrowOnError extends boolean = false>(options: Options<OpusMagnumApiCreateSubmissionData, ThrowOnError>) => (options.client ?? client).post<OpusMagnumApiCreateSubmissionResponses, unknown, ThrowOnError>({
|
||||||
|
...formDataBodySerializer,
|
||||||
|
url: '/api/opus-magnum/submissions',
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': null,
|
||||||
|
...options.headers
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete Submission
|
||||||
|
*
|
||||||
|
* Delete a submission (admin only)
|
||||||
|
*/
|
||||||
|
export const opusMagnumApiDeleteSubmission = <ThrowOnError extends boolean = false>(options: Options<OpusMagnumApiDeleteSubmissionData, ThrowOnError>) => (options.client ?? client).delete<OpusMagnumApiDeleteSubmissionResponses, unknown, ThrowOnError>({ url: '/api/opus-magnum/submissions/{submission_id}', ...options });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Submission
|
||||||
|
*
|
||||||
|
* Get detailed submission by ID
|
||||||
|
*/
|
||||||
|
export const opusMagnumApiGetSubmission = <ThrowOnError extends boolean = false>(options: Options<OpusMagnumApiGetSubmissionData, ThrowOnError>) => (options.client ?? client).get<OpusMagnumApiGetSubmissionResponses, unknown, ThrowOnError>({ url: '/api/opus-magnum/submissions/{submission_id}', ...options });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate Response
|
||||||
|
*
|
||||||
|
* Manually validate a puzzle response
|
||||||
|
*/
|
||||||
|
export const opusMagnumApiValidateResponse = <ThrowOnError extends boolean = false>(options: Options<OpusMagnumApiValidateResponseData, ThrowOnError>) => (options.client ?? client).put<OpusMagnumApiValidateResponseResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/opus-magnum/responses/{response_id}/validate',
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate Auto
|
||||||
|
*
|
||||||
|
* Try to auto validate a puzzle response
|
||||||
|
*/
|
||||||
|
export const opusMagnumApiValidateAuto = <ThrowOnError extends boolean = false>(options: Options<OpusMagnumApiValidateAutoData, ThrowOnError>) => (options.client ?? client).put<OpusMagnumApiValidateAutoResponses, unknown, ThrowOnError>({ url: '/api/opus-magnum/responses/{response_id}/validate/auto', ...options });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List Responses Needing Validation
|
||||||
|
*
|
||||||
|
* Get all responses that need manual validation
|
||||||
|
*/
|
||||||
|
export const opusMagnumApiListResponsesNeedingValidation = <ThrowOnError extends boolean = false>(options?: Options<OpusMagnumApiListResponsesNeedingValidationData, ThrowOnError>) => (options?.client ?? client).get<OpusMagnumApiListResponsesNeedingValidationResponses, unknown, ThrowOnError>({ url: '/api/opus-magnum/responses/needs-validation', ...options });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate Submission
|
||||||
|
*
|
||||||
|
* Mark entire submission as validated
|
||||||
|
*/
|
||||||
|
export const opusMagnumApiValidateSubmission = <ThrowOnError extends boolean = false>(options: Options<OpusMagnumApiValidateSubmissionData, ThrowOnError>) => (options.client ?? client).post<OpusMagnumApiValidateSubmissionResponses, unknown, ThrowOnError>({ url: '/api/opus-magnum/submissions/{submission_id}/validate', ...options });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Stats
|
||||||
|
*
|
||||||
|
* Get submission statistics
|
||||||
|
*/
|
||||||
|
export const opusMagnumApiGetStats = <ThrowOnError extends boolean = false>(options?: Options<OpusMagnumApiGetStatsData, ThrowOnError>) => (options?.client ?? client).get<OpusMagnumApiGetStatsResponses, unknown, ThrowOnError>({ url: '/api/opus-magnum/stats', ...options });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Results
|
||||||
|
*/
|
||||||
|
export const animationsApiResults = <ThrowOnError extends boolean = false>(options?: Options<AnimationsApiResultsData, ThrowOnError>) => (options?.client ?? client).get<AnimationsApiResultsResponses, unknown, ThrowOnError>({ url: '/api/results/results', ...options });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Top Submissions
|
||||||
|
*
|
||||||
|
* Get tournament top submissions for each puzzle. Only available when tournament is closed.
|
||||||
|
*/
|
||||||
|
export const animationsApiTopSubmissions = <ThrowOnError extends boolean = false>(options?: Options<AnimationsApiTopSubmissionsData, ThrowOnError>) => (options?.client ?? client).get<AnimationsApiTopSubmissionsResponses, unknown, ThrowOnError>({ url: '/api/results/top-submissions', ...options });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Puzzle Results
|
||||||
|
*
|
||||||
|
* Get tournament results organized by puzzle with coefficients. Only available when tournament is closed.
|
||||||
|
*/
|
||||||
|
export const animationsApiPuzzleResults = <ThrowOnError extends boolean = false>(options?: Options<AnimationsApiPuzzleResultsData, ThrowOnError>) => (options?.client ?? client).get<AnimationsApiPuzzleResultsResponses, unknown, ThrowOnError>({ url: '/api/results/puzzle-results', ...options });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Results
|
||||||
|
*/
|
||||||
|
export const noitaApiGetResults = <ThrowOnError extends boolean = false>(options?: Options<NoitaApiGetResultsData, ThrowOnError>) => (options?.client ?? client).get<NoitaApiGetResultsResponses, unknown, ThrowOnError>({ url: '/api/noita/results', ...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 noitaApiGetLeaderboard = <ThrowOnError extends boolean = false>(options?: Options<NoitaApiGetLeaderboardData, ThrowOnError>) => (options?.client ?? client).get<NoitaApiGetLeaderboardResponses, unknown, ThrowOnError>({ url: '/api/noita/leaderboard', ...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 noitaApiSubmitLogFile = <ThrowOnError extends boolean = false>(options: Options<NoitaApiSubmitLogFileData, ThrowOnError>) => (options.client ?? client).post<NoitaApiSubmitLogFileResponses, NoitaApiSubmitLogFileErrors, ThrowOnError>({
|
||||||
|
...formDataBodySerializer,
|
||||||
|
url: '/api/noita/submit',
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': null,
|
||||||
|
...options.headers
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List Games
|
||||||
|
*/
|
||||||
|
export const gamesApiListGames = <ThrowOnError extends boolean = false>(options?: Options<GamesApiListGamesData, ThrowOnError>) => (options?.client ?? client).get<GamesApiListGamesResponses, unknown, ThrowOnError>({ url: '/api/games/', ...options });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List Markets
|
||||||
|
*
|
||||||
|
* List all markets (excludes draft markets).
|
||||||
|
*/
|
||||||
|
export const marketApiListMarkets = <ThrowOnError extends boolean = false>(options?: Options<MarketApiListMarketsData, ThrowOnError>) => (options?.client ?? client).get<MarketApiListMarketsResponses, unknown, ThrowOnError>({ url: '/api/market/', ...options });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List User Bets
|
||||||
|
*
|
||||||
|
* List all bets placed by the current user.
|
||||||
|
*/
|
||||||
|
export const marketApiListUserBets = <ThrowOnError extends boolean = false>(options?: Options<MarketApiListUserBetsData, ThrowOnError>) => (options?.client ?? client).get<MarketApiListUserBetsResponses, unknown, ThrowOnError>({ url: '/api/market/user/bets', ...options });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close Market
|
||||||
|
*
|
||||||
|
* Close a market. Admin only.
|
||||||
|
*/
|
||||||
|
export const marketApiCloseMarket = <ThrowOnError extends boolean = false>(options: Options<MarketApiCloseMarketData, ThrowOnError>) => (options.client ?? client).post<MarketApiCloseMarketResponses, unknown, ThrowOnError>({ url: '/api/market/{market_uuid}/actions/close', ...options });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve Market
|
||||||
|
*
|
||||||
|
* Resolve a market with a winning option. Admin only.
|
||||||
|
*/
|
||||||
|
export const marketApiResolveMarket = <ThrowOnError extends boolean = false>(options: Options<MarketApiResolveMarketData, ThrowOnError>) => (options.client ?? client).post<MarketApiResolveMarketResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/market/{market_uuid}/actions/resolve',
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create Bet
|
||||||
|
*
|
||||||
|
* Place a bet on a market option.
|
||||||
|
*/
|
||||||
|
export const marketApiCreateBet = <ThrowOnError extends boolean = false>(options: Options<MarketApiCreateBetData, ThrowOnError>) => (options.client ?? client).post<MarketApiCreateBetResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/market/{market_uuid}/bets',
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers
|
||||||
|
}
|
||||||
|
});
|
||||||
1573
polylan_submitter/src/api/types.gen.ts
Normal file
1573
polylan_submitter/src/api/types.gen.ts
Normal file
File diff suppressed because it is too large
Load Diff
338
polylan_submitter/src/components/MarketCard.vue
Normal file
338
polylan_submitter/src/components/MarketCard.vue
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from "vue";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { marketApiCreateBet, marketApiCloseMarket, marketApiResolveMarket } from "../api";
|
||||||
|
import { useMarketStore } from "../stores/market";
|
||||||
|
import type { Market, MarketOption } from "../types";
|
||||||
|
import type { UserBetSchema } from "../api/types.gen";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
market: Market;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
refresh: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const marketStore = useMarketStore();
|
||||||
|
const { userInfo, userBets } = storeToRefs(marketStore);
|
||||||
|
|
||||||
|
const selectedOption = ref<string | null>(null);
|
||||||
|
const betAmount = ref<number>(0);
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref<string>("");
|
||||||
|
const existingBet = ref<UserBetSchema | null>(null);
|
||||||
|
const showResolveModal = ref(false);
|
||||||
|
const selectedWinningOption = ref<string | null>(null);
|
||||||
|
const resolveLoading = ref(false);
|
||||||
|
|
||||||
|
const timeRemaining = computed(() => {
|
||||||
|
const endDate = new Date(props.market.end_date).getTime();
|
||||||
|
const now = new Date().getTime();
|
||||||
|
const diff = endDate - now;
|
||||||
|
|
||||||
|
if (diff <= 0) return "Ended";
|
||||||
|
|
||||||
|
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||||
|
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||||
|
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
|
|
||||||
|
if (days > 0) return `${days}d ${hours}h`;
|
||||||
|
if (hours > 0) return `${hours}h ${minutes}m`;
|
||||||
|
return `${minutes}m`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusColor = computed(() => {
|
||||||
|
switch (props.market.status) {
|
||||||
|
case "draft":
|
||||||
|
return "badge-secondary";
|
||||||
|
case "open":
|
||||||
|
return "badge-success";
|
||||||
|
case "closed":
|
||||||
|
return "badge-warning";
|
||||||
|
case "resolved":
|
||||||
|
return "badge-info";
|
||||||
|
default:
|
||||||
|
return "badge-ghost";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const canBet = computed(() => {
|
||||||
|
return props.market.status === "open" && userInfo.value?.is_authenticated && selectedOption.value && betAmount.value > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPot = computed(() => {
|
||||||
|
return props.market.options.reduce((sum, opt) => sum + opt.total_bets, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getMultiplier = (option: MarketOption) => {
|
||||||
|
if (option.total_bets === 0) return 0;
|
||||||
|
return totalPot.value / option.total_bets;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPotentialGain = (option: MarketOption) => {
|
||||||
|
if (!existingBet.value || existingBet.value.option.uuid !== option.uuid) return 0;
|
||||||
|
const multiplier = getMultiplier(option);
|
||||||
|
return Math.round(existingBet.value.amount * multiplier * props.market.multiplier);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeMarket = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await marketApiCloseMarket({
|
||||||
|
path: { market_uuid: props.market.uuid },
|
||||||
|
});
|
||||||
|
emit("refresh");
|
||||||
|
} catch (e) {
|
||||||
|
error.value = "Error closing market";
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveMarket = async () => {
|
||||||
|
if (!selectedWinningOption.value) {
|
||||||
|
error.value = "Please select a winning option";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveLoading.value = true;
|
||||||
|
error.value = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await marketApiResolveMarket({
|
||||||
|
path: { market_uuid: props.market.uuid },
|
||||||
|
body: { winning_option_uuid: selectedWinningOption.value },
|
||||||
|
});
|
||||||
|
emit("refresh");
|
||||||
|
showResolveModal.value = false;
|
||||||
|
selectedWinningOption.value = null;
|
||||||
|
} catch (e) {
|
||||||
|
const err = e as any;
|
||||||
|
if (typeof err === 'object' && err?.detail) {
|
||||||
|
error.value = err.detail;
|
||||||
|
} else if (typeof err === 'string') {
|
||||||
|
error.value = err;
|
||||||
|
} else {
|
||||||
|
error.value = "Error resolving market";
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
resolveLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const placeBet = async () => {
|
||||||
|
if (!selectedOption.value || !betAmount.value) return;
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
error.value = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await marketApiCreateBet({
|
||||||
|
path: {
|
||||||
|
market_uuid: props.market.uuid
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
option_uuid: selectedOption.value,
|
||||||
|
amount: betAmount.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.error) {
|
||||||
|
// Reload user bets to reflect the new bet
|
||||||
|
await marketStore.loadUserBets();
|
||||||
|
updateExistingBet();
|
||||||
|
betAmount.value = 0;
|
||||||
|
} else {
|
||||||
|
const err = response.error as any;
|
||||||
|
if (typeof err === 'object' && err?.detail) {
|
||||||
|
error.value = err.detail;
|
||||||
|
} else if (typeof err === 'string') {
|
||||||
|
error.value = err;
|
||||||
|
} else {
|
||||||
|
error.value = "Failed to place bet";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
error.value = "Error placing bet";
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// Update existing bet when component mounts or userBets changes
|
||||||
|
updateExistingBet();
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateExistingBet = () => {
|
||||||
|
if (userBets.value) {
|
||||||
|
existingBet.value = userBets.value.find(bet => bet.market?.uuid === props.market.uuid) || null;
|
||||||
|
if (existingBet.value) {
|
||||||
|
selectedOption.value = existingBet.value.option.uuid;
|
||||||
|
betAmount.value = existingBet.value.amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="card bg-base-100 shadow-xl">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="card-body pb-3">
|
||||||
|
<div class="flex justify-between items-start gap-4">
|
||||||
|
<div class="flex-1">
|
||||||
|
<h2 class="card-title text-2xl mb-2">{{ market.title }}</h2>
|
||||||
|
<p class="text-sm text-base-content/70">{{ market.description }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-end gap-2">
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<div :class="['badge', statusColor, 'text-white']">
|
||||||
|
{{ market.status }}
|
||||||
|
</div>
|
||||||
|
<div v-if="market.multiplier > 1" class="badge badge-accent text-white font-bold">
|
||||||
|
{{ market.multiplier }}x
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="market.status === 'open' || market.status === 'closed'" class="text-sm text-base-content/60 text-right">
|
||||||
|
<div>{{ timeRemaining }}</div>
|
||||||
|
<div class="text-xs">until close</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider my-0"></div>
|
||||||
|
|
||||||
|
<!-- Admin Actions -->
|
||||||
|
<div v-if="userInfo?.is_superuser" class="card-body py-4 bg-base-100">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button v-if="market.status === 'open'" @click="closeMarket" :disabled="loading" class="btn btn-sm btn-warning">
|
||||||
|
<span v-if="loading" class="loading loading-spinner loading-sm"></span>
|
||||||
|
<span v-else>Close Market</span>
|
||||||
|
</button>
|
||||||
|
<button v-if="market.status === 'closed'" @click="showResolveModal = true" class="btn btn-sm btn-success">
|
||||||
|
Resolve Market
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Resolve Modal -->
|
||||||
|
<dialog v-if="showResolveModal" class="modal modal-open">
|
||||||
|
<div class="modal-box">
|
||||||
|
<h3 class="font-bold text-lg mb-4">Resolve Market - Select Winner</h3>
|
||||||
|
|
||||||
|
<div class="space-y-2 mb-4">
|
||||||
|
<div v-for="option in market.options" :key="option.uuid" class="form-control">
|
||||||
|
<label class="label cursor-pointer border rounded-lg p-3 hover:bg-base-200 transition w-full flex justify-between">
|
||||||
|
<span class="label-text font-medium">{{ option.text }}</span>
|
||||||
|
<input type="radio" :value="option.uuid" v-model="selectedWinningOption" class="radio radio-primary" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="error" class="alert alert-error mb-4">
|
||||||
|
<i class="mdi mdi-alert-circle"></i>
|
||||||
|
<span>{{ error }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-action">
|
||||||
|
<button @click="showResolveModal = false" class="btn" :disabled="resolveLoading">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button @click="resolveMarket" :disabled="!selectedWinningOption || resolveLoading" class="btn btn-primary">
|
||||||
|
<span v-if="resolveLoading" class="loading loading-spinner loading-sm"></span>
|
||||||
|
<span v-else>Resolve & Distribute Points</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-backdrop" @click="showResolveModal = false"></div>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<!-- Options -->
|
||||||
|
<div class="card-body py-4">
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div v-for="option in market.options" :key="option.uuid" class="form-control">
|
||||||
|
<label :class="[
|
||||||
|
'label cursor-pointer border rounded-lg p-3 hover:bg-base-200 transition h-full w-full',
|
||||||
|
existingBet && existingBet.option.uuid === option.uuid ? 'border-primary border-2 bg-primary/5' : ''
|
||||||
|
]">
|
||||||
|
<div class="flex flex-col gap-2 flex-1">
|
||||||
|
<span class="label-text font-medium">{{ option.text }}</span>
|
||||||
|
<div class="text-xs text-base-content/60">
|
||||||
|
<div>Pool: {{ option.total_bets }} pts</div>
|
||||||
|
<div v-if="option.total_bets > 0">Multiplier: {{ getMultiplier(option).toFixed(2) }}x</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="existingBet && existingBet.option.uuid === option.uuid" class="flex flex-col gap-1">
|
||||||
|
<span class="badge badge-primary badge-sm w-fit">
|
||||||
|
Your bet: {{ existingBet.amount }} pts
|
||||||
|
</span>
|
||||||
|
<span v-if="option.total_bets > 0" class="badge badge-success badge-sm w-fit">
|
||||||
|
Potential: {{ getPotentialGain(option) }} pts
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input type="radio" :value="option.uuid" v-model="selectedOption" class="radio radio-primary"
|
||||||
|
:disabled="market.status !== 'open' || !!(existingBet && existingBet.option.uuid !== option.uuid)" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bet Input -->
|
||||||
|
<div v-if="market.status === 'open' && userInfo?.is_authenticated && selectedOption" class="card-body py-4">
|
||||||
|
<div class="form-control gap-3">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">
|
||||||
|
<span v-if="existingBet">Increase bet (current: {{ existingBet.amount }} pts)</span>
|
||||||
|
<span v-else>Points to bet</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<input v-model.number="betAmount" type="number"
|
||||||
|
:placeholder="existingBet ? `Enter amount to increase by` : 'Enter points'" class="input input-bordered"
|
||||||
|
min="1" :disabled="loading" />
|
||||||
|
|
||||||
|
<div v-if="error" class="alert alert-error">
|
||||||
|
<i class="mdi mdi-alert-circle"></i>
|
||||||
|
<span>{{ error }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button @click="placeBet" :disabled="!canBet || loading" class="btn btn-primary">
|
||||||
|
<span v-if="loading" class="loading loading-spinner loading-sm"></span>
|
||||||
|
<span v-else-if="existingBet">Increase Bet</span>
|
||||||
|
<span v-else>Place Bet</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Show existing bet (market closed/resolved) -->
|
||||||
|
<div v-else-if="market.status !== 'open' && existingBet" class="card-body py-4 bg-base-200">
|
||||||
|
<div class="text-sm">
|
||||||
|
<div class="font-semibold">Your Bet</div>
|
||||||
|
<div class="text-base-content/70 mt-1">
|
||||||
|
Option: <span class="font-semibold">{{ existingBet.option.text }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-base-content/70">
|
||||||
|
Amount: <span class="font-semibold">{{ existingBet.amount }} pts</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Result -->
|
||||||
|
<div v-else-if="market.status === 'resolved' && market.winning_option" class="card-body py-4 bg-base-200">
|
||||||
|
<div class="text-sm">
|
||||||
|
<div class="font-semibold"
|
||||||
|
:class="existingBet && existingBet.option.uuid === market.winning_option.uuid ? 'text-success' : 'text-error'">
|
||||||
|
<span v-if="existingBet && existingBet.option.uuid === market.winning_option.uuid">✓ You Won!</span>
|
||||||
|
<span v-else-if="existingBet">✗ You Lost</span>
|
||||||
|
<span v-else>Resolved</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-base-content/70 mt-1">
|
||||||
|
Winner: <span class="font-semibold">{{ market.winning_option.text }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
182
polylan_submitter/src/components/UserBets.vue
Normal file
182
polylan_submitter/src/components/UserBets.vue
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useMarketStore } from "../stores/market";
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
refresh: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const marketStore = useMarketStore();
|
||||||
|
const { userBets } = storeToRefs(marketStore);
|
||||||
|
const loading = computed(() => marketStore.isLoading);
|
||||||
|
const isWonBetsExpanded = ref(false);
|
||||||
|
|
||||||
|
const totalBetAmount = computed(() => {
|
||||||
|
return userBets.value.reduce((sum, bet) => sum + bet.amount, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const winningBets = computed(() => {
|
||||||
|
return userBets.value.filter(bet => {
|
||||||
|
const market = bet.market;
|
||||||
|
return market?.status === "resolved" && market?.winning_option?.uuid === bet.option.uuid;
|
||||||
|
}).reverse();
|
||||||
|
});
|
||||||
|
|
||||||
|
const losingBets = computed(() => {
|
||||||
|
return userBets.value.filter(bet => {
|
||||||
|
const market = bet.market;
|
||||||
|
return market?.status === "resolved" && market?.winning_option?.uuid !== bet.option.uuid;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const openBets = computed(() => {
|
||||||
|
return userBets.value.filter(bet => bet.market?.status === "open");
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalWinnings = computed(() => {
|
||||||
|
return winningBets.value.reduce((sum, bet) => sum + bet.amount, 0);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<!-- Loading -->
|
||||||
|
<div v-if="loading" class="flex justify-center py-20">
|
||||||
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div v-else-if="userBets.length === 0" class="alert">
|
||||||
|
<i class="mdi mdi-information mr-2"></i>
|
||||||
|
<span>You haven't placed any bets yet</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<template v-else>
|
||||||
|
<!-- Stats -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<div class="stat bg-base-100 rounded-lg shadow">
|
||||||
|
<div class="stat-title text-sm">Total Bets</div>
|
||||||
|
<div class="stat-value text-2xl">{{ userBets.length }}</div>
|
||||||
|
<div class="stat-desc text-xs">{{ totalBetAmount }} points</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat bg-base-100 rounded-lg shadow">
|
||||||
|
<div class="stat-title text-sm">Active</div>
|
||||||
|
<div class="stat-value text-2xl text-info">{{ openBets.length }}</div>
|
||||||
|
<div class="stat-desc text-xs">Waiting for result</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat bg-base-100 rounded-lg shadow">
|
||||||
|
<div class="stat-title text-sm">Won</div>
|
||||||
|
<div class="stat-value text-2xl text-success">{{ winningBets.length }}</div>
|
||||||
|
<div class="stat-desc text-xs text-success">+{{ totalWinnings }} points</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat bg-base-100 rounded-lg shadow">
|
||||||
|
<div class="stat-title text-sm">Lost</div>
|
||||||
|
<div class="stat-value text-2xl text-error">{{ losingBets.length }}</div>
|
||||||
|
<div class="stat-desc text-xs">Better luck next time</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bets Sections -->
|
||||||
|
<div class="space-y-6">
|
||||||
|
<!-- Open Bets -->
|
||||||
|
<div v-if="openBets.length > 0">
|
||||||
|
<h3 class="text-xl font-bold mb-4 flex items-center gap-2">
|
||||||
|
<i class="mdi mdi-progress-clock text-info"></i>
|
||||||
|
Active Bets ({{ openBets.length }})
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div
|
||||||
|
v-for="bet in openBets"
|
||||||
|
:key="bet.uuid"
|
||||||
|
class="card bg-base-100 shadow hover:shadow-lg transition-shadow"
|
||||||
|
>
|
||||||
|
<div class="card-body py-4">
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<div class="flex-1">
|
||||||
|
<h4 class="font-semibold text-lg">{{ bet.market?.title }}</h4>
|
||||||
|
<p class="text-sm text-base-content/70">
|
||||||
|
Bet on: <span class="font-medium">{{ bet.option.text }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<div class="text-lg font-bold">{{ bet.amount }} pts</div>
|
||||||
|
<div class="text-xs text-base-content/60 mt-1">
|
||||||
|
Status: <span class="badge badge-info badge-sm">{{ bet.market?.status }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Winning Bets -->
|
||||||
|
<div v-if="winningBets.length > 0">
|
||||||
|
<button @click="isWonBetsExpanded = !isWonBetsExpanded" class="text-xl font-bold mb-4 flex items-center gap-2 cursor-pointer hover:opacity-70 transition-opacity">
|
||||||
|
<i :class="['mdi', isWonBetsExpanded ? 'mdi-chevron-down' : 'mdi-chevron-right']"></i>
|
||||||
|
<i class="mdi mdi-check-circle text-success"></i>
|
||||||
|
Won Bets ({{ winningBets.length }})
|
||||||
|
</button>
|
||||||
|
<div v-if="isWonBetsExpanded" class="space-y-4">
|
||||||
|
<div
|
||||||
|
v-for="bet in winningBets"
|
||||||
|
:key="bet.uuid"
|
||||||
|
class="card bg-success/10 border border-success shadow"
|
||||||
|
>
|
||||||
|
<div class="card-body py-4">
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<div class="flex-1">
|
||||||
|
<h4 class="font-semibold text-lg">{{ bet.market?.title }}</h4>
|
||||||
|
<p class="text-sm text-base-content/70">
|
||||||
|
Correct! You bet on: <span class="font-medium text-success">{{ bet.option.text }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<div class="text-lg font-bold text-success">+{{ bet.amount }} pts</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Losing Bets -->
|
||||||
|
<div v-if="losingBets.length > 0">
|
||||||
|
<h3 class="text-xl font-bold mb-4 flex items-center gap-2">
|
||||||
|
<i class="mdi mdi-close-circle text-error"></i>
|
||||||
|
Lost Bets ({{ losingBets.length }})
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div
|
||||||
|
v-for="bet in losingBets"
|
||||||
|
:key="bet.uuid"
|
||||||
|
class="card bg-error/10 border border-error shadow"
|
||||||
|
>
|
||||||
|
<div class="card-body py-4">
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<div class="flex-1">
|
||||||
|
<h4 class="font-semibold text-lg">{{ bet.market?.title }}</h4>
|
||||||
|
<p class="text-sm text-base-content/70">
|
||||||
|
You bet on: <span class="font-medium">{{ bet.option.text }}</span>
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-base-content/60 mt-1">
|
||||||
|
Winner: <span class="font-medium">{{ bet.market?.winning_option?.text }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<div class="text-lg font-bold text-error">-{{ bet.amount }} pts</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
10
polylan_submitter/src/market.ts
Normal file
10
polylan_submitter/src/market.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import Market from '@/Market.vue'
|
||||||
|
import { pinia } from '@/stores'
|
||||||
|
import '@/style.css'
|
||||||
|
|
||||||
|
const selector = "#app"
|
||||||
|
const mountData = document.querySelector<HTMLElement>(selector)
|
||||||
|
const app = createApp(Market, { ...mountData?.dataset })
|
||||||
|
app.use(pinia)
|
||||||
|
app.mount(selector)
|
||||||
@ -1,8 +1,10 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import Noita from '@/Noita.vue'
|
import Noita from '@/Noita.vue'
|
||||||
|
import { pinia } from '@/stores'
|
||||||
import '@/style.css'
|
import '@/style.css'
|
||||||
|
|
||||||
const selector = "#app"
|
const selector = "#app"
|
||||||
const mountData = document.querySelector<HTMLElement>(selector)
|
const mountData = document.querySelector<HTMLElement>(selector)
|
||||||
const app = createApp(Noita, { ...mountData?.dataset })
|
const app = createApp(Noita, { ...mountData?.dataset })
|
||||||
|
app.use(pinia)
|
||||||
app.mount(selector)
|
app.mount(selector)
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
|
Game,
|
||||||
SteamCollection,
|
SteamCollection,
|
||||||
SteamCollectionItem,
|
SteamCollectionItem,
|
||||||
Submission,
|
Submission,
|
||||||
@ -6,7 +7,9 @@ import type {
|
|||||||
SubmissionFile,
|
SubmissionFile,
|
||||||
UserInfo,
|
UserInfo,
|
||||||
TournamentSubmissions,
|
TournamentSubmissions,
|
||||||
TournamentPuzzleResults
|
TournamentPuzzleResults,
|
||||||
|
Market,
|
||||||
|
UserBet
|
||||||
} from '../types'
|
} from '../types'
|
||||||
|
|
||||||
// API Configuration
|
// API Configuration
|
||||||
@ -99,13 +102,18 @@ export class ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Games endpoint
|
||||||
|
async getGames(): Promise<ApiResponse<Game[]>> {
|
||||||
|
return this.request<Game[]>('/games/')
|
||||||
|
}
|
||||||
|
|
||||||
// Puzzle endpoints
|
// Puzzle endpoints
|
||||||
async getPuzzles(): Promise<ApiResponse<SteamCollectionItem[]>> {
|
async getPuzzles(): Promise<ApiResponse<SteamCollectionItem[]>> {
|
||||||
return this.request<SteamCollectionItem[]>('/submissions/puzzles')
|
return this.request<SteamCollectionItem[]>('/opus-magnum/puzzles')
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCollection(): Promise<ApiResponse<SteamCollection>> {
|
async getCollection(): Promise<ApiResponse<SteamCollection>> {
|
||||||
return this.request<SteamCollection>('/submissions/collection')
|
return this.request<SteamCollection>('/opus-magnum/collection')
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTopSubmissions(limit = 5): Promise<ApiResponse<TournamentSubmissions>> {
|
async getTopSubmissions(limit = 5): Promise<ApiResponse<TournamentSubmissions>> {
|
||||||
@ -119,12 +127,12 @@ export class ApiService {
|
|||||||
// Submission endpoints
|
// Submission endpoints
|
||||||
async getSubmissions(limit = 20, offset = 0): Promise<ApiResponse<PaginatedResponse<Submission>>> {
|
async getSubmissions(limit = 20, offset = 0): Promise<ApiResponse<PaginatedResponse<Submission>>> {
|
||||||
return this.request<PaginatedResponse<Submission>>(
|
return this.request<PaginatedResponse<Submission>>(
|
||||||
`/submissions/submissions?limit=${limit}&offset=${offset}`
|
`/opus-magnum/submissions?limit=${limit}&offset=${offset}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSubmission(id: string): Promise<ApiResponse<Submission>> {
|
async getSubmission(id: string): Promise<ApiResponse<Submission>> {
|
||||||
return this.request<Submission>(`/submissions/submissions/${id}`)
|
return this.request<Submission>(`/opus-magnum/submissions/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
async createSubmission(
|
async createSubmission(
|
||||||
@ -155,7 +163,7 @@ export class ApiService {
|
|||||||
formData.append('files', file)
|
formData.append('files', file)
|
||||||
})
|
})
|
||||||
|
|
||||||
return this.uploadRequest<Submission>('/submissions/submissions', formData)
|
return this.uploadRequest<Submission>('/opus-magnum/submissions', formData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Admin endpoints (require staff permissions)
|
// Admin endpoints (require staff permissions)
|
||||||
@ -167,37 +175,37 @@ export class ApiService {
|
|||||||
validated_area?: number
|
validated_area?: number
|
||||||
}
|
}
|
||||||
): Promise<ApiResponse<PuzzleResponse>> {
|
): Promise<ApiResponse<PuzzleResponse>> {
|
||||||
return this.request<PuzzleResponse>(`/submissions/responses/${responseId}/validate`, {
|
return this.request<PuzzleResponse>(`/opus-magnum/responses/${responseId}/validate`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify(validationData),
|
body: JSON.stringify(validationData),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async autoValidateResponses(responseId: number): Promise<ApiResponse<PuzzleResponse>> {
|
async autoValidateResponses(responseId: number): Promise<ApiResponse<PuzzleResponse>> {
|
||||||
return this.request<PuzzleResponse>(`/submissions/responses/${responseId}/validate/auto`, {
|
return this.request<PuzzleResponse>(`/opus-magnum/responses/${responseId}/validate/auto`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async getResponsesNeedingValidation(): Promise<ApiResponse<PuzzleResponse[]>> {
|
async getResponsesNeedingValidation(): Promise<ApiResponse<PuzzleResponse[]>> {
|
||||||
return this.request<PuzzleResponse[]>('/submissions/responses/needs-validation')
|
return this.request<PuzzleResponse[]>('/opus-magnum/responses/needs-validation')
|
||||||
}
|
}
|
||||||
|
|
||||||
async validateSubmission(submissionId: string): Promise<ApiResponse<Submission>> {
|
async validateSubmission(submissionId: string): Promise<ApiResponse<Submission>> {
|
||||||
return this.request<Submission>(`/submissions/submissions/${submissionId}/validate`, {
|
return this.request<Submission>(`/opus-magnum/submissions/${submissionId}/validate`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteSubmission(submissionId: string): Promise<ApiResponse<{ detail: string }>> {
|
async deleteSubmission(submissionId: string): Promise<ApiResponse<{ detail: string }>> {
|
||||||
return this.request<{ detail: string }>(`/submissions/submissions/${submissionId}`, {
|
return this.request<{ detail: string }>(`/opus-magnum/submissions/${submissionId}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Statistics endpoint
|
// Statistics endpoint
|
||||||
async getStats(): Promise<ApiResponse<SubmissionStats>> {
|
async getStats(): Promise<ApiResponse<SubmissionStats>> {
|
||||||
return this.request<SubmissionStats>('/submissions/stats')
|
return this.request<SubmissionStats>('/opus-magnum/stats')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Health check
|
// Health check
|
||||||
@ -209,6 +217,38 @@ export class ApiService {
|
|||||||
async getUserInfo(): Promise<ApiResponse<UserInfo>> {
|
async getUserInfo(): Promise<ApiResponse<UserInfo>> {
|
||||||
return this.request<UserInfo>('/user')
|
return this.request<UserInfo>('/user')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Market endpoints
|
||||||
|
async getMarkets(): Promise<ApiResponse<Market[]>> {
|
||||||
|
return this.request<Market[]>('/market/')
|
||||||
|
}
|
||||||
|
|
||||||
|
async placeBet(marketUuid: string, betData: {
|
||||||
|
option_uuid: string
|
||||||
|
amount: number
|
||||||
|
}): Promise<ApiResponse<UserBet>> {
|
||||||
|
return this.request<UserBet>(`/market/${marketUuid}/bets`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(betData),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserBets(): Promise<ApiResponse<UserBet[]>> {
|
||||||
|
return this.request<UserBet[]>('/market/user/bets')
|
||||||
|
}
|
||||||
|
|
||||||
|
async closeMarket(marketUuid: string): Promise<ApiResponse<{ status: string }>> {
|
||||||
|
return this.request<{ status: string }>(`/market/${marketUuid}/actions/close`, {
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async resolveMarket(marketUuid: string, winningOptionUuid: string): Promise<ApiResponse<Market>> {
|
||||||
|
return this.request<Market>(`/market/${marketUuid}/actions/resolve`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ winning_option_uuid: winningOptionUuid }),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Singleton instance
|
// Singleton instance
|
||||||
|
|||||||
94
polylan_submitter/src/stores/market.ts
Normal file
94
polylan_submitter/src/stores/market.ts
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { polylanSubmitterApiGetUserInfo, marketApiListMarkets, marketApiListUserBets } from '@/api'
|
||||||
|
import type { Market } from '@/types'
|
||||||
|
import type { UserInfoOut, UserBetSchema } from '@/api/types.gen'
|
||||||
|
|
||||||
|
export const useMarketStore = defineStore('market', () => {
|
||||||
|
// State
|
||||||
|
const markets = ref<Market[]>([])
|
||||||
|
const userInfo = ref<UserInfoOut | undefined>()
|
||||||
|
const userBets = ref<UserBetSchema[]>([])
|
||||||
|
const isLoading = ref(true)
|
||||||
|
const error = ref<string>('')
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadMarkets = async () => {
|
||||||
|
try {
|
||||||
|
const response = await marketApiListMarkets()
|
||||||
|
if (response.data) {
|
||||||
|
markets.value = response.data as unknown as Market[]
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error.value = 'Failed to load markets'
|
||||||
|
console.error('Error loading markets:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadUserBets = async () => {
|
||||||
|
try {
|
||||||
|
const response = await marketApiListUserBets()
|
||||||
|
if (response.data) {
|
||||||
|
userBets.value = response.data
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error.value = 'Failed to load user bets'
|
||||||
|
console.error('Error loading user bets:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const initializeMarketPage = async () => {
|
||||||
|
isLoading.value = true
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
loadUserInfo(),
|
||||||
|
loadMarkets(),
|
||||||
|
])
|
||||||
|
|
||||||
|
// Load user bets if authenticated
|
||||||
|
if (userInfo.value?.is_authenticated) {
|
||||||
|
await loadUserBets()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshPage = async () => {
|
||||||
|
await Promise.all([
|
||||||
|
loadUserInfo(),
|
||||||
|
loadMarkets(),
|
||||||
|
userInfo.value?.is_authenticated ? loadUserBets() : Promise.resolve(),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
markets,
|
||||||
|
userInfo,
|
||||||
|
userBets,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
loadUserInfo,
|
||||||
|
loadMarkets,
|
||||||
|
loadUserBets,
|
||||||
|
initializeMarketPage,
|
||||||
|
refreshPage,
|
||||||
|
}
|
||||||
|
})
|
||||||
167
polylan_submitter/src/stores/noita.ts
Normal file
167
polylan_submitter/src/stores/noita.ts
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import type { ObjectivResultOut, LeaderboardEntryOut } from '@/api/types.gen'
|
||||||
|
|
||||||
|
export type Objective = ObjectivResultOut
|
||||||
|
|
||||||
|
interface UserInfo {
|
||||||
|
username: string
|
||||||
|
rank: number | null
|
||||||
|
score: number
|
||||||
|
runsSubmitted: number
|
||||||
|
deathsCount: number
|
||||||
|
isStaff: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useNoitaStore = defineStore('noita', () => {
|
||||||
|
// State
|
||||||
|
const userInfo = ref<UserInfo>({
|
||||||
|
username: 'Player',
|
||||||
|
rank: null,
|
||||||
|
score: 0,
|
||||||
|
runsSubmitted: 0,
|
||||||
|
deathsCount: 0,
|
||||||
|
isStaff: false,
|
||||||
|
})
|
||||||
|
const objectives = ref<Objective[]>([])
|
||||||
|
const leaderboard = ref<LeaderboardEntryOut[]>([])
|
||||||
|
const isLoadingLeaderboard = ref(false)
|
||||||
|
const isUploading = ref(false)
|
||||||
|
const error = ref<string>('')
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
const fetchUserResults = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/noita/results')
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch results')
|
||||||
|
|
||||||
|
const results = await response.json()
|
||||||
|
userInfo.value.score = results.total_score
|
||||||
|
userInfo.value.deathsCount = results.deaths_count
|
||||||
|
userInfo.value.runsSubmitted = results.objectives.length
|
||||||
|
objectives.value = results.objectives
|
||||||
|
} catch (err) {
|
||||||
|
error.value = 'Failed to fetch user results'
|
||||||
|
console.error('Error fetching results:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchLeaderboard = async () => {
|
||||||
|
isLoadingLeaderboard.value = true
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/noita/leaderboard')
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch leaderboard')
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
leaderboard.value = data.leaderboard
|
||||||
|
|
||||||
|
// Find current user's rank
|
||||||
|
const userRank = leaderboard.value.find(
|
||||||
|
(entry) => entry.username === userInfo.value.username
|
||||||
|
)
|
||||||
|
|
||||||
|
if (userRank) {
|
||||||
|
userInfo.value.rank = userRank.rank
|
||||||
|
userInfo.value.score = userRank.total_score
|
||||||
|
userInfo.value.deathsCount = userRank.deaths_count
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error.value = 'Failed to fetch leaderboard'
|
||||||
|
console.error('Error fetching leaderboard:', err)
|
||||||
|
} finally {
|
||||||
|
isLoadingLeaderboard.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadUserData = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/user')
|
||||||
|
if (response.ok) {
|
||||||
|
const user = await response.json()
|
||||||
|
if (user.is_authenticated) {
|
||||||
|
userInfo.value.username = user.username
|
||||||
|
userInfo.value.isStaff = user.is_staff || false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching user info:', err)
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([fetchUserResults(), fetchLeaderboard()])
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitRun = async (files: File[]) => {
|
||||||
|
if (files.length === 0) return
|
||||||
|
|
||||||
|
isUploading.value = true
|
||||||
|
try {
|
||||||
|
for (const file of files) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
|
||||||
|
const response = await fetch('/api/noita/submit', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json()
|
||||||
|
throw new Error(errorData.detail || 'Unknown error')
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
console.log('Submission successful:', result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh objectives, score, and rank after successful submission
|
||||||
|
await Promise.all([fetchUserResults(), fetchLeaderboard()])
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Unknown error'
|
||||||
|
error.value = `Error submitting run: ${errorMessage}`
|
||||||
|
throw err
|
||||||
|
} finally {
|
||||||
|
isUploading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearCache = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/cache/clear', {
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json()
|
||||||
|
throw new Error(errorData.detail || 'Unknown error')
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([fetchUserResults(), fetchLeaderboard()])
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Unknown error'
|
||||||
|
error.value = `Error clearing cache: ${errorMessage}`
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshData = async () => {
|
||||||
|
await Promise.all([fetchUserResults(), fetchLeaderboard()])
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
userInfo,
|
||||||
|
objectives,
|
||||||
|
leaderboard,
|
||||||
|
isLoadingLeaderboard,
|
||||||
|
isUploading,
|
||||||
|
error,
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
fetchUserResults,
|
||||||
|
fetchLeaderboard,
|
||||||
|
loadUserData,
|
||||||
|
submitRun,
|
||||||
|
clearCache,
|
||||||
|
refreshData,
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
import { opusMagnumApiListPuzzles } from '@/api'
|
||||||
import type { SteamCollectionItem } from '@/types'
|
import type { SteamCollectionItem } from '@/types'
|
||||||
import { apiService } from '@/services/apiService'
|
|
||||||
|
|
||||||
export const usePuzzlesStore = defineStore('puzzles', () => {
|
export const usePuzzlesStore = defineStore('puzzles', () => {
|
||||||
// State
|
// State
|
||||||
@ -38,15 +38,15 @@ export const usePuzzlesStore = defineStore('puzzles', () => {
|
|||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
error.value = ''
|
error.value = ''
|
||||||
|
|
||||||
const response = await apiService.getPuzzles()
|
const response = await opusMagnumApiListPuzzles()
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
error.value = response.error
|
error.value = String(response.error)
|
||||||
console.error('Failed to load puzzles:', response.error)
|
console.error('Failed to load puzzles:', response.error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
puzzles.value = response.data
|
puzzles.value = response.data as unknown as SteamCollectionItem[]
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = 'Failed to load puzzles'
|
error.value = 'Failed to load puzzles'
|
||||||
|
|||||||
@ -1,3 +1,9 @@
|
|||||||
|
export interface Game {
|
||||||
|
steam_app_id: number
|
||||||
|
name: string
|
||||||
|
path: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface SteamCollection {
|
export interface SteamCollection {
|
||||||
id: number
|
id: number
|
||||||
steam_id: string
|
steam_id: string
|
||||||
@ -24,9 +30,9 @@ export interface SteamCollectionItem {
|
|||||||
title: string
|
title: string
|
||||||
author_name: string
|
author_name: string
|
||||||
description: string
|
description: string
|
||||||
tags: string[]
|
tags?: string[]
|
||||||
order_index: number
|
order_index?: number
|
||||||
collection: number
|
steam_url: string
|
||||||
points_factor?: PointsFactor
|
points_factor?: PointsFactor
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
@ -163,3 +169,30 @@ export interface PuzzleResults {
|
|||||||
export interface TournamentPuzzleResults {
|
export interface TournamentPuzzleResults {
|
||||||
results: PuzzleResults[]
|
results: PuzzleResults[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MarketOption {
|
||||||
|
uuid: string
|
||||||
|
text: string
|
||||||
|
total_bets: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Market {
|
||||||
|
uuid: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
status: 'draft' | 'open' | 'closed' | 'resolved'
|
||||||
|
end_date: string
|
||||||
|
multiplier: number
|
||||||
|
created_at: string
|
||||||
|
options: MarketOption[]
|
||||||
|
winning_option?: MarketOption | null
|
||||||
|
userHasBet?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserBet {
|
||||||
|
uuid: string
|
||||||
|
amount: number
|
||||||
|
created_at: string
|
||||||
|
option: MarketOption
|
||||||
|
market: Market
|
||||||
|
}
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
import{k as t,l as a,p as n,v as s}from"./style-C9QoPxDN.js";const c={key:0,class:"flex justify-center"},k={key:0,class:"badge badge-warning badge-lg"},d={key:1,class:"badge badge-lg"},l={key:2,class:"badge badge-lg"},o={key:3,class:"badge badge-lg"},g={key:1,class:"text-2xl text-base-content/50"},y=t({__name:"RankBadge",props:{rank:{}},setup(e){return(i,r)=>e.rank!==null?(n(),a("div",c,[e.rank===1?(n(),a("span",k," 🏆 #"+s(e.rank),1)):e.rank===2?(n(),a("span",d," 🥈 #"+s(e.rank),1)):e.rank===3?(n(),a("span",l," 🥉 #"+s(e.rank),1)):(n(),a("span",o," #"+s(e.rank),1))])):(n(),a("div",g," No rank yet "))}});export{y as _};
|
|
||||||
@ -0,0 +1 @@
|
|||||||
|
import{d as v,r as l,q as g,a as o,o as r,b as e,j as f,F as x,g as h,t as y,f as _,E as w}from"./style-BkYIZIDm.js";import{g as k}from"./sdk.gen-CA3PL0uK.js";const j={class:"min-h-screen bg-base-300 flex items-center justify-center px-4"},S={class:"w-full max-w-6xl"},E={key:0,class:"flex justify-center py-20"},C={key:1,class:"grid grid-cols-1 md:grid-cols-2 gap-8"},N=["onClick"],$={class:"relative h-60 bg-base-300 overflow-hidden"},A=["src","alt","onError"],B={key:1,class:"w-full h-full bg-gradient-to-br from-blue-600 to-blue-400 flex items-center justify-center text-white"},L={class:"card-body"},P={class:"card-title text-2xl"},V=v({__name:"Home",setup(F){const i=l(),n=l(!0),d=l(new Set),u=s=>`https://cdn.akamai.steamstatic.com/steam/apps/${s}/header.jpg`,b=s=>{d.value.add(s)},c=s=>{window.location.href=s};return g(async()=>{const s=await k();s.data&&(i.value=s.data),n.value=!1}),(s,t)=>(r(),o("div",j,[e("div",S,[t[6]||(t[6]=e("div",{class:"text-center mb-12"},[e("h1",{class:"text-5xl font-bold mb-4"},"PolyLAN Submitter"),e("p",{class:"text-xl text-base-content/70"}," Choose a game and submit your best solutions ")],-1)),n.value?(r(),o("div",E,[...t[1]||(t[1]=[e("span",{class:"loading loading-spinner loading-lg"},null,-1)])])):(r(),o("div",C,[e("div",{onClick:t[0]||(t[0]=a=>c("/market")),class:"card card-xl bg-base-200 shadow-xl hover:shadow-2xl transition-all cursor-pointer transform hover:-translate-y-2 hover:scale-[1.05] hover:bg-base-100 overflow-hidden"},[...t[2]||(t[2]=[f('<figure class="relative h-60 bg-gradient-to-br from-purple-600 to-blue-600 flex items-center justify-center"><i class="mdi mdi-chart-box text-6xl text-white opacity-80"></i><div class="absolute inset-0 bg-black/30 group-hover:bg-black/20 transition-colors"></div></figure><div class="card-body"><h2 class="card-title text-2xl">Market</h2><p class="text-base-content/70">Place your bets and compete</p><div class="card-actions justify-end mt-4"><button class="btn btn-primary"><i class="mdi mdi-arrow-right mr-2"></i> Place bets </button></div></div>',2)])]),(r(!0),o(x,null,h(i.value,a=>(r(),o("div",{key:a.steam_app_id,onClick:p=>c(a.path),class:"card card-xl bg-base-200 shadow-xl hover:shadow-2xl transition-all cursor-pointer transform hover:-translate-y-2 hover:scale-[1.05] hover:bg-base-100 overflow-hidden"},[e("figure",$,[d.value.has(a.steam_app_id)?(r(),o("div",B,[...t[3]||(t[3]=[e("i",{class:"mdi mdi-gamepad-variant text-5xl"},null,-1)])])):(r(),o("img",{key:0,src:u(a.steam_app_id),alt:a.name,onError:p=>b(a.steam_app_id),class:"w-full h-full object-cover"},null,40,A)),t[4]||(t[4]=e("div",{class:"absolute inset-0 bg-black/30 group-hover:bg-black/20 transition-colors"},null,-1))]),e("div",L,[e("h2",P,y(a.name),1),t[5]||(t[5]=e("div",{class:"card-actions justify-end mt-4"},[e("button",{class:"btn btn-primary"},[e("i",{class:"mdi mdi-arrow-right mr-2"}),_(" Submit results ")])],-1))])],8,N))),128))])),t[7]||(t[7]=e("div",{class:"text-center mt-12 text-base-content/50"},[e("p",null,"Select a game above to begin submitting")],-1))])]))}}),m="#app",q=document.querySelector(m),D=w(V,{...q?.dataset});D.mount(m);
|
||||||
@ -1 +0,0 @@
|
|||||||
import{k as b,c as v,r as g,l as a,p as n,s as t,F as h,y as x,v as i,x as f,O as _}from"./style-C9QoPxDN.js";const y={class:"min-h-screen bg-base-300 flex items-center justify-center px-4"},w={class:"w-full max-w-6xl"},k={class:"grid grid-cols-1 md:grid-cols-2 gap-8"},S=["onClick"],I={class:"relative h-60 bg-base-300 overflow-hidden"},j=["src","alt","onError"],E={key:1,class:"w-full h-full bg-gradient-to-br from-blue-600 to-blue-400 flex items-center justify-center text-white"},N={class:"card-body"},C={class:"card-title text-2xl"},B={class:"text-base-content/70"},O=b({__name:"Home",setup(A){const c=v(()=>[{id:"opus-magnum",title:"Opus Magnum",description:"Submit your best Opus Magnum puzzle solutions",appId:558990,path:"/opus-magnum"},{id:"noita",title:"Noita",description:"Submit your greatest Noita achievements",appId:881100,path:"/noita"}]),r=g(new Set),d=o=>`https://cdn.akamai.steamstatic.com/steam/apps/${o}/header.jpg`,u=o=>{r.value.add(o)},p=o=>{window.location.href=o};return(o,e)=>(n(),a("div",y,[t("div",w,[e[3]||(e[3]=t("div",{class:"text-center mb-12"},[t("h1",{class:"text-5xl font-bold mb-4"},"PolyLAN Submitter"),t("p",{class:"text-xl text-base-content/70"}," Choose a game and submit your best solutions ")],-1)),t("div",k,[(n(!0),a(h,null,x(c.value,s=>(n(),a("div",{key:s.id,onClick:m=>p(s.path),class:"card card-xl bg-base-200 shadow-xl hover:shadow-2xl transition-all cursor-pointer transform hover:-translate-y-2 hover:scale-[1.05] hover:bg-base-100 overflow-hidden"},[t("figure",I,[r.value.has(s.appId)?(n(),a("div",E,[...e[0]||(e[0]=[t("i",{class:"mdi mdi-gamepad-variant text-5xl"},null,-1)])])):(n(),a("img",{key:0,src:d(s.appId),alt:s.title,onError:m=>u(s.appId),class:"w-full h-full object-cover"},null,40,j)),e[1]||(e[1]=t("div",{class:"absolute inset-0 bg-black/30 group-hover:bg-black/20 transition-colors"},null,-1))]),t("div",N,[t("h2",C,i(s.title),1),t("p",B,i(s.description),1),e[2]||(e[2]=t("div",{class:"card-actions justify-end mt-4"},[t("button",{class:"btn btn-primary"},[t("i",{class:"mdi mdi-arrow-right mr-2"}),f(" Submit results ")])],-1))])],8,S))),128))]),e[4]||(e[4]=t("div",{class:"text-center mt-12 text-base-content/50"},[t("p",null,"Select a game above to begin submitting")],-1))])]))}}),l="#app",$=document.querySelector(l),z=_(O,{...$?.dataset});z.mount(l);
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user