feat: add confetti and render overview with svelte

This commit is contained in:
SethBurkart123
2025-06-13 09:06:56 +10:00
parent bb7c27dfea
commit 2292585e60
7 changed files with 507 additions and 604 deletions
+1
View File
@@ -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",
+5 -3
View File
@@ -15,9 +15,11 @@ export default function renderSvelte(
}, },
}); });
const styleElement = document.createElement("style"); if (mountPoint instanceof ShadowRoot) {
styleElement.textContent = style; const styleElement = document.createElement("style");
mountPoint.appendChild(styleElement); styleElement.textContent = style;
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>
+24 -598
View File
@@ -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 });
} }
+4
View File
@@ -0,0 +1,4 @@
declare module 'canvas-confetti' {
const confetti: any;
export default confetti;
}