open-vtop 1.0.7 → 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";
@@ -80,3 +81,20 @@ export const ACADEMICS_CHECK_HEADERS = {
80
81
  Referer: CONTENT,
81
82
  "User-Agent": USER_AGENT_CHROME_139,
82
83
  };
84
+ export const LOGOUT_URL = `${BASE}/vtop/logout`;
85
+ export const LOGOUT_HEADERS = {
86
+ Accept: ACCEPT_HTML_EXTENDED,
87
+ "Accept-Encoding": ACCEPT_ENCODING_ZSTD,
88
+ "Accept-Language": ACCEPT_LANGUAGE,
89
+ "Cache-Control": "no-cache",
90
+ "Content-Type": "application/x-www-form-urlencoded",
91
+ Origin: BASE,
92
+ Pragma: "no-cache",
93
+ Priority: "u=0, i",
94
+ Referer: CONTENT,
95
+ "Sec-Fetch-Dest": "document",
96
+ "Sec-Fetch-Mode": "navigate",
97
+ "Sec-Fetch-Site": "same-origin",
98
+ "Sec-Fetch-User": "?1",
99
+ "Upgrade-Insecure-Requests": "1",
100
+ };
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");
@@ -55,6 +56,10 @@ app.post("/api/login/form", async (c) => {
55
56
  return c.html(_jsx(ErrorMessage, { message: `Server error: ${String(error)}` }));
56
57
  }
57
58
  });
59
+ app.post("/api/logout", async (c) => {
60
+ await sessionManager.logout();
61
+ return c.redirect("/login");
62
+ });
58
63
  app.get("/api/login/events", async (c) => {
59
64
  return streamSSE(c, async (stream) => {
60
65
  console.log("SSE connected");
@@ -120,9 +125,34 @@ app.get("/api/assignments/html", async (c) => {
120
125
  return c.html(_jsx(ErrorMessage, { message: `Failed to load assignments: ${String(error)}` }));
121
126
  }
122
127
  });
123
- serve({
124
- fetch: app.fetch,
125
- port: 6767,
126
- }, (info) => {
127
- console.log(`Server is running on http://localhost:${info.port}`);
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
+ }
128
142
  });
143
+ const main = async () => {
144
+ try {
145
+ await sessionManager.initialize();
146
+ await sessionManager.tryAutoLogin();
147
+ }
148
+ catch (e) {
149
+ console.error("Initialization failed:", e);
150
+ }
151
+ serve({
152
+ fetch: app.fetch,
153
+ port: 6767,
154
+ }, (info) => {
155
+ console.log(`Server is running on http://localhost:${info.port}`);
156
+ });
157
+ };
158
+ main();
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
+ }
@@ -2,9 +2,11 @@
2
2
  //nvm i did sepearations of concerns
3
3
  import { solve, extractDataUriParts, saveCaptchaImage, } from "./captcha-solver.js";
4
4
  import * as path from "path";
5
+ import * as fs from "fs/promises";
5
6
  import { EventEmitter } from "events";
6
- 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, } from "./constants.js";
7
- 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
+ const CREDENTIALS_FILE = path.resolve(process.cwd(), ".credentials.json");
8
10
  class VTOPSessionManager {
9
11
  state = {
10
12
  cookies: new Map(),
@@ -226,6 +228,7 @@ class VTOPSessionManager {
226
228
  console.log("Login POST submitted successfully!");
227
229
  this.state.loggedIn = true;
228
230
  this.state.username = username;
231
+ await this.saveCredentials(username, password);
229
232
  this.events.emit("login-complete");
230
233
  return true;
231
234
  }
@@ -402,8 +405,122 @@ class VTOPSessionManager {
402
405
  }
403
406
  return [];
404
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
+ }
446
+ async logout() {
447
+ if (!this.state.loggedIn) {
448
+ console.log("Already logged out or not logged in.");
449
+ return true;
450
+ }
451
+ console.log("Logging out...");
452
+ // Attempt to notify VTOP server
453
+ try {
454
+ const cookies = this.getCookieHeader();
455
+ const headers = {
456
+ ...LOGOUT_HEADERS,
457
+ Cookie: cookies,
458
+ };
459
+ const formData = new URLSearchParams();
460
+ if (this.state.csrf) {
461
+ formData.set("_csrf", this.state.csrf);
462
+ }
463
+ const res = await fetch(LOGOUT_URL, {
464
+ method: "POST",
465
+ headers,
466
+ body: formData.toString(),
467
+ redirect: "manual",
468
+ });
469
+ console.log(` -> Logout POST status: ${res.status}`);
470
+ if (res.status === 302 || res.status === 200) {
471
+ console.log("Server session cleared effectively.");
472
+ }
473
+ }
474
+ catch (e) {
475
+ console.warn("Logout request failed (network error?), clearing local session anyway.", e);
476
+ }
477
+ // Clear local session
478
+ this.state.cookies.clear();
479
+ this.state.csrf = null;
480
+ this.state.initialized = false;
481
+ this.state.loggedIn = false;
482
+ this.state.username = null;
483
+ this.state.regNo = null;
484
+ this.events.emit("logout");
485
+ console.log("Local session cleared.");
486
+ return true;
487
+ }
488
+ async saveCredentials(username, password) {
489
+ try {
490
+ await fs.writeFile(CREDENTIALS_FILE, JSON.stringify({ username, password }));
491
+ console.log("Credentials saved locally.");
492
+ }
493
+ catch (e) {
494
+ console.error("Failed to save credentials:", e);
495
+ }
496
+ }
497
+ async loadCredentials() {
498
+ try {
499
+ const data = await fs.readFile(CREDENTIALS_FILE, "utf-8");
500
+ return JSON.parse(data);
501
+ }
502
+ catch (e) {
503
+ return null;
504
+ }
505
+ }
506
+ async tryAutoLogin() {
507
+ const creds = await this.loadCredentials();
508
+ if (creds) {
509
+ console.log(`Found saved credentials for user: ${creds.username}`);
510
+ console.log("Attemping auto-login...");
511
+ const success = await this.login(creds.username, creds.password);
512
+ if (success) {
513
+ console.log("Auto-login successful!");
514
+ await this.navigatePostLogin();
515
+ await this.performAcademicsCheck();
516
+ }
517
+ else {
518
+ console.log("Auto-login failed.");
519
+ }
520
+ }
521
+ else {
522
+ console.log("No saved credentials found.");
523
+ }
524
+ }
405
525
  }
406
526
  export const sessionManager = new VTOPSessionManager();
407
- sessionManager.initialize().catch((error) => {
408
- console.error("Failed to auto-initialize session:", error);
409
- });
@@ -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", {})] }), _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.7"
33
+ "version": "1.0.9"
34
34
  }