mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-05 19:24:39 +00:00
feat: analytics page
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
<script lang="ts">
|
||||
import type { Assessment } from "./types";
|
||||
|
||||
interface Props {
|
||||
data: Assessment[];
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
let currentPage = $state(0);
|
||||
let itemsPerPage = $state(10);
|
||||
let sortColumn = $state<keyof Assessment | null>("due");
|
||||
let sortDirection = $state<"asc" | "desc">("desc");
|
||||
|
||||
const sortedData = $derived.by(() => {
|
||||
const list = [...data];
|
||||
if (!sortColumn) return list;
|
||||
list.sort((a, b) => {
|
||||
const av = a[sortColumn!];
|
||||
const bv = b[sortColumn!];
|
||||
if (av === bv) return 0;
|
||||
if (av == null) return 1;
|
||||
if (bv == null) return -1;
|
||||
const cmp = av < bv ? -1 : 1;
|
||||
return sortDirection === "asc" ? cmp : -cmp;
|
||||
});
|
||||
return list;
|
||||
});
|
||||
|
||||
const pageCount = $derived(Math.max(1, Math.ceil(sortedData.length / itemsPerPage)));
|
||||
const pageData = $derived(
|
||||
sortedData.slice(
|
||||
currentPage * itemsPerPage,
|
||||
(currentPage + 1) * itemsPerPage,
|
||||
),
|
||||
);
|
||||
|
||||
function toggleSort(column: keyof Assessment) {
|
||||
if (sortColumn === column) {
|
||||
sortDirection = sortDirection === "asc" ? "desc" : "asc";
|
||||
} else {
|
||||
sortColumn = column;
|
||||
sortDirection = "asc";
|
||||
}
|
||||
currentPage = 0;
|
||||
}
|
||||
|
||||
function formatStatus(status: string) {
|
||||
return status.replace(/_/g, " ").toLowerCase().replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
function gradeDisplay(a: Assessment) {
|
||||
if (a.finalGrade !== undefined) {
|
||||
return a.letterGrade
|
||||
? `${a.finalGrade}% (${a.letterGrade})`
|
||||
: `${a.finalGrade}%`;
|
||||
}
|
||||
return a.letterGrade ?? "—";
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="bsplus-analytics-table-wrap">
|
||||
<header class="bsplus-analytics-table-header">
|
||||
<h2>Assessment history</h2>
|
||||
</header>
|
||||
|
||||
<div class="bsplus-analytics-table-scroll">
|
||||
<table class="bsplus-analytics-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{#each [
|
||||
["title", "Title"],
|
||||
["subject", "Subject"],
|
||||
["due", "Due"],
|
||||
["status", "Status"],
|
||||
["finalGrade", "Grade"],
|
||||
] as [col, label]}
|
||||
<th>
|
||||
<button type="button" onclick={() => toggleSort(col as keyof Assessment)}>
|
||||
{label}
|
||||
{#if sortColumn === col}
|
||||
{sortDirection === "asc" ? " ↑" : " ↓"}
|
||||
{/if}
|
||||
</button>
|
||||
</th>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each pageData as row (row.id)}
|
||||
<tr>
|
||||
<td class="cell-title" title={row.title}>{row.title}</td>
|
||||
<td>{row.subject}</td>
|
||||
<td style="white-space: nowrap">
|
||||
{new Date(row.due).toLocaleDateString(undefined, {
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
})}
|
||||
</td>
|
||||
<td>{formatStatus(row.status)}</td>
|
||||
<td>
|
||||
{#if row.finalGrade !== undefined}
|
||||
<span class="bsplus-analytics-grade-pill">{gradeDisplay(row)}</span>
|
||||
{:else}
|
||||
{gradeDisplay(row)}
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan="5" style="text-align: center; padding: 2rem; color: var(--bsplus-analytics-muted)">
|
||||
No assessments match your filters
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<footer class="bsplus-analytics-table-footer">
|
||||
<label>
|
||||
Rows per page
|
||||
<select bind:value={itemsPerPage} onchange={() => (currentPage = 0)}>
|
||||
{#each [5, 10, 20, 50] as n}
|
||||
<option value={n}>{n}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<button
|
||||
type="button"
|
||||
class="bsplus-analytics-btn bsplus-analytics-btn-ghost"
|
||||
style="padding: 0.4rem 0.85rem; font-size: 0.8125rem;"
|
||||
disabled={currentPage === 0}
|
||||
onclick={() => currentPage--}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<span>Page {currentPage + 1} of {pageCount}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="bsplus-analytics-btn bsplus-analytics-btn-ghost"
|
||||
style="padding: 0.4rem 0.85rem; font-size: 0.8125rem;"
|
||||
disabled={currentPage >= pageCount - 1}
|
||||
onclick={() => currentPage++}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
</section>
|
||||
Reference in New Issue
Block a user