Compare commits

...

2 Commits

Author SHA1 Message Date
89faa7cd40
chore: build assets 2026-05-15 20:47:22 +02:00
a800c7fff7
fix(noita): all objectives display 2026-05-15 20:43:30 +02:00
13 changed files with 107 additions and 80 deletions

View File

@ -87,15 +87,25 @@ def get_results(request: HttpRequest):
) )
# Build response with all objectives and compute total score # Build response with all objectives and compute total score
objectives_with_points = []
total_score = 0 total_score = 0
with_points = {}
for obj in ObjectivPoint.objects.all():
with_points[obj.objectiv_id] = {
"objectiv_id": obj.objectiv_id,
"display_string": obj.display_string,
"count": 0,
"max_count": obj.max_count,
"points_per_objectiv": obj.point,
"total_points": 0,
"first_seen_at": None,
"seed": None,
}
for obj in user_objectives.order_by("-total_points"): for obj in user_objectives.order_by("-total_points"):
points = obj["total_points"] or 0 points = obj["total_points"] or 0
objectives_with_points.append( with_points[obj["objectiv_id"]].update(
{ {
"objectiv_id": obj["objectiv_id"],
"count": obj["count"], "count": obj["count"],
"points_per_objectiv": obj["points_per_objectiv"] or 0,
"total_points": points, "total_points": points,
"first_seen_at": obj["first_seen_at"], "first_seen_at": obj["first_seen_at"],
"seed": obj["seed"], "seed": obj["seed"],
@ -109,7 +119,7 @@ def get_results(request: HttpRequest):
data = { data = {
"total_score": total_score, "total_score": total_score,
"deaths_count": deaths_count, "deaths_count": deaths_count,
"objectives": objectives_with_points, "objectives": list(with_points.values()),
} }
cache.set(f"api:noita:results:{request.user.id}", data, 300) cache.set(f"api:noita:results:{request.user.id}", data, 300)

View File

@ -15,10 +15,13 @@ class NoitaSubmissionOut(Schema):
class ObjectivResultOut(Schema): class ObjectivResultOut(Schema):
objectiv_id: str objectiv_id: str
first_seen_at: datetime display_string: str
seed: str first_seen_at: datetime | None
count: int
max_count: int
seed: str | None
points_per_objectiv: int points_per_objectiv: int
total_points: int total_points: int | None
class ResultsOut(Schema): class ResultsOut(Schema):

View File

@ -14,8 +14,11 @@ import {
interface Objective { interface Objective {
objectiv_id: string; objectiv_id: string;
first_seen_at: string; display_string: string;
seed: string; first_seen_at: string | null;
count: number;
max_count: number;
seed: string | null;
points_per_objectiv: number; points_per_objectiv: number;
total_points: number; total_points: number;
} }
@ -40,23 +43,21 @@ const columnHelper = createColumnHelper<Objective>();
const sorting = ref<SortingState>([]); const sorting = ref<SortingState>([]);
const columnFilters = ref<ColumnFiltersState>([]); const columnFilters = ref<ColumnFiltersState>([]);
const formatDate = (dateString: string) => { const formatDate = (dateString: string | null) => {
if (!dateString) {
return ""
}
const date = dayjs(dateString); const date = dayjs(dateString);
return date.format("MMM DD, YYYY HH:mm"); return date.format("MMM DD, YYYY HH:mm");
}; };
const getDateTooltip = (dateString: string) => {
const date = dayjs(dateString);
return date.format("dddd, MMMM D, YYYY [at] h:mm A");
};
const columns = [ const columns = [
columnHelper.accessor("objectiv_id", { columnHelper.accessor("objectiv_id", {
header: "Objective ID", header: "Objective ID",
cell: (info) => info.getValue(), cell: (info) => info.getValue(),
}), }),
columnHelper.accessor("total_points", { columnHelper.accessor("total_points", {
header: "Total Points", header: "Your points",
cell: (info) => info.getValue() || 0, cell: (info) => info.getValue() || 0,
}), }),
columnHelper.accessor("first_seen_at", { columnHelper.accessor("first_seen_at", {
@ -65,6 +66,12 @@ const columns = [
sortingFn: (rowA, rowB) => { sortingFn: (rowA, rowB) => {
const dateA = dayjs(rowA.original.first_seen_at); const dateA = dayjs(rowA.original.first_seen_at);
const dateB = dayjs(rowB.original.first_seen_at); const dateB = dayjs(rowB.original.first_seen_at);
if (!rowA.original.first_seen_at) {
return rowB.original.first_seen_at ? 1 : 0
}
if (!rowB.original.first_seen_at) {
return rowA.original.first_seen_at ? 0 : 1
}
return dateA.isBefore(dateB) ? -1 : dateA.isAfter(dateB) ? 1 : 0; return dateA.isBefore(dateB) ? -1 : dateA.isAfter(dateB) ? 1 : 0;
}, },
}), }),
@ -104,9 +111,7 @@ const table = computed(() =>
const itemData = row.getValue(columnId); const itemData = row.getValue(columnId);
const searchValue = value.toLowerCase(); const searchValue = value.toLowerCase();
if (columnId === "first_seen_at") { if (columnId === "first_seen_at") {
const dateStr = itemData as string; return formatDate(itemData as string).includes(searchValue)
const formatted = dayjs(dateStr).format("MMM DD, YYYY HH:mm");
return formatted.toLowerCase().includes(searchValue);
} }
return String(itemData).toLowerCase().includes(searchValue); return String(itemData).toLowerCase().includes(searchValue);
}, },
@ -284,7 +289,7 @@ onMounted(() => {
<div class="min-h-screen bg-base-200"> <div class="min-h-screen bg-base-200">
<!-- Header --> <!-- Header -->
<div class="navbar bg-base-100 shadow-lg"> <div class="navbar bg-base-100 shadow-lg">
<div class="container mx-auto w-full flex items-center gap-4"> <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"> <button @click="goHome" class="btn btn-primary btn-sm">
<i class="mdi mdi-arrow-left"></i> <i class="mdi mdi-arrow-left"></i>
Back Back
@ -297,14 +302,16 @@ onMounted(() => {
</div> </div>
<!-- Main Content --> <!-- Main Content -->
<div class="container mx-auto px-4 py-8"> <div class="container min-w-3/4 mx-auto px-4 py-8">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Left Column: User Ranking --> <!-- Left Column: User Ranking -->
<div class="lg:col-span-1"> <div class="lg:col-span-1">
<div class="card bg-base-100 shadow-lg sticky top-8"> <div class="card bg-base-100 shadow-lg sticky top-8">
<div class="bg-gradient-to-br from-purple-600 to-purple-400 p-8 text-white rounded-t-2xl"> <div class="bg-gradient-to-br from-purple-600 to-purple-400 p-8 text-white rounded-t-2xl">
<i class="mdi mdi-trophy text-5xl"></i> <h2 class="text-3xl font-bold">
<h2 class="text-3xl font-bold mt-3">Your Ranking</h2> <i class="mdi mdi-trophy text-3xl"></i>
Your Ranking
</h2>
</div> </div>
<div class="card-body p-8"> <div class="card-body p-8">
<div class="text-center mb-8"> <div class="text-center mb-8">
@ -455,7 +462,7 @@ onMounted(() => {
<div class="card-body"> <div class="card-body">
<h2 class="card-title text-2xl mb-6"> <h2 class="card-title text-2xl mb-6">
<i class="mdi mdi-view-list text-purple-500 mr-2"></i> <i class="mdi mdi-view-list text-purple-500 mr-2"></i>
Your Objectives Objectives
</h2> </h2>
<div v-if="objectives.length === 0" class="text-center py-8"> <div v-if="objectives.length === 0" class="text-center py-8">
@ -509,16 +516,21 @@ onMounted(() => {
cell.column.id === 'objectiv_id' cell.column.id === 'objectiv_id'
? 'font-medium' ? 'font-medium'
: 'text-right', : 'text-right',
cell.column.id === 'total_points' ? 'font-bold text-primary' : '',
]"> ]">
<template v-if="cell.column.id === 'objectiv_id'"> <template v-if="cell.column.id === 'objectiv_id'">
<a :href="`https://noita.wiki.gg/wiki/${row.original.objectiv_id}`" target="_blank"> <a :href="`https://noita.wiki.gg/wiki/${row.original.objectiv_id}`" target="_blank">
{{ row.original.objectiv_id }} {{ row.original.display_string }}
<i class="mdi mdi-open-in-new"></i> <i class="mdi mdi-open-in-new"></i>
</a> </a>
</template> </template>
<template v-else-if="cell.column.id === 'total_points'">
<span :class="row.original.count >= row.original.max_count ? 'text-primary' : 'text-error'">
{{ row.original.total_points }} / {{ row.original.points_per_objectiv *
row.original.max_count }}
</span>
</template>
<template v-else-if="cell.column.id === 'first_seen_at'"> <template v-else-if="cell.column.id === 'first_seen_at'">
<span :title="getDateTooltip(row.original.first_seen_at)"> <span :title="formatDate(row.original.first_seen_at)">
{{ formatDate(row.original.first_seen_at) }} {{ formatDate(row.original.first_seen_at) }}
</span> </span>
</template> </template>

View File

@ -118,7 +118,7 @@ const goHome = () => {
<div class="min-h-screen bg-base-200"> <div class="min-h-screen bg-base-200">
<!-- Header --> <!-- Header -->
<div class="navbar bg-base-100 shadow-lg"> <div class="navbar bg-base-100 shadow-lg">
<div class="container mx-auto w-full flex items-center gap-4"> <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"> <button @click="goHome" class="btn btn-primary btn-sm">
<i class="mdi mdi-arrow-left"></i> <i class="mdi mdi-arrow-left"></i>
Back Back
@ -140,7 +140,7 @@ const goHome = () => {
</div> </div>
<!-- Main Content --> <!-- Main Content -->
<div class="container mx-auto px-4 py-8"> <div class="container min-w-3/4 mx-auto px-4 py-8">
<!-- Loading State --> <!-- Loading State -->
<div v-if="userInfo?.is_superuser" class="flex justify-center"> <div v-if="userInfo?.is_superuser" class="flex justify-center">
<div class="text-center"> <div class="text-center">

View File

@ -173,9 +173,11 @@ onMounted(() => {
<!-- Left Column: Your Ranking --> <!-- Left Column: Your Ranking -->
<div class="lg:col-span-1"> <div class="lg:col-span-1">
<div class="card bg-base-100 shadow-lg sticky top-8"> <div class="card bg-base-100 shadow-lg sticky top-8">
<div class="bg-gradient-to-br from-blue-600 to-blue-400 p-6 text-white rounded-t-2xl"> <div class="bg-gradient-to-br from-purple-600 to-purple-400 p-6 text-white rounded-t-2xl">
<i class="mdi mdi-trophy text-4xl"></i> <h3 class="text-3xl font-bold">
<h3 class="text-2xl font-bold mt-2">Your Ranking</h3> <i class="mdi mdi-trophy text-3xl"></i>
Your Ranking
</h3>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="text-center mb-6"> <div class="text-center mb-6">

View File

@ -1 +1 @@
import{k as t,l as a,p as n,v as s}from"./style-CrNkWMsg.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-CufywNmO.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-CrNkWMsg.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-CufywNmO.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,16 @@
{ {
"_RankBadge.vue_vue_type_script_setup_true_lang-CzaIkt15.js": { "_RankBadge.vue_vue_type_script_setup_true_lang-C5K0DD3W.js": {
"file": "assets/RankBadge.vue_vue_type_script_setup_true_lang-CzaIkt15.js", "file": "assets/RankBadge.vue_vue_type_script_setup_true_lang-C5K0DD3W.js",
"name": "RankBadge.vue_vue_type_script_setup_true_lang", "name": "RankBadge.vue_vue_type_script_setup_true_lang",
"imports": [ "imports": [
"_style-CrNkWMsg.js" "_style-CufywNmO.js"
] ]
}, },
"_style-CrNkWMsg.js": { "_style-CufywNmO.js": {
"file": "assets/style-CrNkWMsg.js", "file": "assets/style-CufywNmO.js",
"name": "style", "name": "style",
"css": [ "css": [
"assets/style-D95xr4by.css" "assets/style-DaHD49X0.css"
], ],
"assets": [ "assets": [
"assets/materialdesignicons-webfont-CSr8KVlo.eot", "assets/materialdesignicons-webfont-CSr8KVlo.eot",
@ -19,9 +19,9 @@
"assets/materialdesignicons-webfont-B7mPwVP_.ttf" "assets/materialdesignicons-webfont-B7mPwVP_.ttf"
] ]
}, },
"_style-D95xr4by.css": { "_style-DaHD49X0.css": {
"file": "assets/style-D95xr4by.css", "file": "assets/style-DaHD49X0.css",
"src": "_style-D95xr4by.css" "src": "_style-DaHD49X0.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",
@ -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-CCO6cuKi.js", "file": "assets/home-2m6DwiDu.js",
"name": "home", "name": "home",
"src": "src/home.ts", "src": "src/home.ts",
"isEntry": true, "isEntry": true,
"imports": [ "imports": [
"_style-CrNkWMsg.js" "_style-CufywNmO.js"
] ]
}, },
"src/noita.ts": { "src/noita.ts": {
"file": "assets/noita-ByhLkfmW.js", "file": "assets/noita-C7qVNYuP.js",
"name": "noita", "name": "noita",
"src": "src/noita.ts", "src": "src/noita.ts",
"isEntry": true, "isEntry": true,
"imports": [ "imports": [
"_style-CrNkWMsg.js", "_style-CufywNmO.js",
"_RankBadge.vue_vue_type_script_setup_true_lang-CzaIkt15.js" "_RankBadge.vue_vue_type_script_setup_true_lang-C5K0DD3W.js"
] ]
}, },
"src/opus-magnum.ts": { "src/opus-magnum.ts": {
"file": "assets/opus_magnum-CBb_5LJ1.js", "file": "assets/opus_magnum-CgBh_a7R.js",
"name": "opus_magnum", "name": "opus_magnum",
"src": "src/opus-magnum.ts", "src": "src/opus-magnum.ts",
"isEntry": true, "isEntry": true,
"imports": [ "imports": [
"_style-CrNkWMsg.js", "_style-CufywNmO.js",
"_RankBadge.vue_vue_type_script_setup_true_lang-CzaIkt15.js" "_RankBadge.vue_vue_type_script_setup_true_lang-C5K0DD3W.js"
] ]
} }
} }