rework noita objectives + deathcounter

This commit is contained in:
2026-05-15 01:50:34 +02:00
parent 754b0b0803
commit 7cfab20826
14 changed files with 481 additions and 167 deletions
+158 -74
View File
@@ -1,11 +1,22 @@
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import dayjs from "dayjs";
import {
createColumnHelper,
useVueTable,
getCoreRowModel,
getFilteredRowModel,
getSortedRowModel,
type ColumnFiltersState,
type SortingState,
} from "@tanstack/vue-table";
interface Objective {
objectiv_id: string;
count: number;
points_per_objectiv?: number;
total_points?: number;
first_seen_at: string;
seed: string;
points_per_objectiv: number;
total_points: number;
}
const userInfo = ref({
@@ -21,61 +32,91 @@ const isUploading = ref(false);
const isDragover = ref(false);
const objectives = ref<Objective[]>([]);
const objectiveSearchQuery = ref("");
const objectiveSortBy = ref<"id" | "count" | "points_per" | "total_points">("id");
const objectiveSortDesc = ref(false);
const isLoadingLeaderboard = ref(false);
const leaderboard = ref<any[]>([]);
const isLeaderboardModalOpen = ref(false);
const filteredObjectives = computed(() => {
const query = objectiveSearchQuery.value.toLowerCase();
const columnHelper = createColumnHelper<Objective>();
const sorting = ref<SortingState>([]);
const columnFilters = ref<ColumnFiltersState>([]);
let filtered = objectives.value;
if (query) {
filtered = filtered.filter(
(obj) =>
obj.objectiv_id.toLowerCase().includes(query) ||
obj.count.toString().includes(query)
);
}
const sorted = [...filtered].sort((a, b) => {
let aValue: number | string;
let bValue: number | string;
switch (objectiveSortBy.value) {
case "points_per":
aValue = a.points_per_objectiv || 0;
bValue = b.points_per_objectiv || 0;
break;
case "total_points":
aValue = a.total_points || 0;
bValue = b.total_points || 0;
break;
case "id":
default:
aValue = a.objectiv_id.toLowerCase();
bValue = b.objectiv_id.toLowerCase();
}
if (aValue < bValue) return objectiveSortDesc.value ? 1 : -1;
if (aValue > bValue) return objectiveSortDesc.value ? -1 : 1;
return 0;
});
return sorted;
});
const toggleObjectiveSort = (column: "id" | "points_per" | "total_points") => {
if (objectiveSortBy.value === column) {
objectiveSortDesc.value = !objectiveSortDesc.value;
} else {
objectiveSortBy.value = column;
objectiveSortDesc.value = false;
}
const formatDate = (dateString: string) => {
const date = dayjs(dateString);
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 = [
columnHelper.accessor("objectiv_id", {
header: "Objective ID",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("total_points", {
header: "Total Points",
cell: (info) => info.getValue() || 0,
}),
columnHelper.accessor("first_seen_at", {
header: "First seen",
cell: (info) => formatDate(info.getValue()),
sortingFn: (rowA, rowB) => {
const dateA = dayjs(rowA.original.first_seen_at);
const dateB = dayjs(rowB.original.first_seen_at);
return dateA.isBefore(dateB) ? -1 : dateA.isAfter(dateB) ? 1 : 0;
},
}),
columnHelper.accessor("seed", {
header: "Seed",
cell: (info) => info.getValue(),
}),
];
const table = computed(() =>
useVueTable({
get data() {
return objectives.value;
},
columns,
state: {
get sorting() {
return sorting.value;
},
get columnFilters() {
return columnFilters.value;
},
},
onSortingChange: (updater) => {
sorting.value =
typeof updater === "function" ? updater(sorting.value) : updater;
},
onColumnFiltersChange: (updater) => {
columnFilters.value =
typeof updater === "function" ? updater(columnFilters.value) : updater;
},
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
filterFns: {
fuzzy: (row, columnId, value) => {
const itemData = row.getValue(columnId);
const searchValue = value.toLowerCase();
if (columnId === "first_seen_at") {
const dateStr = itemData as string;
const formatted = dayjs(dateStr).format("MMM DD, YYYY HH:mm");
return formatted.toLowerCase().includes(searchValue);
}
return String(itemData).toLowerCase().includes(searchValue);
},
},
globalFilterFn: "fuzzy",
})
);
const filteredObjectives = computed(() => table.value.getRowModel().rows);
const handleFileUpload = (event: Event) => {
const input = event.target as HTMLInputElement;
if (input.files) {
@@ -387,8 +428,18 @@ onMounted(() => {
<div v-if="objectives.length > 0" class="space-y-4">
<!-- Search Input -->
<input v-model="objectiveSearchQuery" type="text" placeholder="Search objectives..."
class="input input-bordered w-full" />
<input
:value="columnFilters.find((f) => f.id === 'objectiv_id')?.value ?? ''"
@input="
(e) => {
const target = e.target as HTMLInputElement;
table.getColumn('objectiv_id')?.setFilterValue(target.value);
}
"
type="text"
placeholder="Search objectives..."
class="input input-bordered w-full"
/>
<!-- Results Summary -->
<div class="text-sm text-base-content/70">
@@ -400,31 +451,64 @@ onMounted(() => {
<table class="table table-zebra w-full">
<thead>
<tr>
<th class="cursor-pointer hover:bg-base-300" @click="toggleObjectiveSort('id')">
Objective ID
<i v-if="objectiveSortBy === 'id'"
:class="['mdi ml-2', objectiveSortDesc ? 'mdi-arrow-down' : 'mdi-arrow-up']"></i>
</th>
<th class="text-right cursor-pointer hover:bg-base-300"
@click="toggleObjectiveSort('total_points')">
Total Points
<i v-if="objectiveSortBy === 'total_points'"
:class="['mdi ml-2', objectiveSortDesc ? 'mdi-arrow-down' : 'mdi-arrow-up']"></i>
<th
v-for="header in table.getHeaderGroups()[0]?.headers"
:key="header.id"
:class="[
'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">
<span v-if="header.column.columnDef.id === 'objectiv_id'">
{{ header.isPlaceholder ? null : header.column.columnDef.header }}
</span>
<span v-else class="ml-auto">
{{ header.isPlaceholder ? null : header.column.columnDef.header }}
</span>
<i
v-if="header.column.getIsSorted()"
:class="[
'mdi ml-2',
header.column.getIsSorted() === 'desc'
? 'mdi-arrow-down'
: 'mdi-arrow-up',
]"
></i>
</div>
</th>
</tr>
</thead>
<tbody>
<tr v-for="obj in filteredObjectives" :key="obj.objectiv_id">
<td class="font-medium">
<a :href="`https://noita.wiki.gg/wiki/${obj.objectiv_id}`" target="_blank">
{{ obj.objectiv_id }}
<i class="mdi mdi-open-in-new"></i>
</a>
</td>
<td class="text-right font-bold text-success">
{{ obj.total_points || 0 }}
</td>
<td class="text-right">
<tr v-for="row in filteredObjectives" :key="row.id">
<td
v-for="cell in row.getVisibleCells()"
:key="cell.id"
:class="[
cell.column.id === 'objectiv_id'
? 'font-medium'
: 'text-right',
cell.column.id === 'total_points' ? 'font-bold text-primary' : '',
]"
>
<template v-if="cell.column.id === 'objectiv_id'">
<a
:href="`https://noita.wiki.gg/wiki/${row.original.objectiv_id}`"
target="_blank"
>
{{ row.original.objectiv_id }}
<i class="mdi mdi-open-in-new"></i>
</a>
</template>
<template v-else-if="cell.column.id === 'first_seen_at'">
<span :title="getDateTooltip(row.original.first_seen_at)">
{{ formatDate(row.original.first_seen_at) }}
</span>
</template>
<template v-else>
{{ cell.renderValue() }}
</template>
</td>
</tr>
</tbody>