open-vtop 1.0.8 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,14 +9,15 @@ npx open-vtop logs
9
9
  bunx open-vtop logs
10
10
  ```
11
11
  ## Todo
12
- 1) save usn and password for future use
13
- 2) grab regno from the responses it self
14
- 3) attendance
15
- 4) timetable
16
- 5) cgpa
17
- 6) course-page
18
- 7) callendar
19
- 9) qol like automatic browser open, better logging
12
+ - [x] Save USN and password for future use
13
+ - [x] Grab regno from the responses itself
14
+ - [x] Attendance
15
+ - [ ] Timetable
16
+ - [ ] CGPA (unsure if people actually want to see their marks and grades on this)
17
+ - [ ] Course page
18
+ - [ ] Calendar
19
+ - [x] QoL: automatic browser open, better logging
20
+ - [x] Upcoming exams
20
21
 
21
22
  ## Getting Started
22
23
 
@@ -1,5 +1,6 @@
1
- import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
- import { AssignmentCard } from "./AssignmentCard.js";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
3
2
  export function AssignmentsList({ assignments, }) {
4
- return (_jsx("div", { class: "flex flex-col gap-2", children: assignments.map((assignment, i) => (_jsx(AssignmentCard, { assignment: assignment }, i))) }));
3
+ return (_jsx("div", { class: "overflow-x-auto rounded-lg border border-border bg-surface", children: _jsxs("table", { class: "w-full text-xs text-left", children: [_jsx("thead", { class: "bg-surface border-b border-border/50 text-[0.65rem] uppercase tracking-wider text-muted font-bold", children: _jsxs("tr", { children: [_jsx("th", { class: "px-3 py-2 text-foreground/80", children: "Course" }), _jsx("th", { class: "px-3 py-2 text-foreground/80", children: "Assignment" }), _jsx("th", { class: "px-3 py-2 text-foreground/80", children: "Due Date" }), _jsx("th", { class: "px-3 py-2 text-right text-foreground/80", children: "Status" })] }) }), _jsx("tbody", { class: "divide-y divide-border/30", children: assignments.map((ass, i) => (_jsxs("tr", { class: "hover:bg-muted/5 transition-colors group", children: [_jsx("td", { class: "px-3 py-2 font-mono text-[0.7rem] text-muted whitespace-nowrap", title: ass.courseName, children: ass.courseCode }), _jsx("td", { class: "px-3 py-2 font-medium text-foreground max-w-[150px] truncate", title: ass.assignmentTitle, children: ass.assignmentTitle }), _jsx("td", { class: "px-3 py-2 text-red-400 whitespace-nowrap text-[0.7rem]", children: ass.dueDate || "N/A" }), _jsx("td", { class: "px-3 py-2 text-right", children: _jsx("span", { class: `inline-block text-[0.6rem] px-1.5 py-0.5 rounded font-medium whitespace-nowrap ${ass.status?.toLowerCase().includes("pending")
4
+ ? "bg-red-500/10 text-red-500 border border-red-500/20"
5
+ : "bg-blue-500/10 text-blue-500 border border-blue-500/20"}`, children: ass.status || "Pending" }) })] }, i))) })] }) }));
5
6
  }
@@ -1,5 +1,8 @@
1
- import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
- import { CourseCard } from "./CourseCard.js";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
3
2
  export function CoursesList({ courses }) {
4
- return (_jsx("div", { class: "grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3", children: courses.map((course, i) => (_jsx(CourseCard, { course: course }, i))) }));
3
+ return (_jsx("div", { class: "overflow-x-auto rounded-lg border border-border bg-surface", children: _jsxs("table", { class: "w-full text-xs text-left", children: [_jsx("thead", { class: "bg-surface border-b border-border/50 text-[0.65rem] uppercase tracking-wider text-muted font-bold", children: _jsxs("tr", { children: [_jsx("th", { class: "px-3 py-2 text-foreground/80", children: "Code" }), _jsx("th", { class: "px-3 py-2 text-foreground/80", children: "Course Name" }), _jsx("th", { class: "px-3 py-2 text-foreground/80", children: "Type" }), _jsx("th", { class: "px-3 py-2 text-foreground/80", children: "Attendance" }), _jsx("th", { class: "px-3 py-2 text-right text-foreground/80", children: "Remarks" })] }) }), _jsx("tbody", { class: "divide-y divide-border/30", children: courses.map((course, i) => (_jsxs("tr", { class: "hover:bg-muted/5 transition-colors group", children: [_jsx("td", { class: "px-3 py-2 font-mono text-[0.7rem] text-muted", children: course.code }), _jsx("td", { class: "px-3 py-2 font-medium text-foreground max-w-[200px] truncate", title: course.name, children: course.name }), _jsx("td", { class: "px-3 py-2", children: _jsx("span", { class: "text-[0.6rem] px-1.5 py-0.5 rounded border border-border text-muted", children: course.type }) }), _jsx("td", { class: "px-3 py-2", children: _jsxs("span", { class: `font-bold ${course.attendanceColor === "danger"
4
+ ? "text-red-500"
5
+ : course.attendanceColor === "warning"
6
+ ? "text-yellow-500"
7
+ : "text-green-500"}`, children: [course.attendance, "%"] }) }), _jsx("td", { class: "px-3 py-2 text-right", children: course.remarks && (_jsx("span", { class: `inline-block text-[0.6rem] px-1.5 py-0.5 rounded bg-${course.attendanceColor === "danger" ? "red" : "green"}-500/10 text-${course.attendanceColor === "danger" ? "red" : "green"}-500 whitespace-nowrap`, children: course.remarks })) })] }, i))) })] }) }));
5
8
  }
@@ -0,0 +1,4 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
2
+ export const ExamsList = ({ exams }) => {
3
+ return (_jsx("div", { class: "overflow-x-auto rounded-lg border border-border bg-surface", children: _jsxs("table", { class: "w-full text-xs text-left", children: [_jsx("thead", { class: "bg-surface border-b border-border/50 text-[0.65rem] uppercase tracking-wider text-muted font-bold", children: _jsxs("tr", { children: [_jsx("th", { class: "px-3 py-2", children: "Code" }), _jsx("th", { class: "px-3 py-2", children: "Course Title" }), _jsx("th", { class: "px-3 py-2", children: "Slot" }), _jsx("th", { class: "px-3 py-2", children: "Date (Session)" }), _jsx("th", { class: "px-3 py-2", children: "Time" }), _jsx("th", { class: "px-3 py-2", children: "Venue" }), _jsx("th", { class: "px-3 py-2 text-right", children: "Seat" })] }) }), _jsx("tbody", { class: "divide-y divide-border/30", children: exams.map((exam) => (_jsxs("tr", { class: "hover:bg-muted/5 transition-colors group", children: [_jsxs("td", { class: "px-3 py-2 font-semibold text-foreground whitespace-nowrap", children: [exam.courseCode, _jsx("span", { class: "ml-1.5 text-[0.6rem] px-1 rounded border border-border text-muted font-normal", children: exam.courseType })] }), _jsx("td", { class: "px-3 py-2 font-medium text-foreground/90 max-w-[200px] truncate", title: exam.courseTitle, children: exam.courseTitle }), _jsx("td", { class: "px-3 py-2 text-muted whitespace-nowrap font-mono text-[0.65rem]", children: exam.slot }), _jsxs("td", { class: "px-3 py-2 text-foreground/80 whitespace-nowrap", children: [exam.examDate, _jsx("span", { class: "ml-1.5 text-[0.6rem] font-bold text-blue-400 bg-blue-400/10 px-1 rounded", children: exam.examSession })] }), _jsx("td", { class: "px-3 py-2 text-muted whitespace-nowrap font-mono text-[0.65rem]", children: exam.examTime }), _jsx("td", { class: "px-3 py-2 text-foreground/80 whitespace-nowrap truncate max-w-[100px]", title: exam.venue, children: exam.venue }), _jsxs("td", { class: "px-3 py-2 text-right font-mono text-muted text-[0.65rem] whitespace-nowrap", children: [exam.seatLocation, _jsx("span", { class: "mx-1 text-border", children: "/" }), _jsx("span", { class: "text-foreground", children: exam.seatNo })] })] }))) })] }) }));
4
+ };
package/dist/constants.js CHANGED
@@ -12,6 +12,7 @@ export const CONTENT = `${BASE}/vtop/content`;
12
12
  export const ACADEMICS_CHECK = `${BASE}/vtop/academics/common/AcademicsDefaultCheck`;
13
13
  export const UPCOMING_ASSIGNMENTS = `${BASE}/vtop/get/upcoming/digital/assignments`;
14
14
  export const COURSE_DETAILS = `${BASE}/vtop/get/dashboard/current/semester/course/details`;
15
+ export const EXAM_SCHEDULE = `${BASE}/vtop/examinations/doSearchExamScheduleForStudent`;
15
16
  // HTTP Headers
16
17
  // Common browser headers
17
18
  export const USER_AGENT_CHROME = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ import { CoursesList } from "./components/CoursesList.js";
12
12
  import { AssignmentsList } from "./components/AssignmentsList.js";
13
13
  import { EmptyState } from "./components/EmptyState.js";
14
14
  import { SessionExpired } from "./components/SessionExpired.js";
15
+ import { ExamsList } from "./components/ExamsList.js";
15
16
  const app = new Hono();
16
17
  const require = createRequire(import.meta.url);
17
18
  const htmxPath = require.resolve("htmx.org/dist/htmx.min.js");
@@ -124,6 +125,21 @@ app.get("/api/assignments/html", async (c) => {
124
125
  return c.html(_jsx(ErrorMessage, { message: `Failed to load assignments: ${String(error)}` }));
125
126
  }
126
127
  });
128
+ app.get("/api/exams/html", async (c) => {
129
+ if (!sessionManager.isLoggedIn()) {
130
+ return c.html(_jsx(SessionExpired, {}));
131
+ }
132
+ try {
133
+ const exams = await sessionManager.fetchExamSchedule();
134
+ if (exams.length === 0) {
135
+ return c.html(_jsx(EmptyState, { message: "No exam schedule found for this semester." }));
136
+ }
137
+ return c.html(_jsx(ExamsList, { exams: exams }));
138
+ }
139
+ catch (error) {
140
+ return c.html(_jsx(ErrorMessage, { message: `Failed to load exam schedule: ${String(error)}` }));
141
+ }
142
+ });
127
143
  const main = async () => {
128
144
  try {
129
145
  await sessionManager.initialize();
package/dist/parsers.js CHANGED
@@ -136,3 +136,48 @@ export function parseCourseDetailsHtml(html) {
136
136
  }
137
137
  return courses;
138
138
  }
139
+ /**
140
+ * Parses exam schedule from HTML response
141
+ */
142
+ export function parseExamScheduleHtml(html) {
143
+ const exams = [];
144
+ // Find the table content rows
145
+ const rowRegex = /<tr[^>]*class="tableContent"[^>]*>([\s\S]*?)<\/tr>/g;
146
+ const matches = [...html.matchAll(rowRegex)];
147
+ for (const match of matches) {
148
+ const rowContent = match[1];
149
+ // Skip rows that are just headers (e.g., colspan="13")
150
+ if (rowContent.includes('colspan="13"')) {
151
+ continue;
152
+ }
153
+ const cells = [];
154
+ const cellRegex = /<td[^>]*>([\s\S]*?)<\/td>/g;
155
+ const cellMatches = [...rowContent.matchAll(cellRegex)];
156
+ for (const cellMatch of cellMatches) {
157
+ // Strip HTML and whitespace
158
+ let content = cellMatch[1].replace(/<[^>]+>/g, "").trim();
159
+ // Replace multiple spaces/newlines with single space
160
+ content = content.replace(/\s+/g, " ");
161
+ // Remove "-" placeholders if preferred, or keep them. keeping them is fine.
162
+ cells.push(content);
163
+ }
164
+ if (cells.length >= 13) {
165
+ exams.push({
166
+ sNo: cells[0],
167
+ courseCode: cells[1],
168
+ courseTitle: cells[2],
169
+ courseType: cells[3],
170
+ classId: cells[4],
171
+ slot: cells[5],
172
+ examDate: cells[6],
173
+ examSession: cells[7],
174
+ reportingTime: cells[8], // The HTML has separate columns but let's map them by index
175
+ examTime: cells[9],
176
+ venue: cells[10],
177
+ seatLocation: cells[11],
178
+ seatNo: cells[12],
179
+ });
180
+ }
181
+ }
182
+ return exams;
183
+ }
@@ -4,8 +4,8 @@ import { solve, extractDataUriParts, saveCaptchaImage, } from "./captcha-solver.
4
4
  import * as path from "path";
5
5
  import * as fs from "fs/promises";
6
6
  import { EventEmitter } from "events";
7
- import { BASE, VTOP, OPEN_PAGE, OPEN_PAGE_ALT, PRELOGIN_SETUP, LOGIN_PAGE, INIT_PAGE, MAIN_PAGE, VTOP_OPEN, CONTENT, ACADEMICS_CHECK, UPCOMING_ASSIGNMENTS, COURSE_DETAILS, BROWSER_HEADERS, LOGIN_POST_HEADERS, POST_LOGIN_HEADERS, API_REQUEST_HEADERS, ACADEMICS_CHECK_HEADERS, LOGOUT_URL, LOGOUT_HEADERS, } from "./constants.js";
8
- import { extractCsrf, extractRegNo, detectCaptcha, parseAssignmentsHtml, parseCourseDetailsHtml, } from "./parsers.js";
7
+ import { BASE, VTOP, OPEN_PAGE, OPEN_PAGE_ALT, PRELOGIN_SETUP, LOGIN_PAGE, INIT_PAGE, MAIN_PAGE, VTOP_OPEN, CONTENT, ACADEMICS_CHECK, UPCOMING_ASSIGNMENTS, COURSE_DETAILS, BROWSER_HEADERS, LOGIN_POST_HEADERS, POST_LOGIN_HEADERS, API_REQUEST_HEADERS, ACADEMICS_CHECK_HEADERS, LOGOUT_URL, LOGOUT_HEADERS, EXAM_SCHEDULE, } from "./constants.js";
8
+ import { extractCsrf, extractRegNo, detectCaptcha, parseAssignmentsHtml, parseCourseDetailsHtml, parseExamScheduleHtml, } from "./parsers.js";
9
9
  const CREDENTIALS_FILE = path.resolve(process.cwd(), ".credentials.json");
10
10
  class VTOPSessionManager {
11
11
  state = {
@@ -405,6 +405,44 @@ class VTOPSessionManager {
405
405
  }
406
406
  return [];
407
407
  }
408
+ async fetchExamSchedule() {
409
+ if (!this.state.loggedIn || !this.state.regNo) {
410
+ console.error("Cannot fetch exam schedule: not logged in or no regNo");
411
+ return [];
412
+ }
413
+ const cookies = this.getCookieHeader();
414
+ const now = new Date(); // Although payload doesn't seem to use 'x' timestamp, we'll keep it consistent if needed or omit.
415
+ // Based on user image, payload is: authorizedID, _csrf, semesterSubId.
416
+ const apiHeaders = {
417
+ ...API_REQUEST_HEADERS,
418
+ Cookie: cookies,
419
+ };
420
+ const params = new URLSearchParams();
421
+ params.set("authorizedID", this.state.regNo);
422
+ if (this.state.csrf)
423
+ params.set("_csrf", this.state.csrf);
424
+ params.set("semesterSubId", "VL20252605"); // Hardcoded as requested
425
+ try {
426
+ console.log("Fetching exam schedule...");
427
+ const res = await fetch(EXAM_SCHEDULE, {
428
+ method: "POST",
429
+ headers: apiHeaders,
430
+ body: params.toString(),
431
+ });
432
+ console.log(` -> Exam schedule status: ${res.status}`);
433
+ const html = await res.text();
434
+ // console.log(` -> Exam schedule response length: ${html.length}`);
435
+ if (html) {
436
+ const schedule = parseExamScheduleHtml(html);
437
+ console.log(` Parsed ${schedule.length} exam entries`);
438
+ return schedule;
439
+ }
440
+ }
441
+ catch (e) {
442
+ console.error("Failed to fetch exam schedule:", e);
443
+ }
444
+ return [];
445
+ }
408
446
  async logout() {
409
447
  if (!this.state.loggedIn) {
410
448
  console.log("Already logged out or not logged in.");
@@ -1,15 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
2
2
  import { BaseLayout } from "./layouts/Base.js";
3
+ import { CoursesList } from "../components/CoursesList.js";
4
+ import { AssignmentsList } from "../components/AssignmentsList.js";
3
5
  export const Dashboard = ({ username, courses, assignments }) => {
4
- return (_jsxs(BaseLayout, { title: "Dashboard - Open-VTOP", children: [_jsxs("div", { class: "flex justify-between items-center mb-6 border-b border-border pb-4", children: [_jsxs("div", { children: [_jsx("h1", { class: "text-xl font-bold tracking-tight", children: "Dashboard" }), _jsxs("p", { class: "text-muted text-xs", children: ["Welcome back, ", username] })] }), _jsx("div", { children: _jsx("form", { action: "/api/logout", method: "post", children: _jsx("button", { type: "submit", class: "text-xs text-red-400 hover:text-red-300 transition-colors font-medium cursor-pointer", children: "Logout" }) }) })] }), _jsxs("div", { class: "grid grid-cols-1 lg:grid-cols-12 gap-6 items-start", children: [_jsxs("section", { class: "lg:col-span-8 xl:col-span-9 space-y-4", children: [_jsxs("div", { class: "flex items-center justify-between", children: [_jsx("h2", { class: "text-lg font-semibold", children: "Course Details" }), _jsx("span", { class: "text-[0.65rem] text-muted bg-surface border border-border px-2 py-1 rounded", children: "Winter Semester 2025-26" })] }), courses ? (courses.length === 0 ? (_jsx("div", { class: "p-8 text-center bg-surface border border-border rounded-lg text-muted text-sm", children: "No course details found." })) : (_jsx("div", { class: "grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3", children: courses.map((course, i) => (_jsxs("div", { class: "p-3 bg-surface border border-border rounded-lg flex flex-col justify-between h-full hover:border-muted transition-colors", children: [_jsxs("div", { children: [_jsxs("div", { class: "flex justify-between items-start mb-1.5", children: [_jsx("span", { class: "text-[0.65rem] font-bold text-muted uppercase tracking-wider", children: course.code }), _jsx("span", { class: "text-[0.6rem] px-1.5 py-0.5 rounded border border-border text-muted", children: course.type })] }), _jsx("h4", { class: "font-semibold text-xs mb-2 leading-relaxed line-clamp-2", children: course.name })] }), _jsxs("div", { class: "flex items-end justify-between mt-2 pt-2 border-t border-border/50", children: [_jsxs("div", { class: "flex flex-col", children: [_jsx("span", { class: "text-[0.6rem] text-muted uppercase", children: "Attendance" }), _jsxs("span", { class: `text-base font-bold ${course.attendanceColor === "danger"
5
- ? "text-red-500"
6
- : course.attendanceColor === "warning"
7
- ? "text-yellow-500"
8
- : "text-green-500"}`, children: [course.attendance, "%"] })] }), course.remarks && (_jsx("span", { class: `text-[0.6rem] px-1.5 py-0.5 rounded bg-${course.attendanceColor === "danger"
9
- ? "red"
10
- : "green"}-500/10 text-${course.attendanceColor === "danger"
11
- ? "red"
12
- : "green"}-500`, children: course.remarks }))] })] }, i))) }))) : (_jsx("div", { id: "courses-loader", "hx-get": "/api/courses/html", "hx-trigger": "load", "hx-swap": "innerHTML", class: "w-full", children: _jsx("div", { class: "grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3 animate-pulse", children: [1, 2, 3, 4, 5, 6].map((i) => (_jsx("div", { class: "h-28 bg-surface/50 border border-border rounded-lg" }, i))) }) }))] }), _jsxs("section", { class: "lg:col-span-4 xl:col-span-3 space-y-4", children: [_jsx("h2", { class: "text-lg font-semibold", children: "Assignments" }), assignments ? (assignments.length === 0 ? (_jsx("div", { class: "p-6 text-center bg-surface border border-border rounded-lg", children: _jsx("p", { class: "text-sm text-muted", children: "No assignments pending." }) })) : (_jsx("div", { class: "flex flex-col gap-2", children: assignments.map((ass, i) => (_jsxs("div", { class: "p-3 bg-surface border border-border rounded-lg hover:border-muted transition-colors group flex flex-col gap-1", children: [_jsxs("div", { class: "flex justify-between items-start gap-2", children: [_jsx("span", { class: "font-bold text-xs text-foreground line-clamp-1", title: ass.courseName, children: ass.courseName }), _jsxs("span", { class: "text-[0.65rem] font-medium text-red-400 whitespace-nowrap shrink-0", children: ["Due: ", ass.dueDate || "N/A"] })] }), _jsxs("div", { class: "flex justify-between items-end gap-2", children: [_jsxs("div", { class: "flex flex-col min-w-0", children: [_jsx("span", { class: "text-[0.7rem] text-muted truncate", title: ass.assignmentTitle, children: ass.assignmentTitle }), _jsx("span", { class: "text-[0.6rem] text-muted/60 font-mono", children: ass.courseCode })] }), _jsx("span", { class: `text-[0.6rem] px-1.5 py-0.5 rounded font-medium whitespace-nowrap shrink-0 ${ass.status?.toLowerCase().includes("pending")
13
- ? "bg-red-500/10 text-red-500 border border-red-500/20"
14
- : "bg-blue-500/10 text-blue-500 border border-blue-500/20"}`, children: ass.status || "Pending" })] })] }, i))) }))) : (_jsx("div", { id: "assignments-loader", "hx-get": "/api/assignments/html", "hx-trigger": courses ? "load" : "htmx:afterOnLoad from:#courses-loader", "hx-target": "#assignments-container", "hx-swap": "innerHTML", class: "w-full", children: _jsx("div", { id: "assignments-container", class: "space-y-4", children: _jsxs("div", { id: "loading-state", class: "text-center py-12 text-muted", children: [_jsx("div", { class: "inline-block animate-spin rounded-full h-5 w-5 border-2 border-muted border-t-white mb-3" }), _jsx("p", { class: "text-xs", children: "Syncing assignments..." })] }) }) }))] })] })] }));
6
+ return (_jsxs(BaseLayout, { title: "Dashboard - Open-VTOP", children: [_jsxs("div", { class: "flex justify-between items-center mb-6 border-b border-border pb-4", children: [_jsxs("div", { children: [_jsx("h1", { class: "text-xl font-bold tracking-tight", children: "Dashboard" }), _jsxs("p", { class: "text-muted text-xs", children: ["Welcome back, ", username] })] }), _jsx("div", { children: _jsx("form", { action: "/api/logout", method: "post", children: _jsx("button", { type: "submit", class: "text-xs text-red-400 hover:text-red-300 transition-colors font-medium cursor-pointer", children: "Logout" }) }) })] }), _jsxs("div", { class: "grid grid-cols-1 lg:grid-cols-12 gap-6 items-start", children: [_jsxs("section", { class: "lg:col-span-8 xl:col-span-9 space-y-4", children: [_jsxs("div", { class: "flex items-center justify-between", children: [_jsx("h2", { class: "text-lg font-semibold", children: "Attendance" }), _jsx("span", { class: "text-[0.65rem] text-muted bg-surface border border-border px-2 py-1 rounded", children: "Winter Semester 2025-26" })] }), courses ? (courses.length === 0 ? (_jsx("div", { class: "p-8 text-center bg-surface border border-border rounded-lg text-muted text-sm", children: "No course details found." })) : (_jsx(CoursesList, { courses: courses }))) : (_jsx("div", { id: "courses-loader", "hx-get": "/api/courses/html", "hx-trigger": "load", "hx-swap": "innerHTML", class: "w-full", children: _jsx("div", { class: "grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3 animate-pulse", children: [1, 2, 3, 4, 5, 6].map((i) => (_jsx("div", { class: "h-28 bg-surface/50 border border-border rounded-lg" }, i))) }) }))] }), _jsxs("section", { class: "lg:col-span-4 xl:col-span-3 space-y-4", children: [_jsx("h2", { class: "text-lg font-semibold", children: "Assignments" }), assignments ? (assignments.length === 0 ? (_jsx("div", { class: "p-6 text-center bg-surface border border-border rounded-lg", children: _jsx("p", { class: "text-sm text-muted", children: "No assignments pending." }) })) : (_jsx(AssignmentsList, { assignments: assignments }))) : (_jsx("div", { id: "assignments-loader", "hx-get": "/api/assignments/html", "hx-trigger": courses ? "load" : "htmx:afterOnLoad from:#courses-loader", "hx-target": "#assignments-container", "hx-swap": "innerHTML", class: "w-full", children: _jsx("div", { id: "assignments-container", class: "space-y-4", children: _jsxs("div", { id: "loading-state", class: "text-center py-12 text-muted", children: [_jsx("div", { class: "inline-block animate-spin rounded-full h-5 w-5 border-2 border-muted border-t-white mb-3" }), _jsx("p", { class: "text-xs", children: "Syncing assignments..." })] }) }) }))] }), _jsxs("section", { class: "lg:col-span-12 space-y-4 pt-4 border-t border-border/50", children: [_jsxs("div", { class: "flex items-center justify-between", children: [_jsx("h2", { class: "text-lg font-semibold", children: "Exam Schedule" }), _jsx("span", { class: "text-[0.65rem] text-muted bg-surface border border-border px-2 py-1 rounded", children: "Winter Semester 2025-26" })] }), _jsx("div", { id: "exams-loader", "hx-get": "/api/exams/html", "hx-trigger": courses ? "load" : "htmx:afterOnLoad from:#courses-loader", "hx-swap": "innerHTML", class: "w-full", children: _jsx("div", { class: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 animate-pulse", children: [1, 2, 3].map((i) => (_jsx("div", { class: "h-40 bg-surface/50 border border-border rounded-lg" }, i))) }) })] })] })] }));
15
7
  };
package/package.json CHANGED
@@ -30,5 +30,5 @@
30
30
  "tsx": "^4.7.1",
31
31
  "typescript": "^5.8.3"
32
32
  },
33
- "version": "1.0.8"
33
+ "version": "1.0.9"
34
34
  }