mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-05 19:24:39 +00:00
feat: add confetti and render overview with svelte
This commit is contained in:
@@ -75,6 +75,7 @@
|
|||||||
"@uiw/codemirror-extensions-color": "^4.23.10",
|
"@uiw/codemirror-extensions-color": "^4.23.10",
|
||||||
"@uiw/codemirror-theme-github": "^4.23.10",
|
"@uiw/codemirror-theme-github": "^4.23.10",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
|
"canvas-confetti": "^1.9.3",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"color": "^5.0.0",
|
"color": "^5.0.0",
|
||||||
"dompurify": "^3.2.4",
|
"dompurify": "^3.2.4",
|
||||||
|
|||||||
@@ -15,9 +15,11 @@ export default function renderSvelte(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (mountPoint instanceof ShadowRoot) {
|
||||||
const styleElement = document.createElement("style");
|
const styleElement = document.createElement("style");
|
||||||
styleElement.textContent = style;
|
styleElement.textContent = style;
|
||||||
mountPoint.appendChild(styleElement);
|
mountPoint.appendChild(styleElement);
|
||||||
|
}
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,384 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { determineStatus, formatDate, getGradeValue } from "./utils";
|
||||||
|
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||||
|
import confetti from "canvas-confetti";
|
||||||
|
|
||||||
|
export let data: any;
|
||||||
|
|
||||||
|
interface FilterOptions {
|
||||||
|
subject: string;
|
||||||
|
sortBy: "due" | "grade" | "subject" | "title";
|
||||||
|
}
|
||||||
|
|
||||||
|
function percentageToLetter(percentage: number): string {
|
||||||
|
const letterMap: Record<number, string> = {
|
||||||
|
100: "A+",
|
||||||
|
95: "A",
|
||||||
|
90: "A-",
|
||||||
|
85: "B+",
|
||||||
|
80: "B",
|
||||||
|
75: "B-",
|
||||||
|
70: "C+",
|
||||||
|
65: "C",
|
||||||
|
60: "C-",
|
||||||
|
55: "D+",
|
||||||
|
50: "D",
|
||||||
|
45: "D-",
|
||||||
|
40: "E+",
|
||||||
|
35: "E",
|
||||||
|
30: "E-",
|
||||||
|
0: "F",
|
||||||
|
};
|
||||||
|
|
||||||
|
const rounded = Math.ceil(percentage / 5) * 5;
|
||||||
|
return letterMap[rounded] || "F";
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentFilters: FilterOptions = {
|
||||||
|
subject: "all",
|
||||||
|
sortBy: "due",
|
||||||
|
};
|
||||||
|
|
||||||
|
let filteredAssessments: any[] = [];
|
||||||
|
let statusGroups: Record<string, any[]> = {};
|
||||||
|
|
||||||
|
function updateAssessments() {
|
||||||
|
filteredAssessments = data.assessments.filter((a: any) => {
|
||||||
|
const subjectMatch =
|
||||||
|
currentFilters.subject === "all" || a.code === currentFilters.subject;
|
||||||
|
return subjectMatch;
|
||||||
|
});
|
||||||
|
|
||||||
|
filteredAssessments.sort((a: any, b: any) => {
|
||||||
|
switch (currentFilters.sortBy) {
|
||||||
|
case "due":
|
||||||
|
return new Date(a.due).getTime() - new Date(b.due).getTime();
|
||||||
|
case "grade":
|
||||||
|
const gradeA = getGradeValue(a);
|
||||||
|
const gradeB = getGradeValue(b);
|
||||||
|
if (gradeA === null && gradeB === null) return 0;
|
||||||
|
if (gradeA === null) return 1;
|
||||||
|
if (gradeB === null) return -1;
|
||||||
|
return gradeB - gradeA;
|
||||||
|
case "subject":
|
||||||
|
return a.code.localeCompare(b.code);
|
||||||
|
case "title":
|
||||||
|
return a.title.localeCompare(b.title);
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
statusGroups = {
|
||||||
|
UPCOMING: [],
|
||||||
|
DUE_SOON: [],
|
||||||
|
OVERDUE: [],
|
||||||
|
SUBMITTED: [],
|
||||||
|
MARKS_RELEASED: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
filteredAssessments.forEach((assessment) => {
|
||||||
|
const status = determineStatus(assessment);
|
||||||
|
if (statusGroups[status]) {
|
||||||
|
statusGroups[status].push(assessment);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDueDateClass(assessment: any): string {
|
||||||
|
const status = determineStatus(assessment);
|
||||||
|
switch (status) {
|
||||||
|
case "OVERDUE":
|
||||||
|
return "overdue";
|
||||||
|
case "DUE_SOON":
|
||||||
|
return "due-soon";
|
||||||
|
case "UPCOMING":
|
||||||
|
return "upcoming";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function markAssessmentCompleted(assessment: any) {
|
||||||
|
const completedKey = "betterseqta-completed-assessments";
|
||||||
|
const completed = JSON.parse(localStorage.getItem(completedKey) || "[]");
|
||||||
|
|
||||||
|
if (!completed.includes(assessment.id)) {
|
||||||
|
completed.push(assessment.id);
|
||||||
|
localStorage.setItem(completedKey, JSON.stringify(completed));
|
||||||
|
updateAssessments();
|
||||||
|
checkForCelebration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unmarkAssessmentCompleted(assessment: any) {
|
||||||
|
const completedKey = "betterseqta-completed-assessments";
|
||||||
|
const completed = JSON.parse(localStorage.getItem(completedKey) || "[]");
|
||||||
|
|
||||||
|
const index = completed.indexOf(assessment.id);
|
||||||
|
if (index > -1) {
|
||||||
|
completed.splice(index, 1);
|
||||||
|
localStorage.setItem(completedKey, JSON.stringify(completed));
|
||||||
|
updateAssessments();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkForCelebration() {
|
||||||
|
const overdueCount = statusGroups.OVERDUE?.length || 0;
|
||||||
|
const dueSoonCount = statusGroups.DUE_SOON?.length || 0;
|
||||||
|
|
||||||
|
if (overdueCount === 0 && dueSoonCount === 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
const duration = 100;
|
||||||
|
const end = Date.now() + duration;
|
||||||
|
|
||||||
|
(function frame() {
|
||||||
|
confetti({
|
||||||
|
particleCount: 17,
|
||||||
|
angle: 60,
|
||||||
|
spread: 65,
|
||||||
|
drift: 0.8,
|
||||||
|
startVelocity: 40,
|
||||||
|
scalar: 2,
|
||||||
|
gravity: 2,
|
||||||
|
decay: 0.97,
|
||||||
|
ticks: 300,
|
||||||
|
origin: { x: 0, y: 1 },
|
||||||
|
disableForReducedMotion: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
confetti({
|
||||||
|
particleCount: 17,
|
||||||
|
angle: 120,
|
||||||
|
spread: 65,
|
||||||
|
drift: -0.8,
|
||||||
|
startVelocity: 40,
|
||||||
|
scalar: 2,
|
||||||
|
decay: 0.97,
|
||||||
|
ticks: 300,
|
||||||
|
gravity: 2,
|
||||||
|
origin: { x: 1, y: 1 },
|
||||||
|
disableForReducedMotion: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Date.now() < end) {
|
||||||
|
requestAnimationFrame(frame);
|
||||||
|
}
|
||||||
|
}());
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Confetti celebration failed:", e);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
} else if (overdueCount === 0 || dueSoonCount === 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
confetti({
|
||||||
|
particleCount: 100,
|
||||||
|
spread: 70,
|
||||||
|
origin: { y: 0.6 },
|
||||||
|
scalar: 0.9,
|
||||||
|
disableForReducedMotion: true,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Confetti celebration failed:", e);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isManuallyCompleted(assessmentId: string): boolean {
|
||||||
|
const completedKey = "betterseqta-completed-assessments";
|
||||||
|
const completed = JSON.parse(localStorage.getItem(completedKey) || "[]");
|
||||||
|
return completed.includes(assessmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCardClick(assessment: any, event: Event) {
|
||||||
|
if ((event.target as HTMLElement).closest(".card-menu")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.location.hash = `#?page=/assessments/${assessment.programmeID}:${assessment.metaclassID}&item=${assessment.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let openMenuId: string | null = null;
|
||||||
|
|
||||||
|
function toggleMenu(assessmentId: string, event: Event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
openMenuId = openMenuId === assessmentId ? null : assessmentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeAllMenus() {
|
||||||
|
openMenuId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (data) {
|
||||||
|
updateAssessments();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
key: "UPCOMING",
|
||||||
|
title: "Upcoming",
|
||||||
|
className: "column-upcoming",
|
||||||
|
icon: "📅",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "DUE_SOON",
|
||||||
|
title: "Due Soon",
|
||||||
|
className: "column-due-soon",
|
||||||
|
icon: "⏰",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "OVERDUE",
|
||||||
|
title: "Overdue",
|
||||||
|
className: "column-overdue",
|
||||||
|
icon: "🚨",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "SUBMITTED",
|
||||||
|
title: "Submitted",
|
||||||
|
className: "column-submitted",
|
||||||
|
icon: "📝",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "MARKS_RELEASED",
|
||||||
|
title: "Marked",
|
||||||
|
className: "column-marked",
|
||||||
|
icon: "✅",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:click={closeAllMenus} />
|
||||||
|
|
||||||
|
<div id="grid-view-container">
|
||||||
|
<div class="grid-view-header">
|
||||||
|
<h1 class="grid-view-title">Assessments</h1>
|
||||||
|
<div class="grid-view-filters">
|
||||||
|
<select class="filter-select" bind:value={currentFilters.subject}>
|
||||||
|
<option value="all">All Subjects</option>
|
||||||
|
{#each data.subjects as subject}
|
||||||
|
<option value={subject.code}>{subject.code} - {subject.title}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<select class="filter-select" bind:value={currentFilters.sortBy}>
|
||||||
|
<option value="due">Sort by Due Date</option>
|
||||||
|
<option value="grade">Sort by Grade</option>
|
||||||
|
<option value="subject">Sort by Subject</option>
|
||||||
|
<option value="title">Sort by Title</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="main-grid-content">
|
||||||
|
{#if filteredAssessments.length === 0}
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="empty-icon">📋</div>
|
||||||
|
<p>No assessments found matching your filters</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="kanban-board">
|
||||||
|
{#each columns as column}
|
||||||
|
{#if statusGroups[column.key]?.length > 0}
|
||||||
|
<div class="kanban-column-parent">
|
||||||
|
<div class="kanban-column {column.className}">
|
||||||
|
<div class="column-header">
|
||||||
|
<div class="column-title">
|
||||||
|
{column.icon} {column.title}
|
||||||
|
<span class="column-count">{statusGroups[column.key].length}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column-cards" id="{column.key.toLowerCase()}-cards">
|
||||||
|
{#each statusGroups[column.key] as assessment}
|
||||||
|
{@const status = determineStatus(assessment)}
|
||||||
|
{@const dueDateClass = getDueDateClass(assessment)}
|
||||||
|
{@const isCompleted = isManuallyCompleted(assessment.id)}
|
||||||
|
{@const color = data.colors[assessment.code] || "#6366f1"}
|
||||||
|
<div
|
||||||
|
class="assessment-card"
|
||||||
|
data-subject={assessment.code}
|
||||||
|
data-status={status}
|
||||||
|
style="--subject-color: {color}"
|
||||||
|
on:click={(e) => handleCardClick(assessment, e)}
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
on:keydown={(e) => e.key === 'Enter' && handleCardClick(assessment, e)}
|
||||||
|
>
|
||||||
|
<div class="card-labels">
|
||||||
|
<span class="card-label label-subject">{assessment.code}</span>
|
||||||
|
{#if assessment.submitted}
|
||||||
|
<span class="card-label label-submitted" style="background: #10b981; color: white;">Submitted</span>
|
||||||
|
{/if}
|
||||||
|
{#if isCompleted && status === "MARKS_RELEASED" && !assessment.results}
|
||||||
|
<span class="card-label label-completed" style="background: #059669; color: white;">Completed</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if status !== "MARKS_RELEASED" || isCompleted}
|
||||||
|
<div class="card-menu">
|
||||||
|
<button
|
||||||
|
class="menu-button"
|
||||||
|
data-assessment-id={assessment.id}
|
||||||
|
on:click={(e) => toggleMenu(assessment.id, e)}
|
||||||
|
aria-label="Open menu"
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<circle cx="12" cy="5" r="2"/>
|
||||||
|
<circle cx="12" cy="12" r="2"/>
|
||||||
|
<circle cx="12" cy="19" r="2"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="menu-dropdown" style="display: {openMenuId === assessment.id ? 'block' : 'none'};">
|
||||||
|
{#if status !== "MARKS_RELEASED"}
|
||||||
|
<button class="menu-item mark-completed" on:click={() => markAssessmentCompleted(assessment)}>
|
||||||
|
Mark as Completed
|
||||||
|
</button>
|
||||||
|
{:else if isCompleted}
|
||||||
|
<button class="menu-item mark-not-completed" on:click={() => unmarkAssessmentCompleted(assessment)}>
|
||||||
|
Mark as Not Complete
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<h3 class="assessment-title">{assessment.title}</h3>
|
||||||
|
|
||||||
|
{#if !assessment.results && !isCompleted}
|
||||||
|
<div class="assessment-meta">
|
||||||
|
<div class="due-date {dueDateClass}">
|
||||||
|
📅 {formatDate(assessment.due, assessment.submitted)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if assessment.results}
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="Thermoscore__Thermoscore___WFpL3" style="--fill-colour: {color}">
|
||||||
|
<div style="width: {assessment.results.percentage}%" class="Thermoscore__fill___ojxDI">
|
||||||
|
<div title="{assessment.results.percentage}%" class="Thermoscore__text___XSR_M">
|
||||||
|
{(() => {
|
||||||
|
const allSettings = settingsState.getAll() as unknown as any;
|
||||||
|
const letterGradeSetting = allSettings["plugin.assessments-average.settings"]?.lettergrade;
|
||||||
|
return letterGradeSetting
|
||||||
|
? percentageToLetter(assessment.results.percentage)
|
||||||
|
: `${assessment.results.percentage}%`;
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let error: string;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="error-container">
|
||||||
|
<p class="error-text">Failed to load assessments</p>
|
||||||
|
<p style="color: #94a3b8; font-size: 0.875rem;">{error}</p>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<div id="grid-view-container">
|
||||||
|
<div class="grid-view-header">
|
||||||
|
<h1 class="grid-view-title">Assessments</h1>
|
||||||
|
<div class="grid-view-filters">
|
||||||
|
<select class="filter-select" disabled>
|
||||||
|
<option value="all">Loading subjects...</option>
|
||||||
|
</select>
|
||||||
|
<select class="filter-select" disabled>
|
||||||
|
<option value="due">Sort by Due Date</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="main-grid-content">
|
||||||
|
<div class="kanban-board">
|
||||||
|
{#each columns as column}
|
||||||
|
<div class="kanban-column-parent">
|
||||||
|
<div class="kanban-column {column.className}">
|
||||||
|
<div class="column-header">
|
||||||
|
<div class="column-title">
|
||||||
|
{column.icon} {column.title}
|
||||||
|
<span class="column-count">...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column-cards" id="{column.key.toLowerCase()}-cards">
|
||||||
|
{#each Array(column.skeletonCount) as _}
|
||||||
|
<div class="assessment-card">
|
||||||
|
<div class="skeleton-element skeleton-label"></div>
|
||||||
|
<div class="skeleton-element skeleton-title"></div>
|
||||||
|
<div class="skeleton-element skeleton-title-line2"></div>
|
||||||
|
<div class="skeleton-element skeleton-meta"></div>
|
||||||
|
{#if column.key === "MARKS_RELEASED"}
|
||||||
|
<div class="skeleton-footer">
|
||||||
|
<div class="skeleton-element" style="height: 16px; width: 100%;"></div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
key: "UPCOMING",
|
||||||
|
title: "Upcoming",
|
||||||
|
className: "column-upcoming",
|
||||||
|
icon: "📅",
|
||||||
|
skeletonCount: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "DUE_SOON",
|
||||||
|
title: "Due Soon",
|
||||||
|
className: "column-due-soon",
|
||||||
|
icon: "⏰",
|
||||||
|
skeletonCount: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "OVERDUE",
|
||||||
|
title: "Overdue",
|
||||||
|
className: "column-overdue",
|
||||||
|
icon: "🚨",
|
||||||
|
skeletonCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "MARKS_RELEASED",
|
||||||
|
title: "Marked",
|
||||||
|
className: "column-marked",
|
||||||
|
icon: "✅",
|
||||||
|
skeletonCount: 4,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
@@ -1,619 +1,45 @@
|
|||||||
import { determineStatus, formatDate, getGradeValue } from "./utils";
|
import renderSvelte from "@/interface/main";
|
||||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
import AssessmentsOverview from "./AssessmentsOverview.svelte";
|
||||||
|
import SkeletonLoader from "./SkeletonLoader.svelte";
|
||||||
|
import ErrorState from "./ErrorState.svelte";
|
||||||
|
import { unmount } from "svelte";
|
||||||
|
|
||||||
function percentageToLetter(percentage: number): string {
|
let currentApp: any = null;
|
||||||
const letterMap: Record<number, string> = {
|
|
||||||
100: "A+",
|
|
||||||
95: "A",
|
|
||||||
90: "A-",
|
|
||||||
85: "B+",
|
|
||||||
80: "B",
|
|
||||||
75: "B-",
|
|
||||||
70: "C+",
|
|
||||||
65: "C",
|
|
||||||
60: "C-",
|
|
||||||
55: "D+",
|
|
||||||
50: "D",
|
|
||||||
45: "D-",
|
|
||||||
40: "E+",
|
|
||||||
35: "E",
|
|
||||||
30: "E-",
|
|
||||||
0: "F",
|
|
||||||
};
|
|
||||||
|
|
||||||
const rounded = Math.ceil(percentage / 5) * 5;
|
|
||||||
return letterMap[rounded] || "F";
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FilterOptions {
|
|
||||||
subject: string;
|
|
||||||
sortBy: "due" | "grade" | "subject" | "title";
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentFilters: FilterOptions = {
|
|
||||||
subject: "all",
|
|
||||||
sortBy: "due",
|
|
||||||
};
|
|
||||||
|
|
||||||
export function renderGrid(container: HTMLElement, data: any) {
|
export function renderGrid(container: HTMLElement, data: any) {
|
||||||
|
if (currentApp) {
|
||||||
|
unmount(currentApp);
|
||||||
|
}
|
||||||
|
|
||||||
container.innerHTML = "";
|
container.innerHTML = "";
|
||||||
container.className = "";
|
container.className = "";
|
||||||
container.id = "grid-view-container";
|
|
||||||
|
|
||||||
const header = document.createElement("div");
|
currentApp = renderSvelte(AssessmentsOverview, container, { data });
|
||||||
header.className = "grid-view-header";
|
|
||||||
header.innerHTML = `
|
|
||||||
<h1 class="grid-view-title">Assessments</h1>
|
|
||||||
<div class="grid-view-filters">
|
|
||||||
<select class="filter-select" id="subject-filter">
|
|
||||||
<option value="all">All Subjects</option>
|
|
||||||
${data.subjects.map((s: any) => `<option value="${s.code}">${s.code} - ${s.title}</option>`).join("")}
|
|
||||||
</select>
|
|
||||||
<select class="filter-select" id="sort-filter">
|
|
||||||
<option value="due">Sort by Due Date</option>
|
|
||||||
<option value="grade">Sort by Grade</option>
|
|
||||||
<option value="subject">Sort by Subject</option>
|
|
||||||
<option value="title">Sort by Title</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
container.appendChild(header);
|
|
||||||
|
|
||||||
const subjectFilter = header.querySelector(
|
|
||||||
"#subject-filter",
|
|
||||||
) as HTMLSelectElement;
|
|
||||||
const sortFilter = header.querySelector("#sort-filter") as HTMLSelectElement;
|
|
||||||
|
|
||||||
subjectFilter.addEventListener("change", () => {
|
|
||||||
currentFilters.subject = subjectFilter.value;
|
|
||||||
renderAssessments();
|
|
||||||
});
|
|
||||||
|
|
||||||
sortFilter.addEventListener("change", () => {
|
|
||||||
currentFilters.sortBy = sortFilter.value as any;
|
|
||||||
renderAssessments();
|
|
||||||
});
|
|
||||||
|
|
||||||
const mainContent = document.createElement("div");
|
|
||||||
mainContent.id = "main-grid-content";
|
|
||||||
container.appendChild(mainContent);
|
|
||||||
|
|
||||||
function renderAssessments() {
|
|
||||||
const contentArea = container.querySelector(
|
|
||||||
"#main-grid-content",
|
|
||||||
) as HTMLElement;
|
|
||||||
contentArea.innerHTML = "";
|
|
||||||
|
|
||||||
let filteredAssessments = data.assessments.filter((a: any) => {
|
|
||||||
const subjectMatch =
|
|
||||||
currentFilters.subject === "all" || a.code === currentFilters.subject;
|
|
||||||
return subjectMatch;
|
|
||||||
});
|
|
||||||
|
|
||||||
filteredAssessments.sort((a: any, b: any) => {
|
|
||||||
switch (currentFilters.sortBy) {
|
|
||||||
case "due":
|
|
||||||
return new Date(a.due).getTime() - new Date(b.due).getTime();
|
|
||||||
case "grade":
|
|
||||||
const gradeA = getGradeValue(a);
|
|
||||||
const gradeB = getGradeValue(b);
|
|
||||||
if (gradeA === null && gradeB === null) return 0;
|
|
||||||
if (gradeA === null) return 1;
|
|
||||||
if (gradeB === null) return -1;
|
|
||||||
return gradeB - gradeA;
|
|
||||||
case "subject":
|
|
||||||
return a.code.localeCompare(b.code);
|
|
||||||
case "title":
|
|
||||||
return a.title.localeCompare(b.title);
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (filteredAssessments.length === 0) {
|
|
||||||
contentArea.innerHTML = `
|
|
||||||
<div class="empty-state">
|
|
||||||
<div class="empty-icon">📋</div>
|
|
||||||
<p>No assessments found matching your filters</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderKanbanBoard(contentArea, filteredAssessments, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderKanbanBoard(
|
|
||||||
container: HTMLElement,
|
|
||||||
assessments: any[],
|
|
||||||
data: any,
|
|
||||||
) {
|
|
||||||
const statusGroups = {
|
|
||||||
UPCOMING: [] as any[],
|
|
||||||
DUE_SOON: [] as any[],
|
|
||||||
OVERDUE: [] as any[],
|
|
||||||
SUBMITTED: [] as any[],
|
|
||||||
MARKS_RELEASED: [] as any[],
|
|
||||||
};
|
|
||||||
|
|
||||||
assessments.forEach((assessment) => {
|
|
||||||
const status = determineStatus(assessment);
|
|
||||||
if (statusGroups[status as keyof typeof statusGroups]) {
|
|
||||||
statusGroups[status as keyof typeof statusGroups].push(assessment);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const board = document.createElement("div");
|
|
||||||
board.className = "kanban-board";
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
key: "UPCOMING",
|
|
||||||
title: "Upcoming",
|
|
||||||
className: "column-upcoming",
|
|
||||||
icon: "📅",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "DUE_SOON",
|
|
||||||
title: "Due Soon",
|
|
||||||
className: "column-due-soon",
|
|
||||||
icon: "⏰",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "OVERDUE",
|
|
||||||
title: "Overdue",
|
|
||||||
className: "column-overdue",
|
|
||||||
icon: "🚨",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "SUBMITTED",
|
|
||||||
title: "Submitted",
|
|
||||||
className: "column-submitted",
|
|
||||||
icon: "📝",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "MARKS_RELEASED",
|
|
||||||
title: "Marked",
|
|
||||||
className: "column-marked",
|
|
||||||
icon: "✅",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
columns.forEach((column) => {
|
|
||||||
const assessmentList =
|
|
||||||
statusGroups[column.key as keyof typeof statusGroups];
|
|
||||||
|
|
||||||
// Skip rendering empty columns
|
|
||||||
if (assessmentList.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const columnParentEl = document.createElement("div");
|
|
||||||
columnParentEl.className = "kanban-column-parent";
|
|
||||||
|
|
||||||
const columnEl = document.createElement("div");
|
|
||||||
columnEl.className = `kanban-column ${column.className}`;
|
|
||||||
|
|
||||||
columnEl.innerHTML = `
|
|
||||||
<div class="column-header">
|
|
||||||
<div class="column-title">
|
|
||||||
${column.icon} ${column.title}
|
|
||||||
<span class="column-count">${assessmentList.length}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column-cards" id="${column.key.toLowerCase()}-cards"></div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const cardsContainer = columnEl.querySelector(
|
|
||||||
`#${column.key.toLowerCase()}-cards`,
|
|
||||||
) as HTMLElement;
|
|
||||||
|
|
||||||
assessmentList.forEach((assessment) => {
|
|
||||||
cardsContainer.appendChild(
|
|
||||||
createKanbanCard(
|
|
||||||
assessment,
|
|
||||||
data.colors[assessment.code] || "#6366f1",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
columnParentEl.appendChild(columnEl);
|
|
||||||
board.appendChild(columnParentEl);
|
|
||||||
});
|
|
||||||
|
|
||||||
container.appendChild(board);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createKanbanCard(assessment: any, color: string): HTMLElement {
|
|
||||||
const status = determineStatus(assessment);
|
|
||||||
const dueDateClass = getDueDateClass(assessment);
|
|
||||||
|
|
||||||
const completedKey = "betterseqta-completed-assessments";
|
|
||||||
const completed = JSON.parse(localStorage.getItem(completedKey) || "[]");
|
|
||||||
const isManuallyCompleted = completed.includes(assessment.id);
|
|
||||||
|
|
||||||
const card = document.createElement("div");
|
|
||||||
card.className = "assessment-card";
|
|
||||||
card.dataset.subject = assessment.code;
|
|
||||||
card.dataset.status = status;
|
|
||||||
card.style.setProperty("--subject-color", color);
|
|
||||||
|
|
||||||
card.innerHTML = `
|
|
||||||
<div class="card-labels">
|
|
||||||
<span class="card-label label-subject">${assessment.code}</span>
|
|
||||||
${assessment.submitted ? '<span class="card-label label-submitted" style="background: #10b981; color: white;">Submitted</span>' : ""}
|
|
||||||
${isManuallyCompleted && status === "MARKS_RELEASED" && !assessment.results ? '<span class="card-label label-completed" style="background: #059669; color: white;">Completed</span>' : ""}
|
|
||||||
</div>
|
|
||||||
${
|
|
||||||
status !== "MARKS_RELEASED"
|
|
||||||
? `
|
|
||||||
<div class="card-menu">
|
|
||||||
<button class="menu-button" data-assessment-id="${assessment.id}">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<circle cx="12" cy="5" r="2"/>
|
|
||||||
<circle cx="12" cy="12" r="2"/>
|
|
||||||
<circle cx="12" cy="19" r="2"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<div class="menu-dropdown" style="display: none;">
|
|
||||||
<button class="menu-item mark-completed">Mark as Completed</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: isManuallyCompleted
|
|
||||||
? `
|
|
||||||
<div class="card-menu">
|
|
||||||
<button class="menu-button" data-assessment-id="${assessment.id}">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<circle cx="12" cy="5" r="2"/>
|
|
||||||
<circle cx="12" cy="12" r="2"/>
|
|
||||||
<circle cx="12" cy="19" r="2"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<div class="menu-dropdown" style="display: none;">
|
|
||||||
<button class="menu-item mark-not-completed">Mark as Not Complete</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
<h3 class="assessment-title">${assessment.title}</h3>
|
|
||||||
${
|
|
||||||
!assessment.results && !isManuallyCompleted
|
|
||||||
? `
|
|
||||||
<div class="assessment-meta">
|
|
||||||
<div class="due-date ${dueDateClass}">
|
|
||||||
📅 ${formatDate(assessment.due, assessment.submitted)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
${
|
|
||||||
assessment.results
|
|
||||||
? `
|
|
||||||
<div class="card-footer">
|
|
||||||
<div class="Thermoscore__Thermoscore___WFpL3" style="--fill-colour: ${color}">
|
|
||||||
<div style="width: ${assessment.results.percentage}%" class="Thermoscore__fill___ojxDI">
|
|
||||||
<div title="${assessment.results.percentage}%" class="Thermoscore__text___XSR_M">
|
|
||||||
${(() => {
|
|
||||||
const allSettings = settingsState.getAll() as unknown as any;
|
|
||||||
const letterGradeSetting =
|
|
||||||
allSettings["plugin.assessments-average.settings"]
|
|
||||||
?.lettergrade;
|
|
||||||
return letterGradeSetting
|
|
||||||
? percentageToLetter(assessment.results.percentage)
|
|
||||||
: `${assessment.results.percentage}%`;
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
card.addEventListener("click", (e) => {
|
|
||||||
if ((e.target as HTMLElement).closest(".card-menu")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window.location.hash = `#?page=/assessments/${assessment.programmeID}:${assessment.metaclassID}&item=${assessment.id}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (status !== "MARKS_RELEASED" || isManuallyCompleted) {
|
|
||||||
const menuButton = card.querySelector(
|
|
||||||
".menu-button",
|
|
||||||
) as HTMLButtonElement;
|
|
||||||
const menuDropdown = card.querySelector(".menu-dropdown") as HTMLElement;
|
|
||||||
const markCompletedBtn = card.querySelector(
|
|
||||||
".mark-completed",
|
|
||||||
) as HTMLButtonElement;
|
|
||||||
const markNotCompletedBtn = card.querySelector(
|
|
||||||
".mark-not-completed",
|
|
||||||
) as HTMLButtonElement;
|
|
||||||
|
|
||||||
menuButton?.addEventListener("click", (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
document.querySelectorAll(".menu-dropdown").forEach((dropdown) => {
|
|
||||||
if (dropdown !== menuDropdown) {
|
|
||||||
(dropdown as HTMLElement).style.display = "none";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
menuDropdown.style.display =
|
|
||||||
menuDropdown.style.display === "none" ? "block" : "none";
|
|
||||||
});
|
|
||||||
|
|
||||||
markCompletedBtn?.addEventListener("click", (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
markAssessmentCompleted(assessment);
|
|
||||||
menuDropdown.style.display = "none";
|
|
||||||
});
|
|
||||||
|
|
||||||
markNotCompletedBtn?.addEventListener("click", (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
unmarkAssessmentCompleted(assessment);
|
|
||||||
menuDropdown.style.display = "none";
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener("click", (e) => {
|
|
||||||
if (!card.contains(e.target as Node)) {
|
|
||||||
menuDropdown.style.display = "none";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return card;
|
|
||||||
}
|
|
||||||
|
|
||||||
function markAssessmentCompleted(assessment: any) {
|
|
||||||
const completedKey = "betterseqta-completed-assessments";
|
|
||||||
const completed = JSON.parse(localStorage.getItem(completedKey) || "[]");
|
|
||||||
|
|
||||||
if (!completed.includes(assessment.id)) {
|
|
||||||
completed.push(assessment.id);
|
|
||||||
localStorage.setItem(completedKey, JSON.stringify(completed));
|
|
||||||
|
|
||||||
updateAssessmentCard(assessment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function unmarkAssessmentCompleted(assessment: any) {
|
|
||||||
const completedKey = "betterseqta-completed-assessments";
|
|
||||||
const completed = JSON.parse(localStorage.getItem(completedKey) || "[]");
|
|
||||||
|
|
||||||
const index = completed.indexOf(assessment.id);
|
|
||||||
if (index > -1) {
|
|
||||||
completed.splice(index, 1);
|
|
||||||
localStorage.setItem(completedKey, JSON.stringify(completed));
|
|
||||||
|
|
||||||
updateAssessmentCard(assessment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateAssessmentCard(assessment: any) {
|
|
||||||
const existingCard = document
|
|
||||||
.querySelector(`[data-assessment-id="${assessment.id}"]`)
|
|
||||||
?.closest(".assessment-card") as HTMLElement;
|
|
||||||
if (!existingCard) return;
|
|
||||||
|
|
||||||
const newStatus = determineStatus(assessment);
|
|
||||||
const completedKey = "betterseqta-completed-assessments";
|
|
||||||
const completed = JSON.parse(localStorage.getItem(completedKey) || "[]");
|
|
||||||
const isManuallyCompleted = completed.includes(assessment.id);
|
|
||||||
|
|
||||||
const currentColumn = existingCard.closest(".column-cards") as HTMLElement;
|
|
||||||
const currentColumnId = currentColumn?.id;
|
|
||||||
const targetColumnId = `${newStatus.toLowerCase()}-cards`;
|
|
||||||
|
|
||||||
if (currentColumnId !== targetColumnId) {
|
|
||||||
const targetColumn = document.getElementById(targetColumnId);
|
|
||||||
if (targetColumn) {
|
|
||||||
existingCard.remove();
|
|
||||||
|
|
||||||
const newCard = createKanbanCard(
|
|
||||||
assessment,
|
|
||||||
data.colors[assessment.code] || "#6366f1",
|
|
||||||
);
|
|
||||||
targetColumn.appendChild(newCard);
|
|
||||||
|
|
||||||
updateColumnCounts();
|
|
||||||
|
|
||||||
const emptyState = targetColumn.querySelector(".empty-column");
|
|
||||||
if (emptyState) {
|
|
||||||
emptyState.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentColumn && currentColumn.children.length === 0) {
|
|
||||||
const columnKey = currentColumnId
|
|
||||||
?.replace("-cards", "")
|
|
||||||
.toUpperCase();
|
|
||||||
const columnInfo = getColumnInfo(columnKey);
|
|
||||||
if (columnInfo) {
|
|
||||||
currentColumn.innerHTML = `
|
|
||||||
<div class="empty-column">
|
|
||||||
<div class="empty-icon">${columnInfo.icon}</div>
|
|
||||||
<p>No ${columnInfo.title.toLowerCase()} assessments</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const newCard = createKanbanCard(
|
|
||||||
assessment,
|
|
||||||
data.colors[assessment.code] || "#6366f1",
|
|
||||||
);
|
|
||||||
existingCard.replaceWith(newCard);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateColumnCounts() {
|
|
||||||
document.querySelectorAll(".column-count").forEach((countEl) => {
|
|
||||||
const column = countEl.closest(".kanban-column");
|
|
||||||
const cardsContainer = column?.querySelector(".column-cards");
|
|
||||||
const cardCount =
|
|
||||||
cardsContainer?.querySelectorAll(".assessment-card").length || 0;
|
|
||||||
countEl.textContent = cardCount.toString();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getColumnInfo(columnKey: string | undefined) {
|
|
||||||
const columns = {
|
|
||||||
UPCOMING: { title: "Upcoming", icon: "📅" },
|
|
||||||
DUE_SOON: { title: "Due Soon", icon: "⏰" },
|
|
||||||
OVERDUE: { title: "Overdue", icon: "🚨" },
|
|
||||||
SUBMITTED: { title: "Submitted", icon: "📝" },
|
|
||||||
MARKS_RELEASED: { title: "Marked", icon: "✅" },
|
|
||||||
};
|
|
||||||
return columnKey ? columns[columnKey as keyof typeof columns] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDueDateClass(assessment: any): string {
|
|
||||||
const status = determineStatus(assessment);
|
|
||||||
switch (status) {
|
|
||||||
case "OVERDUE":
|
|
||||||
return "overdue";
|
|
||||||
case "DUE_SOON":
|
|
||||||
return "due-soon";
|
|
||||||
case "UPCOMING":
|
|
||||||
return "upcoming";
|
|
||||||
default:
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderAssessments();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderSkeletonLoader(container: HTMLElement) {
|
export function renderSkeletonLoader(container: HTMLElement) {
|
||||||
|
if (currentApp) {
|
||||||
|
unmount(currentApp);
|
||||||
|
}
|
||||||
|
|
||||||
container.innerHTML = "";
|
container.innerHTML = "";
|
||||||
container.className = "";
|
container.className = "";
|
||||||
container.id = "grid-view-container";
|
|
||||||
|
|
||||||
const header = document.createElement("div");
|
currentApp = renderSvelte(SkeletonLoader, container);
|
||||||
header.className = "grid-view-header";
|
|
||||||
header.innerHTML = `
|
|
||||||
<h1 class="grid-view-title">Assessments</h1>
|
|
||||||
<div class="grid-view-filters">
|
|
||||||
<select class="filter-select" id="subject-filter" disabled>
|
|
||||||
<option value="all">Loading subjects...</option>
|
|
||||||
</select>
|
|
||||||
<select class="filter-select" id="sort-filter" disabled>
|
|
||||||
<option value="due">Sort by Due Date</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
container.appendChild(header);
|
|
||||||
|
|
||||||
const mainContent = document.createElement("div");
|
|
||||||
mainContent.id = "main-grid-content";
|
|
||||||
container.appendChild(mainContent);
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
key: "UPCOMING",
|
|
||||||
title: "Upcoming",
|
|
||||||
className: "column-upcoming",
|
|
||||||
icon: "📅",
|
|
||||||
skeletonCount: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "DUE_SOON",
|
|
||||||
title: "Due Soon",
|
|
||||||
className: "column-due-soon",
|
|
||||||
icon: "⏰",
|
|
||||||
skeletonCount: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "OVERDUE",
|
|
||||||
title: "Overdue",
|
|
||||||
className: "column-overdue",
|
|
||||||
icon: "🚨",
|
|
||||||
skeletonCount: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "MARKS_RELEASED",
|
|
||||||
title: "Marked",
|
|
||||||
className: "column-marked",
|
|
||||||
icon: "✅",
|
|
||||||
skeletonCount: 4,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const board = document.createElement("div");
|
|
||||||
board.className = "kanban-board";
|
|
||||||
|
|
||||||
columns.forEach((column) => {
|
|
||||||
const columnParentEl = document.createElement("div");
|
|
||||||
columnParentEl.className = "kanban-column-parent";
|
|
||||||
|
|
||||||
const columnEl = document.createElement("div");
|
|
||||||
columnEl.className = `kanban-column ${column.className}`;
|
|
||||||
|
|
||||||
columnEl.innerHTML = `
|
|
||||||
<div class="column-header">
|
|
||||||
<div class="column-title">
|
|
||||||
${column.icon} ${column.title}
|
|
||||||
<span class="column-count">...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column-cards" id="${column.key.toLowerCase()}-cards"></div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const cardsContainer = columnEl.querySelector(
|
|
||||||
`#${column.key.toLowerCase()}-cards`,
|
|
||||||
) as HTMLElement;
|
|
||||||
|
|
||||||
for (let i = 0; i < column.skeletonCount; i++) {
|
|
||||||
cardsContainer.appendChild(
|
|
||||||
createSkeletonCard(column.key === "MARKS_RELEASED"),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
columnParentEl.appendChild(columnEl);
|
|
||||||
board.appendChild(columnParentEl);
|
|
||||||
});
|
|
||||||
|
|
||||||
mainContent.appendChild(board);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createSkeletonCard(footer: boolean = false): HTMLElement {
|
|
||||||
const card = document.createElement("div");
|
|
||||||
card.className = "assessment-card";
|
|
||||||
|
|
||||||
card.innerHTML = `
|
|
||||||
<div class="skeleton-element skeleton-label"></div>
|
|
||||||
<div class="skeleton-element skeleton-title"></div>
|
|
||||||
<div class="skeleton-element skeleton-title-line2"></div>
|
|
||||||
<div class="skeleton-element skeleton-meta"></div>
|
|
||||||
${
|
|
||||||
footer
|
|
||||||
? `
|
|
||||||
<div class="skeleton-footer">
|
|
||||||
<div class="skeleton-element" style="height: 16px; width: 100%;"></div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
return card;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function renderLoadingState(container: HTMLElement) {
|
export function renderLoadingState(container: HTMLElement) {
|
||||||
renderSkeletonLoader(container);
|
renderSkeletonLoader(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderErrorState(container: HTMLElement, error: string) {
|
export function renderErrorState(container: HTMLElement, error: string) {
|
||||||
container.innerHTML = `
|
if (currentApp) {
|
||||||
<div class="error-container">
|
unmount(currentApp);
|
||||||
<p class="error-text">Failed to load assessments</p>
|
}
|
||||||
<p style="color: #94a3b8; font-size: 0.875rem;">${error}</p>
|
|
||||||
</div>
|
container.innerHTML = "";
|
||||||
`;
|
container.className = "";
|
||||||
|
|
||||||
|
currentApp = renderSvelte(ErrorState, container, { error });
|
||||||
}
|
}
|
||||||
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
declare module 'canvas-confetti' {
|
||||||
|
const confetti: any;
|
||||||
|
export default confetti;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user