Crank dat!

This commit is contained in:
purplebored committed 2026-11-08 19:28:03 +01:00
commit fe13a4d989
17 files changed
+6624

No files matched your search

+167
View File
@@ -0,0 +1,167 @@
/* ==========================================================================
librus-tui frontend - grades view
========================================================================== */
"use strict";
async function renderGrades(token) {
const data = await api("/api/grades");
if (token !== state.loadToken) return;
const subjects = data.subjects || {};
const sorted = Object.entries(subjects).sort((a, b) =>
String(a[0]).localeCompare(String(b[0]), "pl")
);
state.gradesFlat = [];
state.gradeGroups = sorted.map(([subject, grades]) => {
const items = (grades || []).map((grade) => {
const item = { ...grade, _subject: subject, _index: state.gradesFlat.length };
state.gradesFlat.push(item);
return item;
});
return { subject, grades: items };
});
const overall = averageLabel(state.gradesFlat);
const gradeCount = state.gradesFlat.length;
$("#view").innerHTML = `
<div class="view-inner">
<div class="toolbar">
<span>filter</span>
<input class="tool-input" id="grade-filter" type="text"
placeholder="subject or category…" spellcheck="false" autocomplete="off">
<button class="btn btn-sm" type="button" data-action="grades-clear">[ clear ]</button>
<span class="grow"></span>
<span class="grades-summary">
<span>average <b>${esc(overall)}</b></span>
<span><b>${gradeCount}</b> grades</span>
<span><b>${state.gradeGroups.length}</b> subjects</span>
</span>
</div>
<div class="grades-grid" id="grades-list"></div>
<div class="toolbar" style="margin-top:12px">
<div class="legend">
<span><span class="swatch green"></span>5 / 6</span>
<span><span class="swatch cyan"></span>4</span>
<span><span class="swatch amber"></span>3</span>
<span><span class="swatch red"></span>1 / 2</span>
<span class="dim">dashed chip = final · outlined = semester</span>
</div>
</div>
</div>
`;
const input = $("#grade-filter");
if (input) {
input.value = state.gradeFilter;
input.addEventListener("input", () => {
state.gradeFilter = input.value;
renderGradesList();
});
}
renderGradesList();
}
function gradeWeightLabel(weight) {
const raw = String(weight ?? "").toLowerCase();
if (!raw || raw === "none" || raw === "null") return "";
return `w${raw}`;
}
function renderGradesList() {
const container = $("#grades-list");
if (!container) return;
const query = state.gradeFilter.trim().toLowerCase();
const groups = state.gradeGroups.filter((group) => {
if (!query) return true;
if (group.subject.toLowerCase().includes(query)) return true;
return group.grades.some((grade) =>
`${grade.Category || ""} ${grade.Grade || ""}`.toLowerCase().includes(query)
);
});
if (!groups.length) {
container.innerHTML = `<div class="empty" style="grid-column:1/-1">no matching grades</div>`;
return;
}
container.innerHTML = groups
.map((group) => {
const average = averageLabel(group.grades);
const chips = group.grades.length
? group.grades
.map((grade) => {
const weight = gradeWeightLabel(grade.Weight);
const classes = ["grade-chip", gradeClass(grade.Grade)];
if (grade.isFinal) classes.push("final");
if (grade.isSemester) classes.push("semester");
const label = `${group.subject} · ${grade.Grade} · ${grade.Category || "grade"} · ${fmtDate(grade.AddDate)}`;
return `
<button class="${classes.join(" ")}" type="button" data-grade="${grade._index}" title="${esc(label)}">
${weight ? `<span class="g-weight">${esc(weight)}</span>` : ""}
<span class="g">${esc(grade.Grade)}</span>
<span class="g-meta">${esc(grade.Category || "—")}</span>
</button>`;
})
.join("")
: `<span class="dim" style="font-size:10.5px">— no grades yet —</span>`;
return `
<section class="panel subject-card">
<div class="panel-title">
<span>${esc(group.subject)}</span>
<span class="subject-average">avg ${esc(average)}</span>
</div>
<div class="panel-body">
<div class="grade-chips">${chips}</div>
</div>
</section>`;
})
.join("");
}
function focusGradeFilter() {
const input = $("#grade-filter");
if (input) {
input.focus();
input.select();
}
}
function openGradeDetail(index) {
const grade = state.gradesFlat[index];
if (!grade) return;
const flags = [];
if (grade.isFinal) flags.push("final");
if (grade.isSemester) flags.push("semester");
if (grade.Weight && String(grade.Weight).toLowerCase() !== "none") {
flags.push(`weight ${grade.Weight}`);
}
const comment =
grade.Comment && grade.Comment !== "No comment"
? `<div class="detail-comment">${esc(grade.Comment)}</div>`
: "";
const body = `
<dl class="detail-grid">
<dt>subject</dt><dd>${esc(grade._subject)}</dd>
<dt>grade</dt><dd>${esc(grade.Grade)}</dd>
<dt>category</dt><dd>${esc(grade.Category || "—")}</dd>
<dt>weight</dt><dd>${esc(grade.Weight ?? "—")}</dd>
<dt>added</dt><dd>${esc(fmtDate(grade.AddDate))}</dd>
<dt>teacher</dt><dd>${esc(personName(grade.AddedBy) || "—")}</dd>
<dt>semester</dt><dd>${esc(grade.Semester ?? "—")}${flags.length ? ` · ${esc(flags.join(" · "))}` : ""}</dd>
</dl>
${comment}
`;
openModal(`grade ${grade.Grade} · ${grade._subject}`, body, [
{ label: "[ close ]", close: true },
]);
}