diff --git a/package.json b/package.json index 49c30075..5a06a546 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "@uiw/codemirror-extensions-color": "^4.23.10", "@uiw/codemirror-theme-github": "^4.23.10", "autoprefixer": "^10.4.21", + "canvas-confetti": "^1.9.3", "codemirror": "^6.0.1", "color": "^5.0.0", "dompurify": "^3.2.4", diff --git a/src/interface/main.ts b/src/interface/main.ts index 65028a1b..d3f1e4ee 100644 --- a/src/interface/main.ts +++ b/src/interface/main.ts @@ -15,9 +15,11 @@ export default function renderSvelte( }, }); - const styleElement = document.createElement("style"); - styleElement.textContent = style; - mountPoint.appendChild(styleElement); + if (mountPoint instanceof ShadowRoot) { + const styleElement = document.createElement("style"); + styleElement.textContent = style; + mountPoint.appendChild(styleElement); + } return app; } diff --git a/src/plugins/built-in/assessmentsOverview/AssessmentsOverview.svelte b/src/plugins/built-in/assessmentsOverview/AssessmentsOverview.svelte new file mode 100644 index 00000000..56c44bcf --- /dev/null +++ b/src/plugins/built-in/assessmentsOverview/AssessmentsOverview.svelte @@ -0,0 +1,384 @@ + + + + +
+
+

Assessments

+
+ + +
+
+ +
+ {#if filteredAssessments.length === 0} +
+
📋
+

No assessments found matching your filters

+
+ {:else} +
+ {#each columns as column} + {#if statusGroups[column.key]?.length > 0} +
+
+
+
+ {column.icon} {column.title} + {statusGroups[column.key].length} +
+
+
+ {#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"} +
handleCardClick(assessment, e)} + role="button" + tabindex="0" + on:keydown={(e) => e.key === 'Enter' && handleCardClick(assessment, e)} + > +
+ {assessment.code} + {#if assessment.submitted} + + {/if} + {#if isCompleted && status === "MARKS_RELEASED" && !assessment.results} + Completed + {/if} +
+ + {#if status !== "MARKS_RELEASED" || isCompleted} +
+ + +
+ {/if} + +

{assessment.title}

+ + {#if !assessment.results && !isCompleted} +
+
+ 📅 {formatDate(assessment.due, assessment.submitted)} +
+
+ {/if} + + {#if assessment.results} + + {/if} +
+ {/each} +
+
+
+ {/if} + {/each} +
+ {/if} +
+
\ No newline at end of file diff --git a/src/plugins/built-in/assessmentsOverview/ErrorState.svelte b/src/plugins/built-in/assessmentsOverview/ErrorState.svelte new file mode 100644 index 00000000..1f07d7d7 --- /dev/null +++ b/src/plugins/built-in/assessmentsOverview/ErrorState.svelte @@ -0,0 +1,8 @@ + + +
+

Failed to load assessments

+

{error}

+
\ No newline at end of file diff --git a/src/plugins/built-in/assessmentsOverview/SkeletonLoader.svelte b/src/plugins/built-in/assessmentsOverview/SkeletonLoader.svelte new file mode 100644 index 00000000..e45c3d7b --- /dev/null +++ b/src/plugins/built-in/assessmentsOverview/SkeletonLoader.svelte @@ -0,0 +1,78 @@ +
+
+

Assessments

+
+ + +
+
+ +
+
+ {#each columns as column} +
+
+
+
+ {column.icon} {column.title} + ... +
+
+
+ {#each Array(column.skeletonCount) as _} +
+
+
+
+
+ {#if column.key === "MARKS_RELEASED"} + + {/if} +
+ {/each} +
+
+
+ {/each} +
+
+
+ + \ No newline at end of file diff --git a/src/plugins/built-in/assessmentsOverview/ui.ts b/src/plugins/built-in/assessmentsOverview/ui.ts index 3ac09cab..d2bdae5a 100644 --- a/src/plugins/built-in/assessmentsOverview/ui.ts +++ b/src/plugins/built-in/assessmentsOverview/ui.ts @@ -1,619 +1,45 @@ -import { determineStatus, formatDate, getGradeValue } from "./utils"; -import { settingsState } from "@/seqta/utils/listeners/SettingsState"; +import renderSvelte from "@/interface/main"; +import AssessmentsOverview from "./AssessmentsOverview.svelte"; +import SkeletonLoader from "./SkeletonLoader.svelte"; +import ErrorState from "./ErrorState.svelte"; +import { unmount } from "svelte"; -function percentageToLetter(percentage: number): string { - const letterMap: Record = { - 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", -}; +let currentApp: any = null; export function renderGrid(container: HTMLElement, data: any) { + if (currentApp) { + unmount(currentApp); + } + container.innerHTML = ""; container.className = ""; - container.id = "grid-view-container"; - - const header = document.createElement("div"); - header.className = "grid-view-header"; - header.innerHTML = ` -

Assessments

-
- - -
- `; - - 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 = ` -
-
📋
-

No assessments found matching your filters

-
- `; - 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 = ` -
-
- ${column.icon} ${column.title} - ${assessmentList.length} -
-
-
- `; - - 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 = ` -
- ${assessment.code} - ${assessment.submitted ? '' : ""} - ${isManuallyCompleted && status === "MARKS_RELEASED" && !assessment.results ? 'Completed' : ""} -
- ${ - status !== "MARKS_RELEASED" - ? ` -
- - -
- ` - : isManuallyCompleted - ? ` -
- - -
- ` - : "" - } -

${assessment.title}

- ${ - !assessment.results && !isManuallyCompleted - ? ` -
-
- 📅 ${formatDate(assessment.due, assessment.submitted)} -
-
- ` - : "" - } - ${ - assessment.results - ? ` - - ` - : "" - } - `; - - 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 = ` -
-
${columnInfo.icon}
-

No ${columnInfo.title.toLowerCase()} assessments

-
- `; - } - } - } - } 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(); + + currentApp = renderSvelte(AssessmentsOverview, container, { data }); } export function renderSkeletonLoader(container: HTMLElement) { + if (currentApp) { + unmount(currentApp); + } + container.innerHTML = ""; container.className = ""; - container.id = "grid-view-container"; - - const header = document.createElement("div"); - header.className = "grid-view-header"; - header.innerHTML = ` -

Assessments

-
- - -
- `; - - 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 = ` -
-
- ${column.icon} ${column.title} - ... -
-
-
- `; - - 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); + + currentApp = renderSvelte(SkeletonLoader, container); } -function createSkeletonCard(footer: boolean = false): HTMLElement { - const card = document.createElement("div"); - card.className = "assessment-card"; - - card.innerHTML = ` -
-
-
-
- ${ - footer - ? ` - - ` - : "" - } - `; - - return card; -} export function renderLoadingState(container: HTMLElement) { renderSkeletonLoader(container); } export function renderErrorState(container: HTMLElement, error: string) { - container.innerHTML = ` -
-

Failed to load assessments

-

${error}

-
- `; -} + if (currentApp) { + unmount(currentApp); + } + + container.innerHTML = ""; + container.className = ""; + + currentApp = renderSvelte(ErrorState, container, { error }); +} \ No newline at end of file diff --git a/src/types/canvas-confetti.d.ts b/src/types/canvas-confetti.d.ts new file mode 100644 index 00000000..16d21e0d --- /dev/null +++ b/src/types/canvas-confetti.d.ts @@ -0,0 +1,4 @@ +declare module 'canvas-confetti' { + const confetti: any; + export default confetti; +} \ No newline at end of file