Compare commits
No commits in common. "47812ffd09ead401a35ce0bf2a4b5727d91fbbd7" and "9f94fb39749cd18c3d61ae756dfd77a9dabddbf4" have entirely different histories.
47812ffd09
...
9f94fb3974
@ -34,6 +34,3 @@ 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
|
|
||||||
|
|||||||
@ -1,17 +0,0 @@
|
|||||||
# 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,9 +14,6 @@ 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 opus_magnum.models import PuzzleResponse, SteamCollectionItem, SteamCollection
|
from submissions.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 opus_magnum.models import PuzzleResponse
|
from submissions.models import PuzzleResponse
|
||||||
from opus_magnum.schemas import SteamCollectionItemOut
|
from submissions.schemas import SteamCollectionItemOut
|
||||||
|
|
||||||
|
|
||||||
class PuzzleResponseRankingOut(ModelSchema):
|
class PuzzleResponseRankingOut(ModelSchema):
|
||||||
|
|||||||
@ -1,11 +0,0 @@
|
|||||||
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"]
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
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 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class GamesConfig(AppConfig):
|
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
|
||||||
name = "games"
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
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"],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
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),
|
|
||||||
]
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
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),
|
|
||||||
]
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
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})"
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
from ninja import Schema
|
|
||||||
|
|
||||||
|
|
||||||
class GameOut(Schema):
|
|
||||||
steam_app_id: int
|
|
||||||
name: str
|
|
||||||
path: str
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1,184 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1,187 +0,0 @@
|
|||||||
# 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"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,73 +0,0 @@
|
|||||||
# 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",
|
|
||||||
)
|
|
||||||
],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
# 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",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
# 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"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
# 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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
# 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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,114 +0,0 @@
|
|||||||
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}"
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
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,22 +12,18 @@ 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)
|
||||||
@ -131,7 +127,6 @@ 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.
|
||||||
@ -237,7 +232,6 @@ 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,6 +72,7 @@ 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,
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
import { defineConfig } from '@hey-api/openapi-ts';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
input: `http://localhost:7777/api/openapi.json`,
|
|
||||||
output: 'src/api/',
|
|
||||||
});
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class OpusMagnumConfig(AppConfig):
|
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
|
||||||
name = "opus_magnum"
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
# 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),
|
|
||||||
]
|
|
||||||
@ -1 +0,0 @@
|
|||||||
# Create your tests here.
|
|
||||||
@ -7,8 +7,7 @@
|
|||||||
"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",
|
||||||
@ -22,7 +21,6 @@
|
|||||||
"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,9 +36,6 @@ 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
|
||||||
@ -239,31 +236,6 @@ 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==}
|
||||||
|
|
||||||
@ -280,13 +252,6 @@ 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==}
|
||||||
|
|
||||||
@ -506,9 +471,6 @@ 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==}
|
||||||
|
|
||||||
@ -604,54 +566,16 @@ 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==}
|
||||||
|
|
||||||
@ -661,32 +585,10 @@ 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'}
|
||||||
@ -703,9 +605,6 @@ 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'}
|
||||||
@ -720,13 +619,6 @@ 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==}
|
||||||
|
|
||||||
@ -740,23 +632,9 @@ 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==}
|
||||||
|
|
||||||
@ -764,21 +642,10 @@ 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'}
|
||||||
@ -872,13 +739,6 @@ 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
|
||||||
@ -886,19 +746,9 @@ 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==}
|
||||||
|
|
||||||
@ -915,30 +765,13 @@ 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==}
|
||||||
|
|
||||||
@ -947,23 +780,6 @@ 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'}
|
||||||
@ -1070,15 +886,6 @@ 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'}
|
||||||
@ -1180,56 +987,6 @@ 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
|
||||||
@ -1249,10 +1006,6 @@ 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': {}
|
||||||
@ -1400,8 +1153,6 @@ 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
|
||||||
@ -1530,76 +1281,22 @@ 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
|
||||||
@ -1638,8 +1335,6 @@ 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
|
||||||
@ -1647,12 +1342,6 @@ 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: {}
|
||||||
@ -1661,32 +1350,14 @@ 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
|
||||||
|
|
||||||
@ -1750,29 +1421,12 @@ 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: {}
|
||||||
@ -1784,31 +1438,14 @@ 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:
|
||||||
@ -1839,16 +1476,6 @@ 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: {}
|
||||||
@ -1931,15 +1558,6 @@ 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,13 +1,10 @@
|
|||||||
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 opus_magnum.api import router as submissions_router
|
from submissions.api import router as submissions_router
|
||||||
from opus_magnum.schemas import UserInfoOut
|
from submissions.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(
|
||||||
@ -33,14 +30,10 @@ 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 opus_magnum router
|
# Include the submissions router
|
||||||
api.add_router("/opus-magnum/", submissions_router, tags=["opus-magnum"])
|
api.add_router("/submissions/", submissions_router, tags=["submissions"])
|
||||||
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
|
||||||
@ -70,8 +63,18 @@ def get_user_info(request):
|
|||||||
user = request.user
|
user = request.user
|
||||||
|
|
||||||
if user.is_authenticated:
|
if user.is_authenticated:
|
||||||
return user
|
return {
|
||||||
|
"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,10 +41,8 @@ INSTALLED_APPS = [
|
|||||||
"django_vite",
|
"django_vite",
|
||||||
"accounts",
|
"accounts",
|
||||||
"animations",
|
"animations",
|
||||||
"opus_magnum",
|
"submissions",
|
||||||
"noita",
|
"noita",
|
||||||
"games",
|
|
||||||
"market",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
@ -169,9 +167,6 @@ 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",
|
||||||
@ -198,7 +193,7 @@ STATICFILES_DIRS = [
|
|||||||
from polylan_submitter.settingsLocal import * # noqa
|
from polylan_submitter.settingsLocal import * # noqa
|
||||||
|
|
||||||
|
|
||||||
import sentry_sdk # noqa
|
import sentry_sdk
|
||||||
|
|
||||||
sentry_sdk.init(
|
sentry_sdk.init(
|
||||||
dsn="https://cc62a4ce3f3470890b43accf02cc6d8c@sentry2.polylan.ch/12",
|
dsn="https://cc62a4ce3f3470890b43accf02cc6d8c@sentry2.polylan.ch/12",
|
||||||
|
|||||||
@ -23,31 +23,17 @@ 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):
|
||||||
from django.conf import settings
|
return render(request, "home.html", {})
|
||||||
|
|
||||||
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 opus_magnum.models import SteamCollection
|
from submissions.models import SteamCollection
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
@ -59,7 +45,6 @@ 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", {})
|
||||||
|
|
||||||
@ -69,7 +54,6 @@ 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,18 +1,23 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import { gamesApiListGames } from "./api";
|
|
||||||
import type { GamesApiListGamesResponse } from "./api/types.gen";
|
|
||||||
|
|
||||||
interface Props {
|
const games = computed(() => [
|
||||||
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) => {
|
||||||
@ -26,22 +31,6 @@ 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>
|
||||||
@ -55,38 +44,13 @@ onMounted(async () => {
|
|||||||
</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 v-else class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
<!-- Market Card -->
|
<div v-for="game in games" :key="game.id" @click="navigate(game.path)"
|
||||||
<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.steam_app_id)" :src="getHeaderImage(game.steam_app_id)" :alt="game.name"
|
<img v-if="!imageErrors.has(game.appId)" :src="getHeaderImage(game.appId)" :alt="game.title"
|
||||||
@error="onImageError(game.steam_app_id)" class="w-full h-full object-cover" />
|
@error="onImageError(game.appId)" 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>
|
||||||
@ -94,7 +58,8 @@ onMounted(async () => {
|
|||||||
<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.name }}</h2>
|
<h2 class="card-title text-2xl">{{ game.title }}</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>
|
||||||
@ -110,5 +75,7 @@ onMounted(async () => {
|
|||||||
<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>
|
||||||
|
|||||||
@ -1,106 +0,0 @@
|
|||||||
<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,9 +1,7 @@
|
|||||||
<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,
|
||||||
@ -14,11 +12,32 @@ import {
|
|||||||
type SortingState,
|
type SortingState,
|
||||||
} from "@tanstack/vue-table";
|
} from "@tanstack/vue-table";
|
||||||
|
|
||||||
const noitaStore = useNoitaStore();
|
interface Objective {
|
||||||
const { userInfo, objectives, leaderboard, isLoadingLeaderboard, isUploading } = storeToRefs(noitaStore);
|
objectiv_id: string;
|
||||||
|
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>([]);
|
||||||
@ -134,13 +153,40 @@ 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 {
|
||||||
await noitaStore.submitRun(uploadedFiles.value);
|
for (const file of 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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -148,18 +194,94 @@ 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 {
|
||||||
await noitaStore.clearCache();
|
const response = await fetch("/api/cache/clear", {
|
||||||
|
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(() => {
|
||||||
noitaStore.loadUserData();
|
loadUserData();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -7,8 +7,7 @@ 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 { polylanSubmitterApiGetUserInfo, opusMagnumApiGetCollection } from "@/api";
|
import { apiService, errorHelpers } from "@/services/apiService";
|
||||||
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";
|
||||||
@ -67,9 +66,9 @@ async function initialize() {
|
|||||||
|
|
||||||
// Load user info
|
// Load user info
|
||||||
console.log("Loading user info...");
|
console.log("Loading user info...");
|
||||||
const userResponse = await polylanSubmitterApiGetUserInfo();
|
const userResponse = await apiService.getUserInfo();
|
||||||
if (userResponse.data) {
|
if (userResponse.data) {
|
||||||
userInfo.value = userResponse.data as UserInfo;
|
userInfo.value = userResponse.data;
|
||||||
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);
|
||||||
@ -77,9 +76,9 @@ async function initialize() {
|
|||||||
|
|
||||||
// Load collection data
|
// Load collection data
|
||||||
console.log("Loading collection...");
|
console.log("Loading collection...");
|
||||||
const collectionResponse = await opusMagnumApiGetCollection();
|
const collectionResponse = await apiService.getCollection();
|
||||||
if (collectionResponse.data) {
|
if (collectionResponse.data) {
|
||||||
collection.value = collectionResponse.data as SteamCollection;
|
collection.value = collectionResponse.data;
|
||||||
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);
|
||||||
|
|||||||
@ -1,16 +0,0 @@
|
|||||||
// 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>());
|
|
||||||
@ -1,277 +0,0 @@
|
|||||||
// 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;
|
|
||||||
};
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
// 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';
|
|
||||||
@ -1,217 +0,0 @@
|
|||||||
// 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'>);
|
|
||||||
@ -1,316 +0,0 @@
|
|||||||
// 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,
|
|
||||||
});
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
// 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;
|
|
||||||
};
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
// 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();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,169 +0,0 @@
|
|||||||
// 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;
|
|
||||||
};
|
|
||||||
@ -1,171 +0,0 @@
|
|||||||
// 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;
|
|
||||||
};
|
|
||||||
@ -1,117 +0,0 @@
|
|||||||
// 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;
|
|
||||||
};
|
|
||||||
@ -1,242 +0,0 @@
|
|||||||
// 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 };
|
|
||||||
}
|
|
||||||
@ -1,104 +0,0 @@
|
|||||||
// 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];
|
|
||||||
};
|
|
||||||
@ -1,140 +0,0 @@
|
|||||||
// 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;
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
// 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';
|
|
||||||
@ -1,241 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,338 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,182 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
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,10 +1,8 @@
|
|||||||
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,5 +1,4 @@
|
|||||||
import type {
|
import type {
|
||||||
Game,
|
|
||||||
SteamCollection,
|
SteamCollection,
|
||||||
SteamCollectionItem,
|
SteamCollectionItem,
|
||||||
Submission,
|
Submission,
|
||||||
@ -7,9 +6,7 @@ import type {
|
|||||||
SubmissionFile,
|
SubmissionFile,
|
||||||
UserInfo,
|
UserInfo,
|
||||||
TournamentSubmissions,
|
TournamentSubmissions,
|
||||||
TournamentPuzzleResults,
|
TournamentPuzzleResults
|
||||||
Market,
|
|
||||||
UserBet
|
|
||||||
} from '../types'
|
} from '../types'
|
||||||
|
|
||||||
// API Configuration
|
// API Configuration
|
||||||
@ -102,18 +99,13 @@ 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[]>('/opus-magnum/puzzles')
|
return this.request<SteamCollectionItem[]>('/submissions/puzzles')
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCollection(): Promise<ApiResponse<SteamCollection>> {
|
async getCollection(): Promise<ApiResponse<SteamCollection>> {
|
||||||
return this.request<SteamCollection>('/opus-magnum/collection')
|
return this.request<SteamCollection>('/submissions/collection')
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTopSubmissions(limit = 5): Promise<ApiResponse<TournamentSubmissions>> {
|
async getTopSubmissions(limit = 5): Promise<ApiResponse<TournamentSubmissions>> {
|
||||||
@ -127,12 +119,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>>(
|
||||||
`/opus-magnum/submissions?limit=${limit}&offset=${offset}`
|
`/submissions/submissions?limit=${limit}&offset=${offset}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSubmission(id: string): Promise<ApiResponse<Submission>> {
|
async getSubmission(id: string): Promise<ApiResponse<Submission>> {
|
||||||
return this.request<Submission>(`/opus-magnum/submissions/${id}`)
|
return this.request<Submission>(`/submissions/submissions/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
async createSubmission(
|
async createSubmission(
|
||||||
@ -163,7 +155,7 @@ export class ApiService {
|
|||||||
formData.append('files', file)
|
formData.append('files', file)
|
||||||
})
|
})
|
||||||
|
|
||||||
return this.uploadRequest<Submission>('/opus-magnum/submissions', formData)
|
return this.uploadRequest<Submission>('/submissions/submissions', formData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Admin endpoints (require staff permissions)
|
// Admin endpoints (require staff permissions)
|
||||||
@ -175,37 +167,37 @@ export class ApiService {
|
|||||||
validated_area?: number
|
validated_area?: number
|
||||||
}
|
}
|
||||||
): Promise<ApiResponse<PuzzleResponse>> {
|
): Promise<ApiResponse<PuzzleResponse>> {
|
||||||
return this.request<PuzzleResponse>(`/opus-magnum/responses/${responseId}/validate`, {
|
return this.request<PuzzleResponse>(`/submissions/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>(`/opus-magnum/responses/${responseId}/validate/auto`, {
|
return this.request<PuzzleResponse>(`/submissions/responses/${responseId}/validate/auto`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async getResponsesNeedingValidation(): Promise<ApiResponse<PuzzleResponse[]>> {
|
async getResponsesNeedingValidation(): Promise<ApiResponse<PuzzleResponse[]>> {
|
||||||
return this.request<PuzzleResponse[]>('/opus-magnum/responses/needs-validation')
|
return this.request<PuzzleResponse[]>('/submissions/responses/needs-validation')
|
||||||
}
|
}
|
||||||
|
|
||||||
async validateSubmission(submissionId: string): Promise<ApiResponse<Submission>> {
|
async validateSubmission(submissionId: string): Promise<ApiResponse<Submission>> {
|
||||||
return this.request<Submission>(`/opus-magnum/submissions/${submissionId}/validate`, {
|
return this.request<Submission>(`/submissions/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 }>(`/opus-magnum/submissions/${submissionId}`, {
|
return this.request<{ detail: string }>(`/submissions/submissions/${submissionId}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Statistics endpoint
|
// Statistics endpoint
|
||||||
async getStats(): Promise<ApiResponse<SubmissionStats>> {
|
async getStats(): Promise<ApiResponse<SubmissionStats>> {
|
||||||
return this.request<SubmissionStats>('/opus-magnum/stats')
|
return this.request<SubmissionStats>('/submissions/stats')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Health check
|
// Health check
|
||||||
@ -217,38 +209,6 @@ 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
|
||||||
|
|||||||
@ -1,94 +0,0 @@
|
|||||||
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,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@ -1,167 +0,0 @@
|
|||||||
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 opusMagnumApiListPuzzles()
|
const response = await apiService.getPuzzles()
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
error.value = String(response.error)
|
error.value = 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 as unknown as SteamCollectionItem[]
|
puzzles.value = response.data
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = 'Failed to load puzzles'
|
error.value = 'Failed to load puzzles'
|
||||||
|
|||||||
@ -1,9 +1,3 @@
|
|||||||
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
|
||||||
@ -30,9 +24,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
|
||||||
steam_url: string
|
collection: number
|
||||||
points_factor?: PointsFactor
|
points_factor?: PointsFactor
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
@ -169,30 +163,3 @@ 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
|
|
||||||
}
|
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
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 _};
|
||||||
@ -1 +0,0 @@
|
|||||||
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);
|
|
||||||
@ -0,0 +1 @@
|
|||||||
|
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);
|
||||||
@ -1,5 +0,0 @@
|
|||||||
import{H as K,r as V,I as q,J as z,c as Q,y as p,K as F,L as st,M as nt,N as ot,O as ct,B as rt,C as at,w as ut,h as ft,P as lt,d as it,a as m,o as j,t as E}from"./style-BkYIZIDm.js";/*!
|
|
||||||
* pinia v3.0.3
|
|
||||||
* (c) 2025 Eduardo San Martin Morote
|
|
||||||
* @license MIT
|
|
||||||
*/let U;const I=t=>U=t,X=Symbol();function B(t){return t&&typeof t=="object"&&Object.prototype.toString.call(t)==="[object Object]"&&typeof t.toJSON!="function"}var R;(function(t){t.direct="direct",t.patchObject="patch object",t.patchFunction="patch function"})(R||(R={}));function ht(){const t=K(!0),n=t.run(()=>V({}));let s=[],e=[];const r=q({install(a){I(r),r._a=a,a.provide(X,r),a.config.globalProperties.$pinia=r,e.forEach(u=>s.push(u)),e=[]},use(a){return this._a?s.push(a):e.push(a),this},_p:s,_a:null,_e:t,_s:new Map,state:n});return r}const Y=()=>{};function D(t,n,s,e=Y){t.push(n);const r=()=>{const a=t.indexOf(n);a>-1&&(t.splice(a,1),e())};return!s&&rt()&&at(r),r}function x(t,...n){t.slice().forEach(s=>{s(...n)})}const bt=t=>t(),J=Symbol(),N=Symbol();function W(t,n){t instanceof Map&&n instanceof Map?n.forEach((s,e)=>t.set(e,s)):t instanceof Set&&n instanceof Set&&n.forEach(t.add,t);for(const s in n){if(!n.hasOwnProperty(s))continue;const e=n[s],r=t[s];B(r)&&B(e)&&t.hasOwnProperty(s)&&!p(e)&&!F(e)?t[s]=W(r,e):t[s]=e}return t}const yt=Symbol();function dt(t){return!B(t)||!Object.prototype.hasOwnProperty.call(t,yt)}const{assign:h}=Object;function St(t){return!!(p(t)&&t.effect)}function vt(t,n,s,e){const{state:r,actions:a,getters:u}=n,C=s.state.value[t];let y;function b(){C||(s.state.value[t]=r?r():{});const d=lt(s.state.value[t]);return h(d,a,Object.keys(u||{}).reduce((S,v)=>(S[v]=q(Q(()=>{I(s);const g=s._s.get(t);return u[v].call(g,g)})),S),{}))}return y=Z(t,b,n,s,e,!0),y}function Z(t,n,s={},e,r,a){let u;const C=h({actions:{}},s),y={deep:!0};let b,d,S=[],v=[],g;const k=e.state.value[t];!a&&!k&&(e.state.value[t]={}),V({});let H;function M(c){let o;b=d=!1,typeof c=="function"?(c(e.state.value[t]),o={type:R.patchFunction,storeId:t,events:g}):(W(e.state.value[t],c),o={type:R.patchObject,payload:c,storeId:t,events:g});const f=H=Symbol();ft().then(()=>{H===f&&(b=!0)}),d=!0,x(S,o,e.state.value[t])}const G=a?function(){const{state:o}=s,f=o?o():{};this.$patch(_=>{h(_,f)})}:Y;function $(){u.stop(),S=[],v=[],e._s.delete(t)}const A=(c,o="")=>{if(J in c)return c[N]=o,c;const f=function(){I(e);const _=Array.from(arguments),P=[],L=[];function tt(l){P.push(l)}function et(l){L.push(l)}x(v,{args:_,name:f[N],store:i,after:tt,onError:et});let w;try{w=c.apply(this&&this.$id===t?this:i,_)}catch(l){throw x(L,l),l}return w instanceof Promise?w.then(l=>(x(P,l),l)).catch(l=>(x(L,l),Promise.reject(l))):(x(P,w),w)};return f[J]=!0,f[N]=o,f},T={_p:e,$id:t,$onAction:D.bind(null,v),$patch:M,$reset:G,$subscribe(c,o={}){const f=D(S,c,o.detached,()=>_()),_=u.run(()=>ut(()=>e.state.value[t],P=>{(o.flush==="sync"?d:b)&&c({storeId:t,type:R.direct,events:g},P)},h({},y,o)));return f},$dispose:$},i=ct(T);e._s.set(t,i);const O=(e._a&&e._a.runWithContext||bt)(()=>e._e.run(()=>(u=K()).run(()=>n({action:A}))));for(const c in O){const o=O[c];if(p(o)&&!St(o)||F(o))a||(k&&dt(o)&&(p(o)?o.value=k[c]:W(o,k[c])),e.state.value[t][c]=o);else if(typeof o=="function"){const f=A(o,c);O[c]=f,C.actions[c]=o}}return h(i,O),h(z(i),O),Object.defineProperty(i,"$state",{get:()=>e.state.value[t],set:c=>{M(o=>{h(o,c)})}}),e._p.forEach(c=>{h(i,u.run(()=>c({store:i,app:e._a,pinia:e,options:C})))}),k&&a&&s.hydrate&&s.hydrate(i.$state,k),b=!0,d=!0,i}/*! #__NO_SIDE_EFFECTS__ */function Pt(t,n,s){let e;const r=typeof n=="function";e=r?s:n;function a(u,C){const y=ot();return u=u||(y?nt(X,null):null),u&&I(u),u=U,u._s.has(t)||(r?Z(t,n,e,u):vt(t,e,u)),u._s.get(t)}return a.$id=t,a}function wt(t){const n=z(t),s={};for(const e in n){const r=n[e];r.effect?s[e]=Q({get:()=>t[e],set(a){t[e]=a}}):(p(r)||F(r))&&(s[e]=st(t,e))}return s}const gt={key:0,class:"flex justify-center"},kt={key:0,class:"badge badge-warning badge-lg"},_t={key:1,class:"badge badge-lg"},mt={key:2,class:"badge badge-lg"},jt={key:3,class:"badge badge-lg"},xt={key:1,class:"text-2xl text-base-content/50"},Rt=it({__name:"RankBadge",props:{rank:{}},setup(t){return(n,s)=>t.rank!==null?(j(),m("div",gt,[t.rank===1?(j(),m("span",kt," 🏆 #"+E(t.rank),1)):t.rank===2?(j(),m("span",_t," 🥈 #"+E(t.rank),1)):t.rank===3?(j(),m("span",mt," 🥉 #"+E(t.rank),1)):(j(),m("span",jt," #"+E(t.rank),1))])):(j(),m("div",xt," No rank yet "))}}),pt=ht();export{Rt as _,Pt as d,pt as p,wt as s};
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,24 +1,16 @@
|
|||||||
{
|
{
|
||||||
"_index-C0ZY4EM6.js": {
|
"_RankBadge.vue_vue_type_script_setup_true_lang-CiUtgtLU.js": {
|
||||||
"file": "assets/index-C0ZY4EM6.js",
|
"file": "assets/RankBadge.vue_vue_type_script_setup_true_lang-CiUtgtLU.js",
|
||||||
"name": "index",
|
"name": "RankBadge.vue_vue_type_script_setup_true_lang",
|
||||||
"imports": [
|
"imports": [
|
||||||
"_style-BkYIZIDm.js"
|
"_style-C9QoPxDN.js"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"_sdk.gen-CA3PL0uK.js": {
|
"_style-C9QoPxDN.js": {
|
||||||
"file": "assets/sdk.gen-CA3PL0uK.js",
|
"file": "assets/style-C9QoPxDN.js",
|
||||||
"name": "sdk.gen"
|
|
||||||
},
|
|
||||||
"_style-B5FWYd7r.css": {
|
|
||||||
"file": "assets/style-B5FWYd7r.css",
|
|
||||||
"src": "_style-B5FWYd7r.css"
|
|
||||||
},
|
|
||||||
"_style-BkYIZIDm.js": {
|
|
||||||
"file": "assets/style-BkYIZIDm.js",
|
|
||||||
"name": "style",
|
"name": "style",
|
||||||
"css": [
|
"css": [
|
||||||
"assets/style-B5FWYd7r.css"
|
"assets/style-Cs9btLod.css"
|
||||||
],
|
],
|
||||||
"assets": [
|
"assets": [
|
||||||
"assets/materialdesignicons-webfont-CSr8KVlo.eot",
|
"assets/materialdesignicons-webfont-CSr8KVlo.eot",
|
||||||
@ -27,6 +19,10 @@
|
|||||||
"assets/materialdesignicons-webfont-B7mPwVP_.ttf"
|
"assets/materialdesignicons-webfont-B7mPwVP_.ttf"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"_style-Cs9btLod.css": {
|
||||||
|
"file": "assets/style-Cs9btLod.css",
|
||||||
|
"src": "_style-Cs9btLod.css"
|
||||||
|
},
|
||||||
"node_modules/.pnpm/@mdi+font@7.4.47/node_modules/@mdi/font/fonts/materialdesignicons-webfont.eot": {
|
"node_modules/.pnpm/@mdi+font@7.4.47/node_modules/@mdi/font/fonts/materialdesignicons-webfont.eot": {
|
||||||
"file": "assets/materialdesignicons-webfont-CSr8KVlo.eot",
|
"file": "assets/materialdesignicons-webfont-CSr8KVlo.eot",
|
||||||
"src": "node_modules/.pnpm/@mdi+font@7.4.47/node_modules/@mdi/font/fonts/materialdesignicons-webfont.eot"
|
"src": "node_modules/.pnpm/@mdi+font@7.4.47/node_modules/@mdi/font/fonts/materialdesignicons-webfont.eot"
|
||||||
@ -44,34 +40,32 @@
|
|||||||
"src": "node_modules/.pnpm/@mdi+font@7.4.47/node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff2"
|
"src": "node_modules/.pnpm/@mdi+font@7.4.47/node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff2"
|
||||||
},
|
},
|
||||||
"src/home.ts": {
|
"src/home.ts": {
|
||||||
"file": "assets/home-Bd8zSeuF.js",
|
"file": "assets/home-Cpe9mjX7.js",
|
||||||
"name": "home",
|
"name": "home",
|
||||||
"src": "src/home.ts",
|
"src": "src/home.ts",
|
||||||
"isEntry": true,
|
"isEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_style-BkYIZIDm.js",
|
"_style-C9QoPxDN.js"
|
||||||
"_sdk.gen-CA3PL0uK.js"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/noita.ts": {
|
"src/noita.ts": {
|
||||||
"file": "assets/noita-Cj1wLkUa.js",
|
"file": "assets/noita-C5wjrj1v.js",
|
||||||
"name": "noita",
|
"name": "noita",
|
||||||
"src": "src/noita.ts",
|
"src": "src/noita.ts",
|
||||||
"isEntry": true,
|
"isEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_style-BkYIZIDm.js",
|
"_style-C9QoPxDN.js",
|
||||||
"_index-C0ZY4EM6.js"
|
"_RankBadge.vue_vue_type_script_setup_true_lang-CiUtgtLU.js"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/opus-magnum.ts": {
|
"src/opus-magnum.ts": {
|
||||||
"file": "assets/opus_magnum-CX0R2B7z.js",
|
"file": "assets/opus_magnum-Ce7rjJAF.js",
|
||||||
"name": "opus_magnum",
|
"name": "opus_magnum",
|
||||||
"src": "src/opus-magnum.ts",
|
"src": "src/opus-magnum.ts",
|
||||||
"isEntry": true,
|
"isEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_style-BkYIZIDm.js",
|
"_style-C9QoPxDN.js",
|
||||||
"_index-C0ZY4EM6.js",
|
"_RankBadge.vue_vue_type_script_setup_true_lang-CiUtgtLU.js"
|
||||||
"_sdk.gen-CA3PL0uK.js"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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 opus_magnum.models import (
|
from submissions.models import (
|
||||||
SteamAPIKey,
|
SteamAPIKey,
|
||||||
SteamCollection,
|
SteamCollection,
|
||||||
SteamCollectionItem,
|
SteamCollectionItem,
|
||||||
@ -9,9 +9,7 @@ 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 games.decorators import require_game_enabled
|
from submissions.utils import verify_and_validate_ocr_date_for_submission
|
||||||
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,
|
||||||
@ -30,11 +28,9 @@ 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(
|
||||||
@ -43,7 +39,6 @@ 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)
|
||||||
@ -51,7 +46,6 @@ 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"""
|
||||||
@ -61,7 +55,6 @@ 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(
|
||||||
@ -73,7 +66,6 @@ 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(...)
|
||||||
):
|
):
|
||||||
@ -206,7 +198,6 @@ 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"""
|
||||||
|
|
||||||
@ -242,7 +233,6 @@ 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"""
|
||||||
|
|
||||||
@ -258,7 +248,6 @@ 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"""
|
||||||
|
|
||||||
@ -274,7 +263,6 @@ 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"""
|
||||||
|
|
||||||
@ -303,7 +291,6 @@ 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)"""
|
||||||
|
|
||||||
@ -320,7 +307,6 @@ 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"""
|
||||||
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class MarketConfig(AppConfig):
|
class SubmissionsConfig(AppConfig):
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
name = "market"
|
name = "submissions"
|
||||||
@ -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 opus_magnum.utils import create_or_update_collection
|
from submissions.utils import create_or_update_collection
|
||||||
from opus_magnum.models import SteamAPIKey, SteamCollection
|
from submissions.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 opus_magnum.utils import SteamCollectionFetcher
|
from submissions.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 opus_magnum.utils import verify_and_validate_ocr_date_for_submission
|
from submissions.utils import verify_and_validate_ocr_date_for_submission
|
||||||
from opus_magnum.models import SubmissionFile
|
from submissions.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="opus_magnum.steamcollection",
|
to="submissions.steamcollection",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -5,7 +5,7 @@ from django.db import migrations
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("opus_magnum", "0001_initial"),
|
("submissions", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@ -5,7 +5,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("opus_magnum", "0002_delete_collection"),
|
("submissions", "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 opus_magnum.models
|
import submissions.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 = [
|
||||||
("opus_magnum", "0003_steamapikey"),
|
("submissions", "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="opus_magnum.steamcollectionitem",
|
to="submissions.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="opus_magnum.submission",
|
to="submissions.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=opus_magnum.models.submission_file_upload_path,
|
upload_to=submissions.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="opus_magnum.puzzleresponse",
|
to="submissions.puzzleresponse",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -5,7 +5,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("opus_magnum", "0004_submission_puzzleresponse_submissionfile"),
|
("submissions", "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 = [
|
||||||
("opus_magnum", "0005_alter_submission_notes"),
|
("submissions", "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 = [
|
||||||
("opus_magnum", "0006_remove_puzzleresponse_ocr_confidence_score_and_more"),
|
("submissions", "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 = [
|
||||||
("opus_magnum", "0007_submission_manual_validation_requested"),
|
("submissions", "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"),
|
||||||
("opus_magnum", "0008_alter_puzzleresponse_unique_together"),
|
("submissions", "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 = [
|
||||||
("opus_magnum", "0009_steamcollectionitem_points_factor"),
|
("submissions", "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 = [
|
||||||
("opus_magnum", "0010_alter_puzzleresponse_validated_area_and_more"),
|
("submissions", "0010_alter_puzzleresponse_validated_area_and_more"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user