feat(noita): death counter + ranking display
This commit is contained in:
parent
7cfab20826
commit
3e04f8312a
@ -16,7 +16,7 @@ from ninja.files import UploadedFile
|
|||||||
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 .models import LogfileSubmission, Objectiv, ObjectivPoint
|
from .models import LogfileSubmission, Objectiv, ObjectivPoint, DeathCounter
|
||||||
from .schemas import NoitaSubmissionOut
|
from .schemas import NoitaSubmissionOut
|
||||||
|
|
||||||
|
|
||||||
@ -103,8 +103,12 @@ def get_results(request: HttpRequest):
|
|||||||
)
|
)
|
||||||
total_score += points
|
total_score += points
|
||||||
|
|
||||||
|
# Count deaths for the user
|
||||||
|
deaths_count = DeathCounter.objects.filter(user=request.user).count()
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"total_score": total_score,
|
"total_score": total_score,
|
||||||
|
"deaths_count": deaths_count,
|
||||||
"objectives": objectives_with_points,
|
"objectives": objectives_with_points,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,12 +191,14 @@ def get_leaderboard(request: HttpRequest):
|
|||||||
.distinct()
|
.distinct()
|
||||||
.count()
|
.count()
|
||||||
)
|
)
|
||||||
|
deaths_count = DeathCounter.objects.filter(user_id=user_id).count()
|
||||||
users_with_scores.append(
|
users_with_scores.append(
|
||||||
{
|
{
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"username": user.username,
|
"username": user.username,
|
||||||
"total_score": total_score,
|
"total_score": total_score,
|
||||||
"objectives_count": objectives_count,
|
"objectives_count": objectives_count,
|
||||||
|
"deaths_count": deaths_count,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -204,6 +210,7 @@ def get_leaderboard(request: HttpRequest):
|
|||||||
"username": entry["username"],
|
"username": entry["username"],
|
||||||
"total_score": entry["total_score"],
|
"total_score": entry["total_score"],
|
||||||
"objectives_count": entry["objectives_count"],
|
"objectives_count": entry["objectives_count"],
|
||||||
|
"deaths_count": entry["deaths_count"],
|
||||||
}
|
}
|
||||||
for idx, entry in enumerate(users_with_scores)
|
for idx, entry in enumerate(users_with_scores)
|
||||||
]
|
]
|
||||||
|
|||||||
@ -23,6 +23,7 @@ class ObjectivResultOut(Schema):
|
|||||||
|
|
||||||
class ResultsOut(Schema):
|
class ResultsOut(Schema):
|
||||||
total_score: int
|
total_score: int
|
||||||
|
deaths_count: int
|
||||||
objectives: list[ObjectivResultOut]
|
objectives: list[ObjectivResultOut]
|
||||||
|
|
||||||
|
|
||||||
@ -31,6 +32,7 @@ class LeaderboardEntryOut(Schema):
|
|||||||
username: str
|
username: str
|
||||||
total_score: int
|
total_score: int
|
||||||
objectives_count: int
|
objectives_count: int
|
||||||
|
deaths_count: int
|
||||||
|
|
||||||
|
|
||||||
class LeaderboardOut(Schema):
|
class LeaderboardOut(Schema):
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from "vue";
|
import { ref, computed, onMounted } from "vue";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import RankBadge from "@/components/RankBadge.vue";
|
||||||
import {
|
import {
|
||||||
createColumnHelper,
|
createColumnHelper,
|
||||||
useVueTable,
|
useVueTable,
|
||||||
@ -24,6 +25,7 @@ const userInfo = ref({
|
|||||||
rank: null as number | null,
|
rank: null as number | null,
|
||||||
score: 0,
|
score: 0,
|
||||||
runsSubmitted: 0,
|
runsSubmitted: 0,
|
||||||
|
deathsCount: 0,
|
||||||
isStaff: false,
|
isStaff: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -31,10 +33,8 @@ const uploadedFiles = ref<File[]>([]);
|
|||||||
const isUploading = ref(false);
|
const isUploading = ref(false);
|
||||||
const isDragover = ref(false);
|
const isDragover = ref(false);
|
||||||
const objectives = ref<Objective[]>([]);
|
const objectives = ref<Objective[]>([]);
|
||||||
const objectiveSearchQuery = ref("");
|
|
||||||
const isLoadingLeaderboard = ref(false);
|
const isLoadingLeaderboard = ref(false);
|
||||||
const leaderboard = ref<any[]>([]);
|
const leaderboard = ref<any[]>([]);
|
||||||
const isLeaderboardModalOpen = ref(false);
|
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<Objective>();
|
const columnHelper = createColumnHelper<Objective>();
|
||||||
const sorting = ref<SortingState>([]);
|
const sorting = ref<SortingState>([]);
|
||||||
@ -111,7 +111,6 @@ const table = computed(() =>
|
|||||||
return String(itemData).toLowerCase().includes(searchValue);
|
return String(itemData).toLowerCase().includes(searchValue);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
globalFilterFn: "fuzzy",
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -197,6 +196,7 @@ const fetchUserResults = async () => {
|
|||||||
|
|
||||||
const results = await response.json();
|
const results = await response.json();
|
||||||
userInfo.value.score = results.total_score;
|
userInfo.value.score = results.total_score;
|
||||||
|
userInfo.value.deathsCount = results.deaths_count;
|
||||||
userInfo.value.runsSubmitted = results.objectives.length;
|
userInfo.value.runsSubmitted = results.objectives.length;
|
||||||
objectives.value = results.objectives;
|
objectives.value = results.objectives;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -221,6 +221,7 @@ const fetchLeaderboard = async () => {
|
|||||||
if (userRank) {
|
if (userRank) {
|
||||||
userInfo.value.rank = userRank.rank;
|
userInfo.value.rank = userRank.rank;
|
||||||
userInfo.value.score = userRank.total_score;
|
userInfo.value.score = userRank.total_score;
|
||||||
|
userInfo.value.deathsCount = userRank.deaths_count;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching leaderboard:", error);
|
console.error("Error fetching leaderboard:", error);
|
||||||
@ -297,18 +298,18 @@ onMounted(() => {
|
|||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div class="container mx-auto px-4 py-8">
|
<div class="container mx-auto px-4 py-8">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 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-6 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-4xl"></i>
|
<i class="mdi mdi-trophy text-5xl"></i>
|
||||||
<h2 class="text-2xl font-bold mt-2">Your Ranking</h2>
|
<h2 class="text-3xl font-bold mt-3">Your Ranking</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body p-8">
|
||||||
<div class="text-center mb-6">
|
<div class="text-center mb-8">
|
||||||
<p class="text-sm text-base-content/70">Player</p>
|
<p class="text-base text-base-content/70">Player</p>
|
||||||
<p class="text-3xl font-bold">{{ userInfo.username }}</p>
|
<p class="text-4xl font-bold mt-2">{{ userInfo.username }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
@ -317,30 +318,63 @@ onMounted(() => {
|
|||||||
<span class="loading loading-spinner loading-lg"></span>
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="space-y-4">
|
<div v-else class="space-y-6">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<p class="text-sm text-base-content/70 mb-1">Current Rank</p>
|
<p class="text-base text-base-content/70 mb-3">Current Rank</p>
|
||||||
<p v-if="userInfo.rank !== null" class="text-4xl font-bold text-primary">
|
<RankBadge :rank="userInfo.rank" />
|
||||||
#{{ userInfo.rank }}
|
|
||||||
</p>
|
|
||||||
<p v-else class="text-2xl text-base-content/50">No rank yet</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<p class="text-sm text-base-content/70 mb-1">Total Score</p>
|
<p class="text-base text-base-content/70 mb-2">Total Score</p>
|
||||||
<p class="text-2xl font-bold">{{ userInfo.score.toLocaleString() }}</p>
|
<p class="text-3xl font-bold">{{ userInfo.score.toLocaleString() }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<p class="text-sm text-base-content/70 mb-1">Objectives Completed</p>
|
<p class="text-base text-base-content/70 mb-2">Objectives Completed</p>
|
||||||
<p class="text-2xl font-bold">{{ userInfo.runsSubmitted }}</p>
|
<p class="text-3xl font-bold">{{ userInfo.runsSubmitted }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="text-base text-base-content/70 mb-2">Deaths</p>
|
||||||
|
<p class="text-3xl font-bold text-error">{{ userInfo.deathsCount }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button @click="isLeaderboardModalOpen = true" class="btn btn-outline btn-sm w-full mt-6">
|
<!-- Leaderboard Table -->
|
||||||
<i class="mdi mdi-trophy mr-1"></i>
|
<div class="mt-6">
|
||||||
View Full Leaderboard
|
<h3 class="font-bold text-lg mb-3">Global Leaderboard</h3>
|
||||||
</button>
|
<div class="overflow-x-auto">
|
||||||
|
<table class="table table-sm w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Rank</th>
|
||||||
|
<th>Player</th>
|
||||||
|
<th class="text-right">Score</th>
|
||||||
|
<th class="text-right">Objectives</th>
|
||||||
|
<th class="text-right">Deaths</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="entry in leaderboard" :key="entry.username"
|
||||||
|
:class="{ 'bg-primary/20': entry.username === userInfo.username }">
|
||||||
|
<td class="font-bold">
|
||||||
|
<RankBadge :rank="entry.rank" />
|
||||||
|
</td>
|
||||||
|
<td class="text-sm">
|
||||||
|
{{ entry.username }}
|
||||||
|
<span v-if="entry.username === userInfo.username" class="badge badge-primary badge-sm ml-1">
|
||||||
|
You
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-right text-sm font-bold text-primary">{{ entry.total_score.toLocaleString() }}
|
||||||
|
</td>
|
||||||
|
<td class="text-right text-sm">{{ entry.objectives_count }}</td>
|
||||||
|
<td class="text-right text-sm text-error">{{ entry.deaths_count }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button v-if="userInfo.isStaff" @click="clearCache" class="btn btn-error btn-sm w-full mt-3">
|
<button v-if="userInfo.isStaff" @click="clearCache" class="btn btn-error btn-sm w-full mt-3">
|
||||||
<i class="mdi mdi-cache-clear mr-1"></i>
|
<i class="mdi mdi-cache-clear mr-1"></i>
|
||||||
@ -351,7 +385,7 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right Column: Upload -->
|
<!-- Right Column: Upload -->
|
||||||
<div class="lg:col-span-2">
|
<div class="lg:col-span-1">
|
||||||
<div class="card bg-base-100 shadow-lg">
|
<div class="card bg-base-100 shadow-lg">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title text-2xl mb-6">
|
<h2 class="card-title text-2xl mb-6">
|
||||||
@ -428,18 +462,12 @@ onMounted(() => {
|
|||||||
|
|
||||||
<div v-if="objectives.length > 0" class="space-y-4">
|
<div v-if="objectives.length > 0" class="space-y-4">
|
||||||
<!-- Search Input -->
|
<!-- Search Input -->
|
||||||
<input
|
<input :value="columnFilters.find((f) => f.id === 'objectiv_id')?.value ?? ''" @input="
|
||||||
:value="columnFilters.find((f) => f.id === 'objectiv_id')?.value ?? ''"
|
(e) => {
|
||||||
@input="
|
const target = e.target as HTMLInputElement;
|
||||||
(e) => {
|
table.getColumn('objectiv_id')?.setFilterValue(target.value);
|
||||||
const target = e.target as HTMLInputElement;
|
}
|
||||||
table.getColumn('objectiv_id')?.setFilterValue(target.value);
|
" type="text" placeholder="Search objectives..." class="input input-bordered w-full" />
|
||||||
}
|
|
||||||
"
|
|
||||||
type="text"
|
|
||||||
placeholder="Search objectives..."
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Results Summary -->
|
<!-- Results Summary -->
|
||||||
<div class="text-sm text-base-content/70">
|
<div class="text-sm text-base-content/70">
|
||||||
@ -451,15 +479,10 @@ onMounted(() => {
|
|||||||
<table class="table table-zebra w-full">
|
<table class="table table-zebra w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th v-for="header in table.getHeaderGroups()[0]?.headers" :key="header.id" :class="[
|
||||||
v-for="header in table.getHeaderGroups()[0]?.headers"
|
'cursor-pointer hover:bg-base-300',
|
||||||
:key="header.id"
|
header.column.columnDef.id === 'objectiv_id' ? 'text-left' : 'text-right',
|
||||||
:class="[
|
]" @click="header.column.toggleSorting()">
|
||||||
'cursor-pointer hover:bg-base-300',
|
|
||||||
header.column.columnDef.id === 'objectiv_id' ? 'text-left' : 'text-right',
|
|
||||||
]"
|
|
||||||
@click="header.column.toggleSorting()"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span v-if="header.column.columnDef.id === 'objectiv_id'">
|
<span v-if="header.column.columnDef.id === 'objectiv_id'">
|
||||||
{{ header.isPlaceholder ? null : header.column.columnDef.header }}
|
{{ header.isPlaceholder ? null : header.column.columnDef.header }}
|
||||||
@ -467,36 +490,26 @@ onMounted(() => {
|
|||||||
<span v-else class="ml-auto">
|
<span v-else class="ml-auto">
|
||||||
{{ header.isPlaceholder ? null : header.column.columnDef.header }}
|
{{ header.isPlaceholder ? null : header.column.columnDef.header }}
|
||||||
</span>
|
</span>
|
||||||
<i
|
<i v-if="header.column.getIsSorted()" :class="[
|
||||||
v-if="header.column.getIsSorted()"
|
'mdi ml-2',
|
||||||
:class="[
|
header.column.getIsSorted() === 'desc'
|
||||||
'mdi ml-2',
|
? 'mdi-arrow-down'
|
||||||
header.column.getIsSorted() === 'desc'
|
: 'mdi-arrow-up',
|
||||||
? 'mdi-arrow-down'
|
]"></i>
|
||||||
: 'mdi-arrow-up',
|
|
||||||
]"
|
|
||||||
></i>
|
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="row in filteredObjectives" :key="row.id">
|
<tr v-for="row in filteredObjectives" :key="row.id">
|
||||||
<td
|
<td v-for="cell in row.getVisibleCells()" :key="cell.id" :class="[
|
||||||
v-for="cell in row.getVisibleCells()"
|
cell.column.id === 'objectiv_id'
|
||||||
:key="cell.id"
|
? 'font-medium'
|
||||||
:class="[
|
: 'text-right',
|
||||||
cell.column.id === 'objectiv_id'
|
cell.column.id === 'total_points' ? 'font-bold text-primary' : '',
|
||||||
? 'font-medium'
|
]">
|
||||||
: '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
|
<a :href="`https://noita.wiki.gg/wiki/${row.original.objectiv_id}`" target="_blank">
|
||||||
:href="`https://noita.wiki.gg/wiki/${row.original.objectiv_id}`"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{{ row.original.objectiv_id }}
|
{{ row.original.objectiv_id }}
|
||||||
<i class="mdi mdi-open-in-new"></i>
|
<i class="mdi mdi-open-in-new"></i>
|
||||||
</a>
|
</a>
|
||||||
@ -526,63 +539,5 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Leaderboard Modal -->
|
|
||||||
<div v-if="isLeaderboardModalOpen" class="modal modal-open">
|
|
||||||
<div class="modal-box max-w-4xl">
|
|
||||||
<div class="flex justify-between items-center mb-4">
|
|
||||||
<h3 class="font-bold text-lg">
|
|
||||||
<i class="mdi mdi-trophy text-yellow-500 mr-2"></i>
|
|
||||||
Global Leaderboard
|
|
||||||
</h3>
|
|
||||||
<button @click="isLeaderboardModalOpen = false" class="btn btn-sm btn-circle btn-ghost">
|
|
||||||
<i class="mdi mdi-close"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Leaderboard Table -->
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<table class="table table-zebra w-full">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Rank</th>
|
|
||||||
<th>Username</th>
|
|
||||||
<th class="text-right">Objectives</th>
|
|
||||||
<th class="text-right">Score</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="entry in leaderboard" :key="entry.username"
|
|
||||||
:class="{ 'bg-primary/20': entry.username === userInfo.username }">
|
|
||||||
<td class="font-bold">
|
|
||||||
<span v-if="entry.rank === 1" class="badge badge-warning badge-lg">
|
|
||||||
🏆 #{{ entry.rank }}
|
|
||||||
</span>
|
|
||||||
<span v-else-if="entry.rank === 2" class="badge badge-lg">
|
|
||||||
🥈 #{{ entry.rank }}
|
|
||||||
</span>
|
|
||||||
<span v-else-if="entry.rank === 3" class="badge badge-lg">
|
|
||||||
🥉 #{{ entry.rank }}
|
|
||||||
</span>
|
|
||||||
<span v-else>#{{ entry.rank }}</span>
|
|
||||||
</td>
|
|
||||||
<td class="font-medium">
|
|
||||||
{{ entry.username }}
|
|
||||||
<span v-if="entry.username === userInfo.username" class="badge badge-primary badge-sm ml-2">
|
|
||||||
You
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-right">{{ entry.objectives_count }}</td>
|
|
||||||
<td class="text-right font-bold">{{ entry.total_score.toLocaleString() }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="leaderboard.length === 0" class="text-center py-8">
|
|
||||||
<p class="text-base-content/70">No entries yet</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-backdrop" @click="isLeaderboardModalOpen = false"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
25
polylan_submitter/src/components/RankBadge.vue
Normal file
25
polylan_submitter/src/components/RankBadge.vue
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
rank: number | null;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="rank !== null" class="flex justify-center">
|
||||||
|
<span v-if="rank === 1" class="badge badge-warning badge-lg">
|
||||||
|
🏆 #{{ rank }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="rank === 2" class="badge badge-lg">
|
||||||
|
🥈 #{{ rank }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="rank === 3" class="badge badge-lg">
|
||||||
|
🥉 #{{ rank }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="badge badge-lg">
|
||||||
|
#{{ rank }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-2xl text-base-content/50">
|
||||||
|
No rank yet
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
|
import RankBadge from "./RankBadge.vue";
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: number;
|
id: number;
|
||||||
@ -264,16 +265,7 @@ onMounted(() => {
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(user, index) in getOverallRanking()" :key="user.username">
|
<tr v-for="(user, index) in getOverallRanking()" :key="user.username">
|
||||||
<td class="font-bold">
|
<td class="font-bold">
|
||||||
<span v-if="index === 0" class="badge badge-warning badge-lg">
|
<RankBadge :rank="index + 1" />
|
||||||
🏆 #1
|
|
||||||
</span>
|
|
||||||
<span v-else-if="index === 1" class="badge badge-lg">
|
|
||||||
🥈 #2
|
|
||||||
</span>
|
|
||||||
<span v-else-if="index === 2" class="badge badge-lg">
|
|
||||||
🥉 #3
|
|
||||||
</span>
|
|
||||||
<span v-else>#{{ index + 1 }}</span>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="font-medium">{{ user.username }}</td>
|
<td class="font-medium">{{ user.username }}</td>
|
||||||
<td class="text-right">{{ user.puzzlesSolved }}</td>
|
<td class="text-right">{{ user.puzzlesSolved }}</td>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user