diff --git a/src/css/injected.scss b/src/css/injected.scss index 6721c53b..c12ec16e 100644 --- a/src/css/injected.scss +++ b/src/css/injected.scss @@ -1175,11 +1175,6 @@ div > ol:has(.uiFileHandlerWrapper) { font-size: 20px; font-weight: 400; } -.notices-container h2 { - margin: 20px; - font-size: 20px; - font-weight: 400; -} .notice { position: relative; padding: 20px; @@ -3543,3 +3538,400 @@ body { -ms-overflow-style: none; scrollbar-width: none !important; } + +.notice-modal-content { + border: none !important; +} + +.notice-unified-content.notice-modal-state { + border: none !important; +} + +// Notice card hover effects for main page cards +.notice-unified-content.notice-card-state:not([data-transitioning]) { + cursor: pointer; + + &:hover { + background: var(--background-secondary) !important; + border-color: rgba(255, 255, 255, 0.2) !important; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15) !important; + } + + &:last-child { + margin-bottom: 0; + } +} + +.notice-badge { + padding: 4px 10px; + border-radius: 16px; + font-size: 12px; + font-weight: 500; + white-space: nowrap; +} + +.notice-staff { + font-size: 12px; + color: var(--text-secondary); + opacity: 0.7; +} + +.notice-preview { + font-size: 14px; + color: var(--text-secondary); + line-height: 1.4; + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; +} + +// Modal styles +.notice-modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; +} + +.notice-modal-transition { + position: fixed; + z-index: 10001; + transition: none; // Controlled by motion animations +} + +.notice-modal-content { + background: var(--background-primary); + border-radius: 16px; + max-width: 600px; + max-height: 80vh; + width: 100%; + overflow: hidden; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.1); + + &.notice-transitioning { + max-width: none; + max-height: none; + width: 100%; + height: 100%; + position: relative; + } +} + +.notice-unified-content { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--background-primary); + display: flex; + flex-direction: column; + padding: 16px; + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.notice-unified-content { + // Override any conflicting .notice styles - unified for both states + h1, h2, h3, h4, h5, h6 { + margin: 0 !important; + padding: 0 !important; + font-weight: inherit !important; + color: inherit !important; + text-shadow: none !important; + } + + .notice-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + flex-shrink: 0; + margin-bottom: 12px; + gap: 16px; + } + + .notice-content-title { + font-size: 20px !important; // Nice middle ground - not too big, not too small + font-weight: 600 !important; + color: var(--text-primary) !important; + margin: 0 0 12px 0 !important; + line-height: 1.3 !important; + flex-shrink: 0; + } + + .notice-content-body { + font-size: 14px !important; + color: var(--text-secondary) !important; + line-height: 1.5 !important; + margin: 0 !important; + flex: 1; + display: block; + // Force stable layout dimensions - content renders at full size always + min-width: 600px; // Ensure tables have consistent width for layout + width: 100%; + } + + // The ONLY difference between states is clipping! + &.notice-card-state { + .notice-content-body { + // Clip to show only 2 lines but keep full layout + overflow: hidden; + max-height: 3em; // ~2 lines worth of height + } + } + + &.notice-modal-state { + .notice-close-btn { + opacity: 1; + } + + .notice-content-body { + // Show full content with scrolling + overflow-y: auto; + + // Custom scrollbar for long content + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 3px; + } + + &::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.3); + } + + // Style content elements nicely + p { + margin-bottom: 12px; + + &:last-child { + margin-bottom: 0; + } + } + + a { + color: var(--theme-primary); + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + + ul, ol { + margin: 12px 0; + padding-left: 20px; + } + + li { + margin-bottom: 4px; + } + } + } + } + +.notice-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 16px; +} + +.notice-badge-row { + display: flex; + align-items: center; + gap: 12px; + flex: 1; +} + +.notice-close-btn { + position: absolute !important; + top: 12px; + right: 12px; + background: var(--background-secondary); + border: none; + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-size: 18px; + color: var(--text-primary); + transition: all 0.2s ease !important; + flex-shrink: 0; + opacity: 0; + + &:hover { + background: var(--background-tertiary); + transform: scale(1.1); + } +} + +.notice-modal-badge-row { + display: flex; + align-items: center; + gap: 12px; + flex: 1; +} + +.notice-modal-badge { + padding: 6px 12px; + border-radius: 20px; + font-size: 13px; + font-weight: 500; + white-space: nowrap; +} + +.notice-modal-staff { + font-size: 14px; + color: var(--text-secondary); + opacity: 0.8; +} + +.notice-modal-close { + background: var(--background-secondary); + border: none; + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-size: 18px; + color: var(--text-primary); + transition: all 0.2s ease; + flex-shrink: 0; + + &:hover { + background: var(--background-tertiary); + transform: scale(1.1); + } +} + +.notice-modal-title { + font-size: 24px; + font-weight: 600; + color: var(--text-primary); + margin: 16px 20px 20px 20px; + line-height: 1.3; + flex-shrink: 0; +} + +.notice-modal-body { + padding: 0 20px 20px 20px; + font-size: 15px; + line-height: 1.6; + color: var(--text-secondary); + flex: 1; + overflow-y: auto; + + // Custom scrollbar + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 3px; + } + + &::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.3); + } + + // Style content elements + p { + margin-bottom: 12px; + + &:last-child { + margin-bottom: 0; + } + } + + a { + color: var(--theme-primary); + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + + ul, ol { + margin: 12px 0; + padding-left: 20px; + } + + li { + margin-bottom: 4px; + } +} + +// Dark mode adjustments +.dark { + .notice-card { + border-color: rgba(255, 255, 255, 0.05); + + &:hover { + border-color: rgba(255, 255, 255, 0.1); + } + } + + .notice-modal-content { + border-color: rgba(255, 255, 255, 0.05); + } +} + +// Mobile responsiveness +@media (max-width: 768px) { + .notice-modal-overlay { + padding: 10px; + } + + .notice-modal-content { + max-height: 90vh; + } + + .notice-modal-title { + font-size: 20px; + margin: 12px 16px 16px 16px; + } + + .notice-modal-body { + padding: 0 16px 16px 16px; + } + + .notice-card { + padding: 12px; + } + + .notice-preview { + font-size: 13px; + } +} diff --git a/src/interface/pages/settings/general.svelte b/src/interface/pages/settings/general.svelte index 178caac9..1273d3e5 100644 --- a/src/interface/pages/settings/general.svelte +++ b/src/interface/pages/settings/general.svelte @@ -328,6 +328,18 @@ /> +
+
+

Mock Notices

+

Use fake notice data on homepage instead of real data

+
+
+ settingsState.mockNotices = isOn} + /> +
+
{/if} diff --git a/src/seqta/ui/dev/hideSensitiveContent.ts b/src/seqta/ui/dev/hideSensitiveContent.ts index d9199ff4..e3f6443a 100644 --- a/src/seqta/ui/dev/hideSensitiveContent.ts +++ b/src/seqta/ui/dev/hideSensitiveContent.ts @@ -370,8 +370,177 @@ const mockData = { "Mrs. Martinez", ], }, + noticesData: [ + { + id: 1, + title: "Academic Lunch Support", + contents: `The following table shows the names of the students who are required to attend at the beginning of lunchtime on the respective days.
+  + + + + + + + + + + + + + + + +
+

Monday 16/06
+ Room S201
+ Week A Mrs Thompson
+ Week B Mrs Smith

+

Wednesday 18/06
+ Room S201
+ Week A Mrs Smith
+ Week B Mrs Smith

+

Friday 20/06
+ Room M201 
+ Week A Ms Anderson
+ Week B Ms Anderson    

No Academic Support for year 9 and 10 
+ due to exam in P5/6
+
+
John Smith (Mrs Jones)
+ Wednesday 
+ Michael Brown
+ James Wilson (Miss Davis)
+
+  
Friday 20/6
+ Michael Brown
+  
+
+
`, + staff: "Mrs Jones", + colour: "#9c27b0", + label: 1, + label_title: "Middle & Senior School (5-12)" + }, + { + id: 2, + title: "Year 12 Study Period Changes", + contents: `Please note the following changes to Year 12 study periods for this week:

+ +
+Students are expected to bring all necessary materials and maintain academic focus during these sessions.`, + staff: "Mr. David Chen", + colour: "#2196f3", + label: 2, + label_title: "Year 12 Students" + }, + { + id: 3, + title: "Upcoming Science Fair Preparations", + contents: `The Annual Science Fair is scheduled for Friday, June 28th. All participating students should note:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
ActivityDateLocation
Project SetupThursday 27/06 - Period 5Main Hall
Practice PresentationsThursday 27/06 - Period 6Science Labs 1-3
Final EventFriday 28/06 - All DayMain Hall & Courtyard
+
Please ensure all safety protocols are followed and display materials are ready by Thursday afternoon.`, + staff: "Dr. Sarah Mitchell", + colour: "#4caf50", + label: 3, + label_title: "Science Students" + }, + { + id: 4, + title: "Library Resource Updates", + contents: `Our library has received several important updates this week:

+New Digital Resources: + +
+Facility Changes:
+The quiet study area has been expanded and now includes 8 additional desks with power outlets. Bookings can be made through the student portal under "Library Services". +

+For assistance with any digital resources, please contact the library staff during operating hours: 7:30 AM - 4:00 PM.`, + staff: "Ms. Rebecca Torres", + colour: "#ff9800", + label: 4, + label_title: "All Students" + }, + { + id: 5, + title: "Sports Carnival Team Registrations", + contents: `House Sports Carnival is approaching on August 15th! Team registrations are now open for all year levels.

+Available Events: +
+
+ Track Events: + +
+
+ Field Events: + +
+
+
+Registration Deadline: July 25th
+Training Sessions: Tuesdays & Thursdays, 3:30-4:30 PM
+
+Register through the PE department or see your house captains for more information.`, + staff: "Coach Michael Park", + colour: "#e91e63", + label: 5, + label_title: "All Houses" + } + ] }; +export function getMockNotices() { + return { + payload: mockData.noticesData + }; +} + export default function hideSensitiveContent() { Object.entries(contentConfig).forEach(([_, { selector, action }]) => { const elements = document.querySelectorAll(selector); diff --git a/src/seqta/utils/Loaders/LoadHomePage.ts b/src/seqta/utils/Loaders/LoadHomePage.ts index 8453c63a..75dcee2a 100644 --- a/src/seqta/utils/Loaders/LoadHomePage.ts +++ b/src/seqta/utils/Loaders/LoadHomePage.ts @@ -12,6 +12,7 @@ import stringToHTML from "../stringToHTML"; import { CreateCustomShortcutDiv } from "@/seqta/utils/CreateEnable/CreateCustomShortcutDiv"; import { CreateElement } from "@/seqta/utils/CreateEnable/CreateElement"; import { FilterUpcomingAssessments } from "@/seqta/utils/FilterUpcomingAssessments"; +import { getMockNotices } from "@/seqta/ui/dev/hideSensitiveContent"; let LessonInterval: any; let currentSelectedDate = new Date(); @@ -20,37 +21,29 @@ let loadingTimeout: any; export async function loadHomePage() { console.info("[BetterSEQTA+] Started Loading Home Page"); - // Reset currentSelectedDate to today when remounting the home page currentSelectedDate = new Date(); - // Wait for the DOM to finish clearing await delay(10); document.title = "Home ― SEQTA Learn"; const element = document.querySelector("[data-key=home]"); element?.classList.add("active"); - // Cache DOM queries const main = document.getElementById("main"); if (!main) { console.error("[BetterSEQTA+] Main element not found."); return; } - // Create root container first - const homeRoot = stringToHTML( - /* html */ `
`, - ); + const homeRoot = stringToHTML(`
`); - // Clear main and add home root main.innerHTML = ""; main.appendChild(homeRoot?.firstChild!); - // Get reference to home container for all subsequent additions const homeContainer = document.getElementById("home-root"); if (!homeContainer) return; - const skeletonStructure = stringToHTML(/* html */ ` + const skeletonStructure = stringToHTML(`
@@ -88,10 +81,8 @@ export async function loadHomePage() {
`); - // Add skeleton structure homeContainer.appendChild(skeletonStructure.firstChild!); - // Run animations if enabled if (settingsState.animations) { animate( ".home-container > div", @@ -106,10 +97,8 @@ export async function loadHomePage() { ); } - // Setup event listeners with cleanup const cleanup = setupTimetableListeners(); - // Initialize shortcuts immediately try { addShortcuts(settingsState.shortcuts); } catch (err: any) { @@ -117,13 +106,10 @@ export async function loadHomePage() { } AddCustomShortcutsToPage(); - // Get current date const date = new Date(); const TodayFormatted = formatDate(date); - // Start all data fetching in parallel const [timetablePromise, assessmentsPromise, classesPromise, prefsPromise] = [ - // Timetable data fetch(`${location.origin}/seqta/student/load/timetable?`, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -134,13 +120,10 @@ export async function loadHomePage() { }), }).then((res) => res.json()), - // Assessments data GetUpcomingAssessments(), - // Classes data GetActiveClasses(), - // Preferences data fetch(`${location.origin}/seqta/student/load/prefs?`, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -148,7 +131,6 @@ export async function loadHomePage() { }).then((res) => res.json()), ]; - // Process all data in parallel const [timetableData, assessments, classes, prefs] = await Promise.all([ timetablePromise, assessmentsPromise, @@ -156,7 +138,6 @@ export async function loadHomePage() { prefsPromise, ]); - // Process timetable data const dayContainer = document.getElementById("day-container"); if (dayContainer && timetableData.payload.items.length > 0) { const lessonArray = timetableData.payload.items.sort((a: any, b: any) => @@ -164,7 +145,6 @@ export async function loadHomePage() { ); const colours = await GetLessonColours(); - // Process and display lessons dayContainer.innerHTML = ""; for (let i = 0; i < lessonArray.length; i++) { const lesson = lessonArray[i]; @@ -196,7 +176,6 @@ export async function loadHomePage() { dayContainer.appendChild(div.firstChild!); } - // Check current lessons if (currentSelectedDate.getDate() === date.getDate()) { for (let i = 0; i < lessonArray.length; i++) { CheckCurrentLesson(lessonArray[i], i + 1); @@ -204,7 +183,7 @@ export async function loadHomePage() { CheckCurrentLessonAll(lessonArray); } } else if (dayContainer) { - dayContainer.innerHTML = /* html */ ` + dayContainer.innerHTML = `

No lessons available.

@@ -212,7 +191,6 @@ export async function loadHomePage() { } dayContainer?.classList.remove("loading"); - // Process assessments data const activeClass = classes.find((c: any) => c.hasOwnProperty("active")); const activeSubjects = activeClass?.subjects || []; const activeSubjectCodes = activeSubjects.map((s: any) => s.code); @@ -226,7 +204,6 @@ export async function loadHomePage() { upcomingItems.classList.remove("loading"); } - // Process notices data const labelArray = prefs.payload .filter((item: any) => item.name === "notices.filters") .map((item: any) => item.value); @@ -271,12 +248,10 @@ function setupTimetableListeners() { const timetableForward = document.getElementById("home-timetable-forward"); function changeTimetable(value: number) { - // Clear any existing loading timeout if (loadingTimeout) { clearTimeout(loadingTimeout); } - - // Only show loading state after 200ms to avoid flicker on fast connections + loadingTimeout = setTimeout(() => { const dayContainer = document.getElementById("day-container"); if (dayContainer) { @@ -284,7 +259,7 @@ function setupTimetableListeners() { dayContainer.innerHTML = ""; } }, 200); - + currentSelectedDate.setDate(currentSelectedDate.getDate() + value); const formattedDate = formatDate(currentSelectedDate); callHomeTimetable(formattedDate, true); @@ -340,19 +315,25 @@ function setupNotices(labelArray: string[], date: string) { ) as HTMLInputElement; const fetchNotices = async (date: string) => { - const response = await fetch( - `${location.origin}/seqta/student/load/notices?`, - { - method: "POST", - headers: { "Content-Type": "application/json; charset=utf-8" }, - body: JSON.stringify({ date }), - }, - ); - const data = await response.json(); + let data; + + if (settingsState.mockNotices) { + data = getMockNotices(); + } else { + const response = await fetch( + `${location.origin}/seqta/student/load/notices?`, + { + method: "POST", + headers: { "Content-Type": "application/json; charset=utf-8" }, + body: JSON.stringify({ date }), + }, + ); + data = await response.json(); + } + processNotices(data, labelArray); }; - // Debounce the input handler const debouncedInputChange = debounce((e: Event) => { const target = e.target as HTMLInputElement; fetchNotices(target.value); @@ -399,7 +380,6 @@ function processNotices(response: any, labelArray: string[]) { const NoticeContainer = document.getElementById("notice-container"); if (!NoticeContainer) return; - // Clear existing notices NoticeContainer.innerHTML = ""; const notices = response.payload; @@ -411,19 +391,20 @@ function processNotices(response: any, labelArray: string[]) { return; } - // Create document fragment for batch DOM updates const fragment = document.createDocumentFragment(); - // Process notices in batch notices.forEach((notice: any) => { - if (labelArray.includes(JSON.stringify(notice.label))) { + const shouldInclude = + settingsState.mockNotices || + labelArray.includes(JSON.stringify(notice.label)); + + if (shouldInclude) { const colour = processNoticeColor(notice.colour); const noticeElement = createNoticeElement(notice, colour); fragment.appendChild(noticeElement); } }); - // Single DOM update NoticeContainer.appendChild(fragment); } @@ -438,137 +419,413 @@ function processNoticeColor(colour: string): string | undefined { } function createNoticeElement(notice: any, colour: string | undefined): Node { - const htmlContent = ` -
-

${notice.title}

- ${notice.label_title !== undefined ? `
${notice.label_title}
` : ""} -
${notice.staff}
- ${notice.contents.replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, "").replace(/ +/, " ")} -
-
`; + const cleanContent = notice.contents + .replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, "") + .replace(/ +/, " "); + const noticeId = `notice-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - const element = stringToHTML(htmlContent).firstChild; - if (element instanceof HTMLElement) { - element.style.setProperty("--colour", colour ?? ""); + const htmlContent = ` +
+
+
+ + ${notice.label_title || "General"} + + ${notice.staff} +
+ +
+

${notice.title}

+
${cleanContent}
+
`; + + const element = stringToHTML(htmlContent).firstChild as HTMLElement; + if (element) { + element.addEventListener("click", () => + openNoticeModal(notice, colour, element), + ); } return element!; } +function openNoticeModal( + notice: any, + colour: string | undefined, + sourceElement: HTMLElement, +) { + const cleanContent = notice.contents + .replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, "") + .replace(/ +/, " "); + + const existingModal = document.getElementById("notice-modal"); + if (existingModal) { + existingModal.remove(); + } + + const sourceRect = sourceElement.getBoundingClientRect(); + let scrollY = Math.round(window.scrollY); + let scrollX = Math.round(window.scrollX); + + let sourceLeft = sourceRect.left; + let sourceTop = sourceRect.top; + let sourceWidth = sourceRect.width; + let sourceHeight = sourceRect.height; + + const modalHtml = ` +
+
+
+
+
+
+ + ${notice.label_title || "General"} + + ${notice.staff} +
+ +
+

${notice.title}

+
${cleanContent}
+
+
+
+
`; + + const modal = stringToHTML(modalHtml).firstChild as HTMLElement; + const transitionContainer = modal.querySelector( + ".notice-modal-transition", + ) as HTMLElement; + const unifiedContent = modal.querySelector( + ".notice-unified-content", + ) as HTMLElement; + const closeBtn = modal.querySelector(".notice-close-btn") as HTMLElement; + + document.body.appendChild(modal); + + sourceElement.setAttribute("data-transitioning", "true"); + sourceElement.style.opacity = "0"; + sourceElement.style.transform = "scale(0.95)"; + + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + let targetWidth = Math.round( + Math.min(Math.max(sourceWidth, 800), viewportWidth - 40), + ); + + const tempMeasureDiv = document.createElement("div"); + tempMeasureDiv.style.position = "absolute"; + tempMeasureDiv.style.left = "-9999px"; + tempMeasureDiv.style.width = targetWidth + "px"; + tempMeasureDiv.style.visibility = "hidden"; + tempMeasureDiv.innerHTML = ` +
+
+
+ ${notice.label_title || "General"} + ${notice.staff} +
+ +
+

${notice.title}

+
${cleanContent}
+
+ `; + document.body.appendChild(tempMeasureDiv); + const measuredHeight = + tempMeasureDiv.firstElementChild!.getBoundingClientRect().height; + document.body.removeChild(tempMeasureDiv); + + let targetHeight = Math.round( + Math.min(Math.max(measuredHeight, 200), viewportHeight * 0.85), + ); + + let targetLeft = Math.round((viewportWidth - targetWidth) / 2); + let targetTop = Math.round((viewportHeight - targetHeight) / 2) + scrollY; + + const closeModal = () => { + window.removeEventListener("resize", handleResize); + document.removeEventListener("keydown", handleEscape); + + if (!settingsState.animations) { + modal.remove(); + sourceElement.style.opacity = "1"; + sourceElement.style.transform = ""; + sourceElement.removeAttribute("data-transitioning"); + return; + } + + animate( + modal, + { + backgroundColor: ["rgba(0, 0, 0, 0.5)", "rgba(0, 0, 0, 0)"], + backdropFilter: ["blur(4px)", "blur(0px)"], + }, + { duration: 0.2 }, + ); + + animate( + transitionContainer, + { opacity: [1, 0] }, + { duration: 0.2, delay: 0.3 }, + ); + + sourceElement.style.opacity = "1"; + sourceElement.style.transform = ""; + + modal.style.pointerEvents = "none"; + + animate( + transitionContainer, + { + left: [targetLeft + scrollX, sourceLeft + scrollX], + top: [targetTop, sourceTop + scrollY], + width: [targetWidth, sourceWidth], + height: [targetHeight, sourceHeight], + scale: [1, 1], + }, + { + duration: 0.35, + type: "spring", + stiffness: 400, + damping: 35, + }, + ).finished.then(async () => { + modal.remove(); + sourceElement.removeAttribute("data-transitioning"); + }); + }; + + closeBtn?.addEventListener("click", closeModal); + modal?.addEventListener("click", (e) => { + if (e.target === modal) { + closeModal(); + } + }); + + const handleEscape = (e: KeyboardEvent) => { + if (e.key === "Escape") { + closeModal(); + document.removeEventListener("keydown", handleEscape); + window.removeEventListener("resize", handleResize); + } + }; + document.addEventListener("keydown", handleEscape); + + const handleResize = () => { + const newSourceRect = sourceElement.getBoundingClientRect(); + const newScrollY = Math.round(window.scrollY); + const newScrollX = Math.round(window.scrollX); + + // Get the current scale applied to the source element and compensate for it + const computedStyle = getComputedStyle(sourceElement); + const transform = computedStyle.transform; + let scaleX = 1, scaleY = 1; + + if (transform && transform !== 'none') { + const matrix = transform.match(/matrix.*\((.+)\)/); + if (matrix) { + const values = matrix[1].split(', '); + scaleX = parseFloat(values[0]); + scaleY = parseFloat(values[3]); + } + } + + // Apply inverse scale to get true original dimensions and positions + const newSourceWidth = newSourceRect.width / scaleX; + const newSourceHeight = newSourceRect.height / scaleY; + + // Calculate position shift due to center-based scaling + const deltaX = (newSourceWidth - newSourceRect.width) / 2; + const deltaY = (newSourceHeight - newSourceRect.height) / 2; + + const newSourceLeft = newSourceRect.left - deltaX; + const newSourceTop = newSourceRect.top - deltaY; + + const newViewportWidth = window.innerWidth; + const newViewportHeight = window.innerHeight; + const newTargetWidth = Math.round( + Math.min(Math.max(newSourceWidth, 800), newViewportWidth - 40), + ); + + // Just measure the existing modal content + const currentHeight = unifiedContent.getBoundingClientRect().height; + const newTargetHeight = Math.round( + Math.min(Math.max(currentHeight, 200), newViewportHeight * 0.85), + ); + + const newTargetLeft = Math.round((newViewportWidth - newTargetWidth) / 2); + const newTargetTop = + Math.round((newViewportHeight - newTargetHeight) / 2) + newScrollY; + + transitionContainer.style.left = + Math.round(newTargetLeft + newScrollX) + "px"; + transitionContainer.style.top = Math.round(newTargetTop) + "px"; + transitionContainer.style.width = Math.round(newTargetWidth) + "px"; + transitionContainer.style.height = Math.round(newTargetHeight) + "px"; + + sourceLeft = newSourceLeft; + sourceTop = newSourceTop; + sourceWidth = newSourceWidth; + sourceHeight = newSourceHeight; + targetLeft = newTargetLeft; + targetTop = newTargetTop; + targetWidth = newTargetWidth; + targetHeight = newTargetHeight; + scrollY = newScrollY; + scrollX = newScrollX; + }; + + window.addEventListener("resize", handleResize); + + if (settingsState.animations) { + animate(modal, { opacity: [0, 1] }, { duration: 0.2 }); + + animate( + transitionContainer, + { + left: [sourceLeft + scrollX, targetLeft + scrollX], + top: [sourceTop + scrollY, targetTop], + width: [sourceWidth, targetWidth], + height: [sourceHeight, targetHeight], + scale: [1, 1], + }, + { + duration: 0.5, + type: "spring", + stiffness: 280, + damping: 24, + }, + ); + + unifiedContent.classList.remove("notice-card-state"); + unifiedContent.classList.add("notice-modal-state"); + } else { + modal.style.opacity = "1"; + transitionContainer.style.left = Math.round(targetLeft + scrollX) + "px"; + transitionContainer.style.top = Math.round(targetTop) + "px"; + transitionContainer.style.width = Math.round(targetWidth) + "px"; + transitionContainer.style.height = Math.round(targetHeight) + "px"; + unifiedContent.classList.remove("notice-card-state"); + unifiedContent.classList.add("notice-modal-state"); + } +} + function callHomeTimetable(date: string, change?: any) { - // Creates a HTTP Post Request to the SEQTA page for the students timetable var xhr = new XMLHttpRequest(); xhr.open("POST", `${location.origin}/seqta/student/load/timetable?`, true); - // Sets the response type to json + xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8"); xhr.onreadystatechange = function () { - // Once the response is ready if (xhr.readyState === 4) { - // Clear the loading timeout since we got a response if (loadingTimeout) { clearTimeout(loadingTimeout); loadingTimeout = null; } - + const DayContainer = document.getElementById("day-container")!; - + try { var serverResponse = JSON.parse(xhr.response); let lessonArray: Array = []; - // If items in response: - if (serverResponse.payload.items.length > 0) { - if (DayContainer.innerText || change) { - for (let i = 0; i < serverResponse.payload.items.length; i++) { - lessonArray.push(serverResponse.payload.items[i]); - } - lessonArray.sort(function (a, b) { - return a.from.localeCompare(b.from); - }); - // If items in the response, set each corresponding value into divs - // lessonArray = lessonArray.splice(1) - GetLessonColours().then((colours) => { - let subjects = colours; - for (let i = 0; i < lessonArray.length; i++) { - let subjectname = `timetable.subject.colour.${lessonArray[i].code}`; - let subject = subjects.find( - (element: any) => element.name === subjectname, - ); - if (!subject) { - lessonArray[i].colour = "--item-colour: #8e8e8e;"; - } else { - lessonArray[i].colour = `--item-colour: ${subject.value};`; - let result = GetThresholdOfColor(subject.value); - - if (result > 300) { - lessonArray[i].invert = true; - } - } - // Removes seconds from the start and end times - lessonArray[i].from = lessonArray[i].from.substring(0, 5); - lessonArray[i].until = lessonArray[i].until.substring(0, 5); - - if (settingsState.timeFormat === "12") { - lessonArray[i].from = convertTo12HourFormat( - lessonArray[i].from, - ); - lessonArray[i].until = convertTo12HourFormat( - lessonArray[i].until, - ); - } - - // Checks if attendance is unmarked, and sets the string to " ". - lessonArray[i].attendanceTitle = CheckUnmarkedAttendance( - lessonArray[i].attendance, - ); + if (serverResponse.payload.items.length > 0) { + if (DayContainer.innerText || change) { + for (let i = 0; i < serverResponse.payload.items.length; i++) { + lessonArray.push(serverResponse.payload.items[i]); } - // If on home page, apply each lesson to HTML with information in each div - DayContainer.innerText = ""; - for (let i = 0; i < lessonArray.length; i++) { - var div = makeLessonDiv(lessonArray[i], i + 1); - // Append each of the lessons into the day-container - if (lessonArray[i].invert) { - const div1 = div.firstChild! as HTMLElement; - div1.classList.add("day-inverted"); - } + lessonArray.sort(function (a, b) { + return a.from.localeCompare(b.from); + }); - DayContainer.append(div.firstChild as HTMLElement); - } - - // Remove loading state after lessons are loaded - DayContainer.classList.remove("loading"); - - const today = new Date(); - if (currentSelectedDate.getDate() == today.getDate()) { + GetLessonColours().then((colours) => { + let subjects = colours; for (let i = 0; i < lessonArray.length; i++) { - CheckCurrentLesson(lessonArray[i], i + 1); + let subjectname = `timetable.subject.colour.${lessonArray[i].code}`; + + let subject = subjects.find( + (element: any) => element.name === subjectname, + ); + if (!subject) { + lessonArray[i].colour = "--item-colour: #8e8e8e;"; + } else { + lessonArray[i].colour = `--item-colour: ${subject.value};`; + let result = GetThresholdOfColor(subject.value); + + if (result > 300) { + lessonArray[i].invert = true; + } + } + + lessonArray[i].from = lessonArray[i].from.substring(0, 5); + lessonArray[i].until = lessonArray[i].until.substring(0, 5); + + if (settingsState.timeFormat === "12") { + lessonArray[i].from = convertTo12HourFormat( + lessonArray[i].from, + ); + lessonArray[i].until = convertTo12HourFormat( + lessonArray[i].until, + ); + } + + lessonArray[i].attendanceTitle = CheckUnmarkedAttendance( + lessonArray[i].attendance, + ); } - // For each lesson, check the start and end times - CheckCurrentLessonAll(lessonArray); - } - }); + + DayContainer.innerText = ""; + for (let i = 0; i < lessonArray.length; i++) { + var div = makeLessonDiv(lessonArray[i], i + 1); + + if (lessonArray[i].invert) { + const div1 = div.firstChild! as HTMLElement; + div1.classList.add("day-inverted"); + } + + DayContainer.append(div.firstChild as HTMLElement); + } + + DayContainer.classList.remove("loading"); + + const today = new Date(); + if (currentSelectedDate.getDate() == today.getDate()) { + for (let i = 0; i < lessonArray.length; i++) { + CheckCurrentLesson(lessonArray[i], i + 1); + } + + CheckCurrentLessonAll(lessonArray); + } + }); + } + } else { + DayContainer.innerHTML = ""; + var dummyDay = document.createElement("div"); + dummyDay.classList.add("day-empty"); + let img = document.createElement("img"); + img.src = browser.runtime.getURL(LogoLight); + let text = document.createElement("p"); + text.innerText = "No lessons available."; + dummyDay.append(img); + dummyDay.append(text); + DayContainer.append(dummyDay); + + DayContainer.classList.remove("loading"); } - } else { - DayContainer.innerHTML = ""; - var dummyDay = document.createElement("div"); - dummyDay.classList.add("day-empty"); - let img = document.createElement("img"); - img.src = browser.runtime.getURL(LogoLight); - let text = document.createElement("p"); - text.innerText = "No lessons available."; - dummyDay.append(img); - dummyDay.append(text); - DayContainer.append(dummyDay); - - // Remove loading state when no lessons available - DayContainer.classList.remove("loading"); - } } catch (error) { console.error("Error loading timetable data:", error); - // Remove loading state even if there's an error + DayContainer.classList.remove("loading"); - - // Show error message + DayContainer.innerHTML = ""; const errorDiv = document.createElement("div"); errorDiv.classList.add("day-empty"); @@ -582,17 +839,15 @@ function callHomeTimetable(date: string, change?: any) { }; xhr.send( JSON.stringify({ - // Information sent to SEQTA page as a request with the dates and student number from: date, until: date, - // Funny number + student: 69, }), ); } function CheckCurrentLessonAll(lessons: any) { - // Checks each lesson and sets an interval to run every 60 seconds to continue updating LessonInterval = setInterval( function () { for (let i = 0; i < lessons.length; i++) { @@ -614,7 +869,6 @@ async function CheckCurrentLesson(lesson: any, num: number) { } = lesson; const currentDate = new Date(); - // Create Date objects for start and end times const [startHour, startMinute] = startTime.split(":").map(Number); const [endHour, endMinute] = endTime.split(":").map(Number); @@ -624,7 +878,6 @@ async function CheckCurrentLesson(lesson: any, num: number) { const endDate = new Date(currentDate); endDate.setHours(endHour, endMinute, 0); - // Check if the current time is within the lesson time range const isValidTime = startDate < currentDate && endDate > currentDate; const elementId = `${code}${num}`; @@ -687,8 +940,7 @@ function makeLessonDiv(lesson: any, num: number) { assessments, } = lesson; - // Construct the base lesson string with default values using ternary operators - let lessonString = /* html */ ` + let lessonString = `

${description || "Unknown"}

${staff || "Unknown"}

@@ -697,15 +949,13 @@ function makeLessonDiv(lesson: any, num: number) {
${attendanceTitle || "Unknown"}
`; - // Add buttons for assessments and courses if applicable if (programmeID !== 0) { - lessonString += /* html */ ` + lessonString += `
${assessmentsicon}
${coursesicon}
`; } - // Add assessments if they exist if (assessments && assessments.length > 0) { const assessmentString = assessments .map( @@ -714,7 +964,7 @@ function makeLessonDiv(lesson: any, num: number) { ) .join(""); - lessonString += /* html */ ` + lessonString += `
@@ -752,7 +1002,6 @@ async function CreateUpcomingSection(assessments: any, activeSubjects: any) { var Today = new Date(); - // Removes overdue assessments from the upcoming assessments array and pushes to overdue array for (let i = 0; i < assessments.length; i++) { const assessment = assessments[i]; let assessmentdue = new Date(assessment.due); @@ -782,7 +1031,7 @@ async function CreateUpcomingSection(assessments: any, activeSubjects: any) { assessments[i].colour = "--item-colour: #8e8e8e;"; } else { assessments[i].colour = `--item-colour: ${subject.value};`; - GetThresholdOfColor(subject.value); // result (originally) result = GetThresholdOfColor + GetThresholdOfColor(subject.value); } } @@ -803,9 +1052,7 @@ async function CreateUpcomingSection(assessments: any, activeSubjects: any) { CreateFilters(activeSubjects); - // @ts-ignore let type; - // @ts-ignore let class_; for (let i = 0; i < assessments.length; i++) { @@ -813,10 +1060,7 @@ async function CreateUpcomingSection(assessments: any, activeSubjects: any) { if (!upcomingDates[element.due as keyof typeof upcomingDates]) { let dateObj: any = new Object(); dateObj.div = CreateElement( - // TODO: not sure whats going on here? - // eslint-disable-next-line (type = "div"), - // eslint-disable-next-line (class_ = "upcoming-date-container"), ); dateObj.assessments = []; @@ -845,7 +1089,7 @@ async function CreateUpcomingSection(assessments: any, activeSubjects: any) { assessmentDate = createAssessmentDateDiv( date, upcomingDates[date as keyof typeof upcomingDates], - // eslint-disable-next-line + datecase, ); } else { @@ -950,9 +1194,6 @@ function createAssessmentDateDiv(date: string, value: any, datecase?: any) { if (response.payload.length > 0) { const assessment = document.querySelector(`#assessment${element.id}`); - // ticksvg = stringToHTML(``).firstChild - // ticksvg.classList.add('upcoming-tick') - // assessment.append(ticksvg) let submittedtext = document.createElement("div"); submittedtext.classList.add("upcoming-submittedtext"); submittedtext.innerText = "Submitted"; @@ -1009,7 +1250,7 @@ function CreateFilters(subjects: any) { let filterdiv = document.querySelector("#upcoming-filters"); for (let i = 0; i < subjects.length; i++) { const element = subjects[i]; - // eslint-disable-next-line + if (!Object.prototype.hasOwnProperty.call(filteroptions, element.code)) { filteroptions[element.code] = true; settingsState.subjectfilters = filteroptions; diff --git a/src/types/storage.ts b/src/types/storage.ts index 0ee0a408..74c7790d 100644 --- a/src/types/storage.ts +++ b/src/types/storage.ts @@ -36,6 +36,7 @@ export interface SettingsState { devMode?: boolean; originalDarkMode?: boolean; newsSource?: string; + mockNotices?: boolean; // depreciated keys animatedbk: boolean;