fix ranking order + display winning gifs

This commit is contained in:
Loïc Gremaud 2026-05-22 06:32:18 +02:00
parent 92dddca964
commit 4ba6a48246
Signed by: Legrems
GPG Key ID: D4620E6DF3E0121D
13 changed files with 56 additions and 127 deletions

View File

@ -8,10 +8,8 @@ from django.shortcuts import get_object_or_404
from accounts.models import CustomUser from accounts.models import CustomUser
from animations.schemas import ( from animations.schemas import (
RankingSchema, RankingSchema,
TournamentWinnersOut,
TournamentSubmissionsOut, TournamentSubmissionsOut,
TournamentPuzzleResultsOut, TournamentPuzzleResultsOut,
PuzzleWinnerOut,
PuzzleSubmissionsOut, PuzzleSubmissionsOut,
PuzzleResultsOut, PuzzleResultsOut,
PuzzleSubmissionWithRankOut, PuzzleSubmissionWithRankOut,
@ -62,72 +60,6 @@ def results(request: HttpRequest) -> dict:
return data return data
@router.get("winners", response=TournamentWinnersOut)
def winners(request: HttpRequest) -> TournamentWinnersOut:
"""Get tournament winners with submission file URLs. Only available when tournament is closed."""
collection = get_object_or_404(SteamCollection, is_active=True)
# Only allow access when tournament is closed
if collection.accepting_submissions:
raise HttpError(403, "Tournament is still accepting submissions")
# Get all puzzles
puzzles = SteamCollectionItem.objects.filter(collection=collection).order_by("order_index")
# Get best response for each puzzle
winners_by_puzzle = {}
for puzzle in puzzles:
# Get the best response for this puzzle (ranked by points)
best_response = (
PuzzleResponse.objects
.filter(puzzle=puzzle, needs_manual_validation=False)
.filter_user_best_response()
.annotate_rank_points()
.order_by("rank_points")
.first()
)
if best_response:
winners_by_puzzle[puzzle.id] = best_response
# Build response
winners_list = []
for puzzle in puzzles:
best_response = winners_by_puzzle.get(puzzle.id)
winner_data = None
if best_response:
# Get submission files
files = best_response.files.all()
winner_files = [
WinnerFileOut(
file_url=file.file_url or "",
original_filename=file.original_filename
)
for file in files
]
winner_data = WinnerResponseOut(
user_id=best_response.submission.user.id if best_response.submission.user else 0,
username=best_response.submission.user.username if best_response.submission.user else "Anonymous",
final_cost=best_response.final_cost,
final_cycles=best_response.final_cycles,
final_area=best_response.final_area,
rank_points=best_response.rank_points,
files=winner_files,
)
winners_list.append(
PuzzleWinnerOut(
puzzle_id=puzzle.id,
puzzle_title=puzzle.title,
winner=winner_data,
)
)
return TournamentWinnersOut(winners=winners_list)
@router.get("top-submissions", response=TournamentSubmissionsOut) @router.get("top-submissions", response=TournamentSubmissionsOut)
def top_submissions(request: HttpRequest, limit: int = 5) -> TournamentSubmissionsOut: def top_submissions(request: HttpRequest, limit: int = 5) -> TournamentSubmissionsOut:
"""Get tournament top submissions for each puzzle. Only available when tournament is closed.""" """Get tournament top submissions for each puzzle. Only available when tournament is closed."""
@ -143,13 +75,13 @@ def top_submissions(request: HttpRequest, limit: int = 5) -> TournamentSubmissio
# Build response # Build response
submissions_list = [] submissions_list = []
for puzzle in puzzles: for puzzle in puzzles:
# Get the top N responses for this puzzle (ranked by points) # Get the top N responses for this puzzle (ranked by points, highest first)
top_responses = ( top_responses = (
PuzzleResponse.objects PuzzleResponse.objects
.filter(puzzle=puzzle, needs_manual_validation=False) .filter(puzzle=puzzle, needs_manual_validation=False)
.filter_user_best_response() .filter_user_best_response()
.annotate_rank_points() .annotate_rank_points()
.order_by("rank_points")[:limit] .order_by("-rank_points")[:limit]
) )
# Build submission list for this puzzle # Build submission list for this puzzle
@ -218,7 +150,7 @@ def puzzle_results(request: HttpRequest, limit: int = 5) -> TournamentPuzzleResu
.filter(puzzle=puzzle, needs_manual_validation=False) .filter(puzzle=puzzle, needs_manual_validation=False)
.filter_user_best_response() .filter_user_best_response()
.annotate_rank_points() .annotate_rank_points()
.order_by("rank_points")[:limit] .order_by("-rank_points")[:limit]
) )
# Build submission list for this puzzle with rank # Build submission list for this puzzle with rank

