open-vtop 1.0.4 → 1.0.5
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 +10 -0
- package/dist/index.js +19 -162
- package/dist/session-manager.js +1 -10
- package/dist/views/Dashboard.js +5 -0
- package/dist/views/Login.js +5 -0
- package/dist/views/layouts/Base.js +40 -118
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
An open-source VTOP client with automatic session management.
|
|
4
4
|
|
|
5
|
+
## Todo
|
|
6
|
+
1) save usn and password for future use
|
|
7
|
+
2) grab regno from the responses it self
|
|
8
|
+
3) attendance
|
|
9
|
+
4) timetable
|
|
10
|
+
5) cgpa
|
|
11
|
+
6) course-page
|
|
12
|
+
7) callendar
|
|
13
|
+
8) many more...
|
|
14
|
+
9) qol like automatic browser open, better logging, save usn password for faster logins, logout
|
|
5
15
|
## Features
|
|
6
16
|
|
|
7
17
|
- 🚀 **Automatic Session Initialization**: VTOP session cookies and CSRF tokens are automatically established when the server starts
|
package/dist/index.js
CHANGED
|
@@ -3,107 +3,20 @@ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
|
3
3
|
import { serve } from "@hono/node-server";
|
|
4
4
|
import { Hono } from "hono";
|
|
5
5
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { Login } from "./views/Login.js";
|
|
7
|
+
import { Dashboard } from "./views/Dashboard.js";
|
|
8
8
|
import { sessionManager } from "./session-manager.js";
|
|
9
|
-
const app = new Hono();
|
|
10
9
|
import { createRequire } from "module";
|
|
10
|
+
const app = new Hono();
|
|
11
11
|
const require = createRequire(import.meta.url);
|
|
12
12
|
const htmxPath = require.resolve("htmx.org/dist/htmx.min.js");
|
|
13
|
-
// Serve htmx from node_modules
|
|
14
13
|
app.use("/static/htmx.js", serveStatic({ path: htmxPath }));
|
|
15
|
-
// Main page
|
|
16
14
|
app.get("/", (c) => {
|
|
17
|
-
|
|
18
|
-
});
|
|
19
|
-
// API Endpoints
|
|
20
|
-
app.get("/api/hello", (c) => {
|
|
21
|
-
return c.html(_jsx(SuccessMessage, { message: "Hello from HTMX!" }));
|
|
22
|
-
});
|
|
23
|
-
// Session status endpoint
|
|
24
|
-
app.get("/api/session/status", (c) => {
|
|
25
|
-
const state = sessionManager.getState();
|
|
26
|
-
const status = state.initialized ? "✅ Initialized" : "❌ Not Initialized";
|
|
27
|
-
const lastInit = state.lastInitialized
|
|
28
|
-
? new Date(state.lastInitialized).toLocaleString()
|
|
29
|
-
: "Never";
|
|
30
|
-
const csrfToken = state.csrf || "None";
|
|
31
|
-
const jsessionid = state.cookies.get("JSESSIONID") || "None";
|
|
32
|
-
const serverid = state.cookies.get("SERVERID") || "None";
|
|
33
|
-
return c.html(_jsxs("div", { style: "font-family: monospace; padding: 1rem; background: #f5f5f5; border-radius: 4px; color: #000;", children: [_jsxs("div", { style: "margin-bottom: 0.5rem;", children: [_jsx("strong", { children: "Status:" }), " ", status] }), _jsxs("div", { style: "margin-bottom: 0.5rem;", children: [_jsx("strong", { children: "Last Initialized:" }), " ", lastInit] }), _jsxs("div", { style: "margin-bottom: 0.5rem;", children: [_jsx("strong", { children: "Total Cookies:" }), " ", state.cookies.size] }), _jsxs("div", { style: "margin-bottom: 0.5rem;", children: [_jsx("strong", { children: "CSRF Token:" }), " ", csrfToken] }), _jsxs("div", { style: "margin-bottom: 0.5rem;", children: [_jsx("strong", { children: "JSESSIONID:" }), " ", jsessionid] }), _jsxs("div", { children: [_jsx("strong", { children: "SERVERID:" }), " ", serverid] })] }));
|
|
34
|
-
});
|
|
35
|
-
// Manual session refresh endpoint
|
|
36
|
-
app.post("/api/session/refresh", async (c) => {
|
|
37
|
-
try {
|
|
38
|
-
await sessionManager.refresh();
|
|
39
|
-
return c.json({ success: true, message: "Session refreshed successfully" });
|
|
40
|
-
}
|
|
41
|
-
catch (error) {
|
|
42
|
-
return c.json({ success: false, error: String(error) }, 500);
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
// Login endpoint
|
|
46
|
-
app.post("/api/login", async (c) => {
|
|
47
|
-
try {
|
|
48
|
-
const body = await c.req.json();
|
|
49
|
-
const { username, password, regNo } = body;
|
|
50
|
-
if (!username || !password || !regNo) {
|
|
51
|
-
return c.json({
|
|
52
|
-
success: false,
|
|
53
|
-
error: "Username, password, and registration number are required",
|
|
54
|
-
}, 400);
|
|
55
|
-
}
|
|
56
|
-
console.log(`🔐 Login requested for user: ${username} (${regNo})`);
|
|
57
|
-
const success = await sessionManager.login(username, password, regNo);
|
|
58
|
-
if (success) {
|
|
59
|
-
return c.json({
|
|
60
|
-
success: true,
|
|
61
|
-
message: "Login successful",
|
|
62
|
-
username: sessionManager.getUsername(),
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
return c.json({
|
|
67
|
-
success: false,
|
|
68
|
-
error: "Login failed - check credentials or try again",
|
|
69
|
-
}, 401);
|
|
70
|
-
}
|
|
15
|
+
if (sessionManager.isLoggedIn()) {
|
|
16
|
+
return c.html(_jsx(Dashboard, { username: sessionManager.getUsername() }));
|
|
71
17
|
}
|
|
72
|
-
|
|
73
|
-
console.error("Login error:", error);
|
|
74
|
-
return c.json({ success: false, error: String(error) }, 500);
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
// Login status endpoint
|
|
78
|
-
app.get("/api/login/status", (c) => {
|
|
79
|
-
return c.json({
|
|
80
|
-
loggedIn: sessionManager.isLoggedIn(),
|
|
81
|
-
username: sessionManager.getUsername(),
|
|
82
|
-
});
|
|
18
|
+
return c.html(_jsx(Login, {}));
|
|
83
19
|
});
|
|
84
|
-
// Login status HTML endpoint
|
|
85
|
-
app.get("/api/login/status/html", (c) => {
|
|
86
|
-
const isLoggedIn = sessionManager.isLoggedIn();
|
|
87
|
-
const username = sessionManager.getUsername();
|
|
88
|
-
if (isLoggedIn) {
|
|
89
|
-
return c.html(_jsxs("div", { style: "padding: 1rem; background: #d4edda; border: 1px solid #c3e6cb; border-radius: 4px; color: #155724;", children: [_jsx("strong", { children: "\u2705 Logged In" }), _jsxs("div", { style: "margin-top: 0.5rem;", children: ["Username: ", _jsx("code", { children: username })] })] }));
|
|
90
|
-
}
|
|
91
|
-
else {
|
|
92
|
-
return c.html(_jsxs("div", { style: "padding: 1rem; background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px; color: #721c24;", children: [_jsx("strong", { children: "\u274C Not Logged In" }), _jsx("div", { style: "margin-top: 0.5rem;", children: "Please use the login form above." })] }));
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
// Debug logs storage
|
|
96
|
-
const debugLogs = [];
|
|
97
|
-
function addDebugLog(level, message) {
|
|
98
|
-
const timestamp = new Date().toLocaleTimeString();
|
|
99
|
-
debugLogs.push({ timestamp, level, message });
|
|
100
|
-
// Keep only last 100 logs
|
|
101
|
-
if (debugLogs.length > 100) {
|
|
102
|
-
debugLogs.shift();
|
|
103
|
-
}
|
|
104
|
-
console.log(`[${level}] ${message}`);
|
|
105
|
-
}
|
|
106
|
-
// Form-based login endpoint (for HTMX)
|
|
107
20
|
app.post("/api/login/form", async (c) => {
|
|
108
21
|
try {
|
|
109
22
|
const formData = await c.req.parseBody();
|
|
@@ -111,98 +24,42 @@ app.post("/api/login/form", async (c) => {
|
|
|
111
24
|
const password = formData["password"];
|
|
112
25
|
const regNo = formData["regNo"];
|
|
113
26
|
if (!username || !password || !regNo) {
|
|
114
|
-
|
|
115
|
-
return c.html(_jsxs("div", { style: "padding: 1rem; background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px; color: #721c24;", children: [_jsx("strong", { children: "\u274C Error:" }), " Username, password, and registration number are required."] }));
|
|
27
|
+
return c.html(_jsx("div", { id: "error-message", class: "mt-4 p-3 bg-red-500/10 border border-red-500/50 rounded-md text-red-500 text-sm", children: "Missing credentials. Please try again." }));
|
|
116
28
|
}
|
|
117
|
-
addDebugLog("INFO", `Login requested for user: ${username} (${regNo})`);
|
|
118
|
-
// Show that login is in progress
|
|
119
|
-
const startTime = Date.now();
|
|
120
|
-
addDebugLog("INFO", "Starting login process - polling for text CAPTCHA...");
|
|
121
29
|
const success = await sessionManager.login(username, password, regNo);
|
|
122
|
-
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
123
30
|
if (success) {
|
|
124
|
-
|
|
125
|
-
return c.html(_jsxs("div", { style: "padding: 1rem; background: #d4edda; border: 1px solid #c3e6cb; border-radius: 4px; color: #155724;", children: [_jsx("strong", { children: "\uD83C\uDF89 Login Successful!" }), _jsxs("div", { style: "margin-top: 0.5rem;", children: ["Welcome, ", _jsx("strong", { children: username })] }), _jsxs("div", { style: "margin-top: 0.25rem; font-size: 0.85rem;", children: ["Login took ", duration, " seconds"] })] }));
|
|
31
|
+
return c.html(_jsx(Dashboard, { username: username }));
|
|
126
32
|
}
|
|
127
33
|
else {
|
|
128
|
-
|
|
129
|
-
return c.html(_jsxs("div", { style: "padding: 1rem; background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px; color: #721c24;", children: [_jsx("strong", { children: "\u274C Login Failed" }), _jsx("div", { style: "margin-top: 0.5rem;", children: "Check your credentials or try again. VTOP might be showing reCAPTCHA." }), _jsxs("div", { style: "margin-top: 0.25rem; font-size: 0.85rem;", children: ["Attempt took ", duration, " seconds"] })] }));
|
|
34
|
+
return c.html(_jsx("div", { id: "error-message", class: "mt-4 p-3 bg-red-500/10 border border-red-500/50 rounded-md text-red-500 text-sm", children: "Login failed. Invalid credentials or captcha error." }));
|
|
130
35
|
}
|
|
131
36
|
}
|
|
132
37
|
catch (error) {
|
|
133
|
-
|
|
134
|
-
return c.html(_jsxs("div", { style: "padding: 1rem; background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px; color: #721c24;", children: [_jsx("strong", { children: "\u274C Error:" }), " ", String(error)] }));
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
// Debug logs endpoint
|
|
138
|
-
app.get("/api/debug/logs", (c) => {
|
|
139
|
-
if (debugLogs.length === 0) {
|
|
140
|
-
return c.html(_jsx("div", { style: "color: #888;", children: "No logs yet. Try logging in to see debug output." }));
|
|
141
|
-
}
|
|
142
|
-
return c.html(_jsx("div", { children: debugLogs.map((log, i) => (_jsxs("div", { style: {
|
|
143
|
-
color: log.level === "ERROR"
|
|
144
|
-
? "#ff6b6b"
|
|
145
|
-
: log.level === "SUCCESS"
|
|
146
|
-
? "#51cf66"
|
|
147
|
-
: log.level === "WARN"
|
|
148
|
-
? "#fcc419"
|
|
149
|
-
: "#0f0",
|
|
150
|
-
marginBottom: "0.25rem",
|
|
151
|
-
}, children: [_jsxs("span", { style: "color: #888;", children: ["[", log.timestamp, "]"] }), " ", _jsxs("span", { style: "font-weight: bold;", children: ["[", log.level, "]"] }), " ", log.message] }, i))) }));
|
|
152
|
-
});
|
|
153
|
-
// Clear debug logs endpoint
|
|
154
|
-
app.post("/api/debug/logs/clear", (c) => {
|
|
155
|
-
debugLogs.length = 0;
|
|
156
|
-
return c.html(_jsx("div", { style: "color: #888;", children: "Logs cleared." }));
|
|
157
|
-
});
|
|
158
|
-
// Assignments JSON endpoint
|
|
159
|
-
app.get("/api/assignments", async (c) => {
|
|
160
|
-
if (!sessionManager.isLoggedIn()) {
|
|
161
|
-
return c.json({ success: false, error: "Not logged in" }, 401);
|
|
162
|
-
}
|
|
163
|
-
try {
|
|
164
|
-
const assignments = await sessionManager.fetchUpcomingAssignments();
|
|
165
|
-
return c.json({ success: true, assignments });
|
|
166
|
-
}
|
|
167
|
-
catch (error) {
|
|
168
|
-
return c.json({ success: false, error: String(error) }, 500);
|
|
38
|
+
return c.html(_jsxs("div", { id: "error-message", class: "mt-4 p-3 bg-red-500/10 border border-red-500/50 rounded-md text-red-500 text-sm", children: ["Server error: ", String(error)] }));
|
|
169
39
|
}
|
|
170
40
|
});
|
|
171
|
-
// Assignments HTML endpoint (for HTMX)
|
|
172
41
|
app.get("/api/assignments/html", async (c) => {
|
|
173
42
|
if (!sessionManager.isLoggedIn()) {
|
|
174
|
-
|
|
43
|
+
// If session expired, redirect/render login
|
|
44
|
+
return c.html(_jsx("div", { class: "text-red-500", children: "Session expired. Please refresh to log in again." }));
|
|
175
45
|
}
|
|
176
46
|
try {
|
|
177
47
|
const assignments = await sessionManager.fetchUpcomingAssignments();
|
|
178
48
|
if (assignments.length === 0) {
|
|
179
|
-
return c.html(_jsxs("div", {
|
|
49
|
+
return c.html(_jsxs("div", { class: "p-12 text-center bg-surface border border-border rounded-lg", children: [_jsx("h3", { class: "text-lg font-medium mb-2", children: "No Upcoming Assignments" }), _jsx("p", { class: "text-muted", children: "You are all caught up!" })] }));
|
|
180
50
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
border: "1px solid rgba(255,255,255,0.1)",
|
|
186
|
-
color: "#fff",
|
|
187
|
-
boxShadow: "0 4px 15px rgba(0,0,0,0.2)",
|
|
188
|
-
}, children: [_jsxs("div", { style: "display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.75rem;", children: [_jsxs("div", { children: [_jsx("div", { style: "font-size: 0.85rem; color: #64ffda; font-weight: 600;", children: ass.courseCode }), _jsx("div", { style: "font-size: 0.8rem; color: #aaa;", children: ass.courseName })] }), _jsx("div", { style: {
|
|
189
|
-
padding: "0.25rem 0.75rem",
|
|
190
|
-
background: ass.status?.toLowerCase().includes("pending")
|
|
191
|
-
? "#ff6b6b"
|
|
192
|
-
: "#4CAF50",
|
|
193
|
-
borderRadius: "20px",
|
|
194
|
-
fontSize: "0.75rem",
|
|
195
|
-
fontWeight: "bold",
|
|
196
|
-
}, children: ass.status || "Pending" })] }), _jsx("div", { style: "font-weight: bold; font-size: 1rem; margin-bottom: 0.5rem;", children: ass.assignmentTitle }), _jsxs("div", { style: "display: flex; justify-content: space-between; font-size: 0.85rem; color: #aaa;", children: [_jsxs("span", { children: ["\uD83D\uDCC5 Due: ", ass.dueDate || "N/A"] }), _jsxs("span", { children: ["\uD83D\uDCCA Max: ", ass.maxMarks || "N/A", " marks"] })] })] }, i)))] }));
|
|
51
|
+
//ass
|
|
52
|
+
return c.html(_jsx("div", { class: "flex flex-col gap-3", children: assignments.map((ass, i) => (_jsxs("div", { class: "flex items-center justify-between p-4 bg-surface border border-border rounded-lg transition-colors hover:border-muted group", children: [_jsxs("div", { class: "flex-1 min-w-0 pr-4", children: [_jsxs("div", { class: "flex items-baseline gap-3 overflow-hidden whitespace-nowrap text-ellipsis", children: [_jsx("span", { class: "text-xs font-bold text-muted uppercase tracking-wider min-w-fit", children: ass.courseCode }), _jsx("span", { class: "font-semibold text-sm truncate", children: ass.assignmentTitle }), _jsxs("span", { class: "text-xs text-muted", children: ["\u2014 ", ass.courseName] })] }), _jsxs("div", { class: "flex gap-4 mt-1 text-xs text-muted", children: [_jsxs("span", { children: ["Due:", " ", _jsx("span", { class: "text-foreground", children: ass.dueDate || "N/A" })] }), _jsxs("span", { children: ["Max:", " ", _jsx("span", { class: "text-foreground", children: ass.maxMarks || "N/A" })] })] })] }), _jsx("span", { class: `text-xs px-2.5 py-1 rounded-full font-medium whitespace-nowrap ${ass.status?.toLowerCase().includes("pending")
|
|
53
|
+
? "bg-red-500/10 text-red-500 border border-red-500/20"
|
|
54
|
+
: "bg-blue-500/10 text-blue-500 border border-blue-500/20"}`, children: ass.status || "Pending" })] }, i))) }));
|
|
197
55
|
}
|
|
198
56
|
catch (error) {
|
|
199
|
-
return c.html(_jsxs("div", {
|
|
57
|
+
return c.html(_jsxs("div", { class: "p-4 border border-red-500 rounded-md text-red-500", children: ["Failed to load assignments: ", String(error)] }));
|
|
200
58
|
}
|
|
201
59
|
});
|
|
202
60
|
serve({
|
|
203
61
|
fetch: app.fetch,
|
|
204
62
|
port: 6767,
|
|
205
63
|
}, (info) => {
|
|
206
|
-
console.log(
|
|
207
|
-
console.log(`📊 Session status: http://localhost:${info.port}/api/session/status`);
|
|
64
|
+
console.log(`Server is running on http://localhost:${info.port}`);
|
|
208
65
|
});
|
package/dist/session-manager.js
CHANGED
|
@@ -10,7 +10,6 @@ const OPEN_PAGE = `${BASE}/vtop/openPage`;
|
|
|
10
10
|
const OPEN_PAGE_ALT = `${BASE}/vtop/open/page`;
|
|
11
11
|
const PRELOGIN_SETUP = `${BASE}/vtop/prelogin/setup`;
|
|
12
12
|
const LOGIN_PAGE = `${BASE}/vtop/login`;
|
|
13
|
-
// Post-login endpoints
|
|
14
13
|
const INIT_PAGE = `${BASE}/vtop/init/page`;
|
|
15
14
|
const MAIN_PAGE = `${BASE}/vtop/main/page`;
|
|
16
15
|
const VTOP_OPEN = `${BASE}/vtop/open`;
|
|
@@ -27,9 +26,7 @@ class VTOPSessionManager {
|
|
|
27
26
|
username: null,
|
|
28
27
|
regNo: null,
|
|
29
28
|
};
|
|
30
|
-
|
|
31
|
-
* Extract CSRF token from HTML response
|
|
32
|
-
*/
|
|
29
|
+
//cheerio at home
|
|
33
30
|
extractCsrf(html) {
|
|
34
31
|
// Look for _csrf token in various forms
|
|
35
32
|
const patterns = [
|
|
@@ -46,9 +43,6 @@ class VTOPSessionManager {
|
|
|
46
43
|
}
|
|
47
44
|
return null;
|
|
48
45
|
}
|
|
49
|
-
/**
|
|
50
|
-
* Parse Set-Cookie headers and store cookies
|
|
51
|
-
*/
|
|
52
46
|
storeCookies(response) {
|
|
53
47
|
const setCookieHeaders = response.headers.getSetCookie?.() || [];
|
|
54
48
|
for (const cookieStr of setCookieHeaders) {
|
|
@@ -59,9 +53,6 @@ class VTOPSessionManager {
|
|
|
59
53
|
}
|
|
60
54
|
}
|
|
61
55
|
}
|
|
62
|
-
/**
|
|
63
|
-
* Get cookie header string for requests
|
|
64
|
-
*/
|
|
65
56
|
getCookieHeader() {
|
|
66
57
|
return Array.from(this.state.cookies.entries())
|
|
67
58
|
.map(([name, value]) => `${name}=${value}`)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
2
|
+
import { BaseLayout } from "./layouts/Base.js";
|
|
3
|
+
export const Dashboard = ({ username }) => {
|
|
4
|
+
return (_jsxs(BaseLayout, { title: "Dashboard - Open-VTOP", children: [_jsxs("div", { class: "flex justify-between items-center mb-8", children: [_jsxs("div", { children: [_jsx("h1", { class: "text-2xl font-bold tracking-tight", children: "Dashboard" }), _jsxs("p", { class: "text-muted text-sm", children: ["Welcome back, ", username] })] }), _jsx("div", {})] }), _jsx("div", { "hx-get": "/api/assignments/html", "hx-trigger": "load", "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-16 text-muted", children: [_jsx("div", { class: "inline-block animate-spin rounded-full h-6 w-6 border-2 border-muted border-t-white mb-4" }), _jsx("p", { class: "text-sm", children: "Syncing assignments..." })] }) }) })] }));
|
|
5
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
2
|
+
import { BaseLayout } from "./layouts/Base.js";
|
|
3
|
+
export const Login = () => {
|
|
4
|
+
return (_jsx(BaseLayout, { title: "Login - Open-VTOP", children: _jsxs("div", { "x-data": "{ \n showPassword: false, \n loading: false \n }", class: "w-full", children: [_jsxs("div", { class: "text-center mb-8", children: [_jsx("h1", { class: "text-3xl font-bold tracking-tight mb-2", children: "Log In" }), _jsx("p", { class: "text-muted text-sm", children: "Enter your VTOP credentials to continue." })] }), _jsx("div", { class: "bg-surface border border-border rounded-lg p-6 shadow-sm", children: _jsxs("form", { id: "login-form", "hx-post": "/api/login/form", "hx-target": "body", "hx-swap": "outerHTML", "x-on:submit": "loading = true", "x-on:htmx:after-request": "loading = false", children: [_jsxs("div", { class: "mb-4", children: [_jsx("label", { class: "block text-sm text-muted mb-2", for: "username", children: "Username" }), _jsx("input", { type: "text", id: "username", name: "username", required: true, autofocus: true, class: "w-full bg-background border border-border rounded-md px-3 py-2 text-foreground focus:outline-none focus:border-white transition-colors placeholder-muted" })] }), _jsxs("div", { class: "mb-4", children: [_jsx("label", { class: "block text-sm text-muted mb-2", for: "password", children: "Password" }), _jsxs("div", { class: "relative", children: [_jsx("input", { "x-bind:type": "showPassword ? 'text' : 'password'", id: "password", name: "password", required: true, class: "w-full bg-background border border-border rounded-md px-3 py-2 text-foreground focus:outline-none focus:border-white transition-colors placeholder-muted pr-10" }), _jsxs("button", { type: "button", "x-on:click": "showPassword = !showPassword", class: "absolute inset-y-0 right-0 px-3 flex items-center text-muted hover:text-foreground transition-colors", children: [_jsx("span", { "x-show": "!showPassword", class: "text-xs font-medium", children: "SHOW" }), _jsx("span", { "x-show": "showPassword", style: "display: none;", class: "text-xs font-medium", children: "HIDE" })] })] })] }), _jsxs("div", { class: "mb-6", children: [_jsx("label", { class: "block text-sm text-muted mb-2", for: "regNo", children: "Registration Number" }), _jsx("input", { type: "text", id: "regNo", name: "regNo", required: true, class: "w-full bg-background border border-border rounded-md px-3 py-2 text-foreground focus:outline-none focus:border-white transition-colors placeholder-muted" })] }), _jsxs("button", { type: "submit", class: "w-full bg-white text-black font-semibold py-2.5 rounded-md hover:bg-gray-200 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex justify-center items-center gap-2", "x-bind:disabled": "loading", children: [_jsx("span", { "x-show": "!loading", children: "Log In" }), _jsxs("span", { "x-show": "loading", style: "display: none;", class: "flex items-center gap-2", children: [_jsxs("svg", { class: "animate-spin h-4 w-4 text-black", xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", children: [_jsx("circle", { class: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", "stroke-width": "4" }), _jsx("path", { class: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })] }), "Authenticating..."] })] }), _jsx("div", { id: "error-message" })] }) }), _jsx("div", { class: "text-center mt-8 text-muted text-xs", children: _jsx("p", { children: "Open-VTOP \u00A9 2026" }) })] }) }));
|
|
5
|
+
};
|
|
@@ -1,121 +1,43 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
2
|
-
export const BaseLayout = ({ title
|
|
3
|
-
return (_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [_jsx("meta", { charset: "UTF-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }), _jsx("title", { children: title }), _jsx("script", { src: "/static/htmx.js" }), _jsx("
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
export const BaseLayout = ({ title, children, }) => {
|
|
3
|
+
return (_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [_jsx("meta", { charset: "UTF-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }), _jsx("title", { children: title }), _jsx("script", { src: "/static/htmx.js" }), _jsx("script", { src: "https://cdn.tailwindcss.com" }), _jsx("script", { dangerouslySetInnerHTML: {
|
|
4
|
+
__html: `
|
|
5
|
+
tailwind.config = {
|
|
6
|
+
theme: {
|
|
7
|
+
extend: {
|
|
8
|
+
colors: {
|
|
9
|
+
background: '#000000',
|
|
10
|
+
surface: '#111111',
|
|
11
|
+
border: '#333333',
|
|
12
|
+
foreground: '#ffffff',
|
|
13
|
+
muted: '#888888',
|
|
14
|
+
primary: '#ffffff',
|
|
15
|
+
'primary-fg': '#000000',
|
|
16
|
+
},
|
|
17
|
+
fontFamily: {
|
|
18
|
+
sans: ['Inter', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'],
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
`,
|
|
24
|
+
} }), _jsx("script", { defer: true, src: "https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js" }), _jsx("style", { dangerouslySetInnerHTML: {
|
|
25
|
+
__html: `
|
|
10
26
|
body {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
font-size: 1.25rem;
|
|
28
|
-
font-weight: 500;
|
|
29
|
-
margin-bottom: 1rem;
|
|
30
|
-
letter-spacing: -0.01em;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
.container {
|
|
34
|
-
max-width: 800px;
|
|
35
|
-
margin: 0 auto;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
.section {
|
|
39
|
-
background: #111;
|
|
40
|
-
border: 1px solid #333;
|
|
41
|
-
border-radius: 8px;
|
|
42
|
-
padding: 2rem;
|
|
43
|
-
margin-bottom: 2rem;
|
|
44
|
-
transition: border-color 0.2s ease;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
.section:hover {
|
|
48
|
-
border-color: #555;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
button {
|
|
52
|
-
background: #fff;
|
|
53
|
-
color: #000;
|
|
54
|
-
border: none;
|
|
55
|
-
padding: 0.75rem 1.5rem;
|
|
56
|
-
border-radius: 6px;
|
|
57
|
-
cursor: pointer;
|
|
58
|
-
font-size: 0.875rem;
|
|
59
|
-
font-family: inherit;
|
|
60
|
-
font-weight: 500;
|
|
61
|
-
transition: all 0.2s ease;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
button:hover {
|
|
65
|
-
background: #e6e6e6;
|
|
66
|
-
transform: translateY(-1px);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
button:active {
|
|
70
|
-
transform: translateY(0);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
input {
|
|
74
|
-
background: #111;
|
|
75
|
-
color: #fff;
|
|
76
|
-
border: 1px solid #333;
|
|
77
|
-
padding: 0.75rem 1rem;
|
|
78
|
-
border-radius: 6px;
|
|
79
|
-
font-size: 0.875rem;
|
|
80
|
-
font-family: inherit;
|
|
81
|
-
width: 100%;
|
|
82
|
-
transition: border-color 0.2s ease;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
input:focus {
|
|
86
|
-
outline: none;
|
|
87
|
-
border-color: #fff;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
input::placeholder {
|
|
91
|
-
color: #666;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
#result {
|
|
95
|
-
margin-top: 1rem;
|
|
96
|
-
padding: 1rem;
|
|
97
|
-
background: #0a0a0a;
|
|
98
|
-
border: 1px solid #222;
|
|
99
|
-
border-radius: 6px;
|
|
100
|
-
min-height: 2rem;
|
|
101
|
-
font-size: 0.875rem;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
.success {
|
|
105
|
-
color: #0f0;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
.error {
|
|
109
|
-
color: #f00;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
.htmx-swapping {
|
|
113
|
-
opacity: 0.5;
|
|
114
|
-
transition: opacity 0.2s;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
.htmx-request {
|
|
118
|
-
opacity: 0.8;
|
|
119
|
-
}
|
|
120
|
-
` })] }), _jsx("body", { children: _jsx("div", { class: "container", children: children }) })] }));
|
|
27
|
+
background-color: theme('colors.background');
|
|
28
|
+
color: theme('colors.foreground');
|
|
29
|
+
}
|
|
30
|
+
/* Custom spinner animation since Tailwind's animate-spin is utility based */
|
|
31
|
+
.htmx-indicator {
|
|
32
|
+
display: none;
|
|
33
|
+
opacity: 0;
|
|
34
|
+
transition: opacity 200ms ease-in;
|
|
35
|
+
}
|
|
36
|
+
.htmx-request .htmx-indicator,
|
|
37
|
+
.htmx-request.htmx-indicator {
|
|
38
|
+
display: inline-block;
|
|
39
|
+
opacity: 1;
|
|
40
|
+
}
|
|
41
|
+
`,
|
|
42
|
+
} })] }), _jsx("body", { class: "bg-background text-foreground antialiased min-h-screen flex flex-col items-center justify-center p-4", children: _jsx("main", { class: "w-full max-w-md flex flex-col gap-6", children: children }) })] }));
|
|
121
43
|
};
|
package/package.json
CHANGED