top winners + by puzzle

This commit is contained in:
Loïc Gremaud 2026-05-22 09:32:13 +02:00
parent 4ba6a48246
commit 9e5ab8539a
Signed by: Legrems
GPG Key ID: D4620E6DF3E0121D
12 changed files with 129 additions and 31 deletions

View File

@ -6,6 +6,7 @@ import AdminPanel from "@/components/AdminPanel.vue";
import Results from "@/components/Results.vue";
import Winners from "@/components/Winners.vue";
import PuzzleResults from "@/components/PuzzleResults.vue";
import TopUsersLeaderboard from "@/components/TopUsersLeaderboard.vue";
import { apiService, errorHelpers } from "@/services/apiService";
import { usePuzzlesStore } from "@/stores/puzzles";
import { useSubmissionsStore } from "@/stores/submissions";
@ -191,10 +192,11 @@ const goHome = () => {
<!-- Main Content -->
<div v-else class="space-y-8">
<!-- Winners Section (only when tournament is closed) -->
<!-- Results Section (only when tournament is closed) -->
<div v-if="isTournamentClosed" class="space-y-8">
<Winners />
<TopUsersLeaderboard />
<PuzzleResults />
<Winners />
</div>
<template v-else>

View File

@ -0,0 +1,96 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { apiService } from "@/services/apiService";
import type { TournamentPuzzleResults } from "@/types";
const isLoading = ref(true);
const resultsData = ref<TournamentPuzzleResults | null>(null);
const error = ref<string>("");
const fetchResults = async () => {
isLoading.value = true;
error.value = "";
try {
const response = await apiService.getPuzzleResults(100);
if (response.data) {
resultsData.value = response.data;
} else if (response.error) {
error.value = response.error;
console.error("Error fetching results:", response.error);
}
} catch (err) {
error.value = err instanceof Error ? err.message : "Failed to fetch results";
console.error("Error fetching results:", err);
} finally {
isLoading.value = false;
}
};
const getTop10Users = () => {
if (!resultsData.value) return [];
const userScores: Record<string, { username: string; user_id: number; total_points: number }> = {};
resultsData.value.results.forEach((puzzle) => {
puzzle.submissions.forEach((submission) => {
const key = submission.user_id;
if (!userScores[key]) {
userScores[key] = {
username: submission.username,
user_id: submission.user_id,
total_points: 0
};
}
userScores[key].total_points += submission.rank_points || 0;
});
});
return Object.values(userScores)
.sort((a, b) => b.total_points - a.total_points)
.slice(0, 10);
};
onMounted(() => {
fetchResults();
});
</script>
<template>
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
<h2 class="card-title text-2xl flex items-center gap-2">
<i class="mdi mdi-podium text-yellow-500 text-3xl"></i>
Top 10 Users
</h2>
<div v-if="isLoading" class="flex justify-center py-12">
<span class="loading loading-spinner loading-lg"></span>
</div>
<div v-else-if="error" class="alert alert-error">
<i class="mdi mdi-alert-circle text-xl"></i>
<div>{{ error }}</div>
</div>
<div v-else-if="getTop10Users().length === 0" class="text-center py-8">
<p class="text-base-content/70">No results available yet.</p>
</div>
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-3">
<template v-for="(user, index) in getTop10Users()" :key="user.user_id">
<div class="bg-base-200 p-4 rounded-lg text-center" :class="{
'ring-2 ring-yellow-500': index === 0,
'ring-2 ring-gray-400': index === 1,
'ring-2 ring-orange-400': index === 2,
}">
<div class="text-3xl font-bold mb-2">
{{ index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : `#${index + 1}` }}
</div>
<div class="font-semibold text-sm mb-2 truncate">{{ user.username }}</div>
<div class="text-2xl font-bold text-primary">{{ user.total_points }} pts</div>
</div>
</template>
</div>
</div>
</div>
</template>

View File