View File

@ -64,14 +64,6 @@ class WinnerResponseOut(Schema):
files: List[WinnerFileOut] files: List[WinnerFileOut]
class PuzzleWinnerOut(Schema):
"""Schema for puzzle with winner info"""
puzzle_id: int
puzzle_title: str
winner: Optional[WinnerResponseOut]
class PuzzleSubmissionsOut(Schema): class PuzzleSubmissionsOut(Schema):
"""Schema for puzzle with all top submissions""" """Schema for puzzle with all top submissions"""
@ -80,12 +72,6 @@ class PuzzleSubmissionsOut(Schema):
submissions: List[WinnerResponseOut] submissions: List[WinnerResponseOut]
class TournamentWinnersOut(Schema):
"""Schema for tournament winners results"""
winners: List[PuzzleWinnerOut]
class TournamentSubmissionsOut(Schema): class TournamentSubmissionsOut(Schema):
"""Schema for tournament top submissions results""" """Schema for tournament top submissions results"""

View File

@ -52,6 +52,12 @@ const getRankBadge = (rank: number) => {
return badges[rank - 1] || `#${rank}`; return badges[rank - 1] || `#${rank}`;
}; };
const getWinnersForPuzzle = (puzzle: any) => {
if (!puzzle.submissions || puzzle.submissions.length === 0) return [];
const topPoints = puzzle.submissions[0].rank_points;
return puzzle.submissions.filter((submission: any) => submission.rank_points === topPoints);
};
onMounted(() => { onMounted(() => {
fetchResults(); fetchResults();
}); });
@ -99,6 +105,26 @@ onMounted(() => {
</div> </div>
</div> </div>
</div> </div>
<!-- Winners GIF Display (all with same top points) -->
<div v-if="puzzle.submissions.length > 0 && getWinnersForPuzzle(puzzle).length > 0"
class="bg-base-200 p-4 rounded-lg">
<p class="text-xs text-base-content/70 font-semibold mb-3 text-center">🏆 Winning Solutions</p>
<div class="flex flex-wrap justify-center gap-6">
<template v-for="submission in getWinnersForPuzzle(puzzle)"
:key="`${puzzle.puzzle_id}-${submission.user_id}`">
<div v-if="submission && submission.files && submission.files.length > 0" class="text-center w-96">
<div class="text-sm text-base-content/70 mb-3 font-semibold truncate">{{ submission.username }}
</div>
<button @click="openImageModal(submission.files[0].file_url, submission.files[0].original_filename)"
class="hover:opacity-80 transition-opacity cursor-pointer w-full flex items-center justify-center">
<img :src="submission.files[0].file_url" :alt="`${puzzle.puzzle_title} - ${submission.username}`"
class="max-h-full max-w-full object-contain" />
</button>
</div>
</template>
</div>
</div>
</div> </div>
<!-- Results Table --> <!-- Results Table -->

View File

@ -5,7 +5,6 @@ import type {
PuzzleResponse, PuzzleResponse,
SubmissionFile, SubmissionFile,
UserInfo, UserInfo,
TournamentWinners,
TournamentSubmissions, TournamentSubmissions,
TournamentPuzzleResults TournamentPuzzleResults
} from '../types' } from '../types'
@ -109,10 +108,6 @@ export class ApiService {
return this.request<SteamCollection>('/submissions/collection') return this.request<SteamCollection>('/submissions/collection')
} }
async getWinners(): Promise<ApiResponse<TournamentWinners>> {
return this.request<TournamentWinners>('/results/winners')
}
async getTopSubmissions(limit = 5): Promise<ApiResponse<TournamentSubmissions>> { async getTopSubmissions(limit = 5): Promise<ApiResponse<TournamentSubmissions>> {
return this.request<TournamentSubmissions>(`/results/top-submissions?limit=${limit}`) return this.request<TournamentSubmissions>(`/results/top-submissions?limit=${limit}`)
} }

View File

@ -125,16 +125,6 @@ export interface WinnerResponse {
files: WinnerFile[] files: WinnerFile[]
} }
export interface PuzzleWinner {
puzzle_id: number
puzzle_title: string
winner?: WinnerResponse
}
export interface TournamentWinners {
winners: PuzzleWinner[]
}
export interface PuzzleSubmissions { export interface PuzzleSubmissions {
puzzle_id: number puzzle_id: number
puzzle_title: string puzzle_title: string

View File

@ -1 +1 @@
import{k as t,l as a,p as n,v as s}from"./style-C433w8gz.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 _}; import{k as t,l as a,p as n,v as s}from"./style-BKSucaDP.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 _};

View File

@ -1 +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-C433w8gz.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); 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-BKSucaDP.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);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,16 +1,20 @@
{ {
"_RankBadge.vue_vue_type_script_setup_true_lang-IllWPCyW.js": { "_RankBadge.vue_vue_type_script_setup_true_lang-c__WwHxT.js": {
"file": "assets/RankBadge.vue_vue_type_script_setup_true_lang-IllWPCyW.js", "file": "assets/RankBadge.vue_vue_type_script_setup_true_lang-c__WwHxT.js",
"name": "RankBadge.vue_vue_type_script_setup_true_lang", "name": "RankBadge.vue_vue_type_script_setup_true_lang",
"imports": [ "imports": [
"_style-C433w8gz.js" "_style-BKSucaDP.js"
] ]
}, },
"_style-C433w8gz.js": { "_style-8zNjBvNQ.css": {
"file": "assets/style-C433w8gz.js", "file": "assets/style-8zNjBvNQ.css",
"src": "_style-8zNjBvNQ.css"
},
"_style-BKSucaDP.js": {
"file": "assets/style-BKSucaDP.js",
"name": "style", "name": "style",
"css": [ "css": [
"assets/style-CMHHCLN4.css" "assets/style-8zNjBvNQ.css"
], ],
"assets": [ "assets": [
"assets/materialdesignicons-webfont-CSr8KVlo.eot", "assets/materialdesignicons-webfont-CSr8KVlo.eot",
@ -19,10 +23,6 @@
"assets/materialdesignicons-webfont-B7mPwVP_.ttf" "assets/materialdesignicons-webfont-B7mPwVP_.ttf"
] ]
}, },
"_style-CMHHCLN4.css": {
"file": "assets/style-CMHHCLN4.css",
"src": "_style-CMHHCLN4.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"
@ -40,32 +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-f2CGvY1q.js", "file": "assets/home-YbwRQP1Y.js",
"name": "home", "name": "home",
"src": "src/home.ts", "src": "src/home.ts",
"isEntry": true, "isEntry": true,
"imports": [ "imports": [
"_style-C433w8gz.js" "_style-BKSucaDP.js"
] ]
}, },
"src/noita.ts": { "src/noita.ts": {
"file": "assets/noita-BcY9X9eS.js", "file": "assets/noita-Su4dRkwf.js",
"name": "noita", "name": "noita",
"src": "src/noita.ts", "src": "src/noita.ts",
"isEntry": true, "isEntry": true,
"imports": [ "imports": [
"_style-C433w8gz.js", "_style-BKSucaDP.js",
"_RankBadge.vue_vue_type_script_setup_true_lang-IllWPCyW.js" "_RankBadge.vue_vue_type_script_setup_true_lang-c__WwHxT.js"
] ]
}, },
"src/opus-magnum.ts": { "src/opus-magnum.ts": {
"file": "assets/opus_magnum-CCZTjOvR.js", "file": "assets/opus_magnum-BftkLCBu.js",
"name": "opus_magnum", "name": "opus_magnum",
"src": "src/opus-magnum.ts", "src": "src/opus-magnum.ts",
"isEntry": true, "isEntry": true,
"imports": [ "imports": [
"_style-C433w8gz.js", "_style-BKSucaDP.js",
"_RankBadge.vue_vue_type_script_setup_true_lang-IllWPCyW.js" "_RankBadge.vue_vue_type_script_setup_true_lang-c__WwHxT.js"
] ]
} }
} }