@ -65,7 +65,7 @@ const flattenedRows = computed(() => {
});
});
return rows;
return rows.sort((a, b) => (b.total || 0) - (a.total || 0));
});
const openImageModal = (fileUrl: string, fileName: string) => {

View File

@ -1 +1 @@
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 _};
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 _};

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-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);
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);

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,20 +1,16 @@
{
"_RankBadge.vue_vue_type_script_setup_true_lang-c__WwHxT.js": {
"file": "assets/RankBadge.vue_vue_type_script_setup_true_lang-c__WwHxT.js",
"_RankBadge.vue_vue_type_script_setup_true_lang-CiUtgtLU.js": {
"file": "assets/RankBadge.vue_vue_type_script_setup_true_lang-CiUtgtLU.js",
"name": "RankBadge.vue_vue_type_script_setup_true_lang",
"imports": [
"_style-BKSucaDP.js"
"_style-C9QoPxDN.js"
]
},
"_style-8zNjBvNQ.css": {
"file": "assets/style-8zNjBvNQ.css",
"src": "_style-8zNjBvNQ.css"
},
"_style-BKSucaDP.js": {
"file": "assets/style-BKSucaDP.js",
"_style-C9QoPxDN.js": {
"file": "assets/style-C9QoPxDN.js",
"name": "style",
"css": [
"assets/style-8zNjBvNQ.css"
"assets/style-Cs9btLod.css"
],
"assets": [
"assets/materialdesignicons-webfont-CSr8KVlo.eot",
@ -23,6 +19,10 @@
"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": {
"file": "assets/materialdesignicons-webfont-CSr8KVlo.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/home.ts": {
"file": "assets/home-YbwRQP1Y.js",
"file": "assets/home-Cpe9mjX7.js",
"name": "home",
"src": "src/home.ts",
"isEntry": true,
"imports": [
"_style-BKSucaDP.js"
"_style-C9QoPxDN.js"
]
},
"src/noita.ts": {
"file": "assets/noita-Su4dRkwf.js",
"file": "assets/noita-C5wjrj1v.js",
"name": "noita",
"src": "src/noita.ts",
"isEntry": true,
"imports": [
"_style-BKSucaDP.js",
"_RankBadge.vue_vue_type_script_setup_true_lang-c__WwHxT.js"
"_style-C9QoPxDN.js",
"_RankBadge.vue_vue_type_script_setup_true_lang-CiUtgtLU.js"
]
},
"src/opus-magnum.ts": {
"file": "assets/opus_magnum-BftkLCBu.js",
"file": "assets/opus_magnum-BwlGd4nK.js",
"name": "opus_magnum",
"src": "src/opus-magnum.ts",
"isEntry": true,
"imports": [
"_style-BKSucaDP.js",
"_RankBadge.vue_vue_type_script_setup_true_lang-c__WwHxT.js"
"_style-C9QoPxDN.js",
"_RankBadge.vue_vue_type_script_setup_true_lang-CiUtgtLU.js"
]
}
}

View File

@ -1 +1 @@
{"root":["./src/home.ts","./src/noita.ts","./src/opus-magnum.ts","./src/services/apiService.ts","./src/services/ocrService.ts","./src/stores/index.ts","./src/stores/puzzles.ts","./src/stores/submissions.ts","./src/stores/uploads.ts","./src/types/index.ts","./src/Home.vue","./src/Noita.vue","./src/OpusMagnum.vue","./src/components/AdminPanel.vue","./src/components/FileUpload.vue","./src/components/PuzzleCard.vue","./src/components/PuzzleResults.vue","./src/components/RankBadge.vue","./src/components/Results.vue","./src/components/SubmissionForm.vue","./src/components/Winners.vue"],"version":"5.9.3"}
{"root":["./src/home.ts","./src/noita.ts","./src/opus-magnum.ts","./src/services/apiService.ts","./src/services/ocrService.ts","./src/stores/index.ts","./src/stores/puzzles.ts","./src/stores/submissions.ts","./src/stores/uploads.ts","./src/types/index.ts","./src/Home.vue","./src/Noita.vue","./src/OpusMagnum.vue","./src/components/AdminPanel.vue","./src/components/FileUpload.vue","./src/components/PuzzleCard.vue","./src/components/PuzzleResults.vue","./src/components/RankBadge.vue","./src/components/Results.vue","./src/components/SubmissionForm.vue","./src/components/TopUsersLeaderboard.vue","./src/components/Winners.vue"],"version":"5.9.3"}