Merge branch 'main' into docs/CONTRACT.md

This commit is contained in:
Jose Daniel G. Percy 2025-04-08 23:08:43 +08:00
commit 13fd32d038
10 changed files with 219 additions and 204 deletions

143
README.md
View File

@ -1,38 +1,40 @@
# Learner Management System Frontend # Learner Management System Frontend
This is the frontend for a modular and responsive Learner Management System (LMS), built with Vanilla Typescript, Vite, and Bootstrap v5.3. It utilizes a widget-based system and a global API structure designed to interact with a backend server. This is the frontend for a modular and responsive Learner Management System (LMS), built with Vanilla Typescript, Vite, and Bootstrap v5.3. It utilizes a widget-based system and interacts with a dedicated backend server via a RESTful API using PAKE SRP for authentication.
## Features ## Features
* **Modular Architecture:** Organized into modules for easy maintainability and scalability. * **Modular Architecture:** Organized into distinct modules (widgets, layouts, pages, API) for maintainability and scalability.
* **Responsive Design:** Built with Bootstrap 5.3 to ensure responsiveness across various devices. * **Responsive Design:** Uses Bootstrap 5.3 for a consistent experience across desktops, tablets, and mobiles.
* **Widget System:** Extensible widget system with two size types (default full-width and icon-type for sidebar). * **Widget System:** Extensible widgets with `default` (full-width) and `icon` (sidebar) sizes.
* **Global API System:** Clearly defined API structure for communication with a backend (currently mocked). * **API Integration:** Connects to a live backend API (default: `http://localhost:8080/api`) for data fetching and actions.
* **Layout Modules:** Pre-built layouts (Centered, Three-Column, Split-Column) for different page structures. * **Secure Authentication:** Implements Password-Authenticated Key Exchange (PAKE SRP) for secure login via the `thin-srp` library.
* **Page Components:** Includes pre-built pages for Login, Register, Dashboard, Settings, Admin, Profile, and Manage Students. * **Token-Based Sessions:** Manages user sessions using tokens obtained from the backend upon successful login (stored in localStorage by default).
* **Topbar Module:** Navigation and user profile dropdown. * **Layout Modules:** Provides pre-built layouts: `CenteredLayout`, `ThreeColumnLayout`, `SplitColumnLayout`.
* **Modal Module:** (To be implemented) For reusable modal components. * **Core Pages:** Includes pages for Login, Register (info only), Dashboard, Profile, Account Settings (Modal), Admin Dashboard, Manage Students.
* **Basic Authentication Flow:** Login and logout functionality with token-based session management (localStorage). * **Topbar Module:** Features main navigation and a user profile dropdown with account actions.
* **Modal Module:** Provides a reusable container for modal dialogs (e.g., Account Settings, notifications).
## Technologies Used ## Technologies Used
* **Vanilla Typescript:** For a type-safe and maintainable codebase. * **Vanilla Typescript:** For a robust, type-safe, and maintainable codebase without framework overhead.
* **Vite:** For fast and efficient development and build process. * **Vite:** For an extremely fast development server and optimized build process.
* **Bootstrap v5.3:** For responsive layout and styling. * **Bootstrap v5.3:** For responsive layout, styling components, and utility classes.
* **pnpm:** Package manager for efficient dependency management. * **pnpm:** Efficient package manager for dependency management.
* **argon2-browser:** (For demonstration purposes - **Backend Hashing Recommended**) For frontend password hashing demonstration (Argon2id). **Important:** In a production environment, password hashing should be performed on the backend for security reasons. * **thin-srp:** JavaScript client library for Secure Remote Password (SRP) protocol implementation.
## Prerequisites ## Prerequisites
* **Node.js** (>= 18 recommended) * **Node.js** (>= 18 recommended)
* **pnpm** (Install globally: `npm install -g pnpm`) * **pnpm** (Install globally: `npm install -g pnpm`)
* **Running Backend:** The LMS backend service must be running (see backend README) and accessible (defaults to `http://localhost:8080`).
## Installation and Setup ## Installation and Setup
1. **Clone the repository:** 1. **Clone the repository:**
```bash ```bash
git clone <repository_url> git clone <your-frontend-repository-url>
cd lms-frontend cd lms-frontend
``` ```
@ -42,85 +44,86 @@ This is the frontend for a modular and responsive Learner Management System (LMS
pnpm install pnpm install
``` ```
3. **Start the development server:** 3. **Configure API Base URL (Optional):**
* If your backend runs on a different address or port, update the `API_BASE_URL` constant in `src/api/api.ts`.
4. **Start the development server:**
```bash ```bash
pnpm dev pnpm dev
``` ```
This will start the Vite development server. Open your browser and navigate to the address provided in the console (usually `http://localhost:5173/`). This will start the Vite development server. Open your browser and navigate to the address provided (usually `http://localhost:5173`).
## Project Structure ## Project Structure
```bash ```bash
lms-frontend/ lms-frontend/
├── index.html # Main HTML entry point ├── index.html # Main HTML entry point
├── pnpm-lock.yaml # pnpm lock file for dependency management ├── package.json # Project dependencies and scripts
├── pnpm-workspace.yaml # pnpm workspace configuration ├── pnpm-lock.yaml # pnpm lock file
├── public/ # Public assets (images, etc.) ├── public/ # Static assets served directly
├── README.md # This README file ├── README.md # This README file
├── src/ # Source code directory ├── src/ # Source code directory
│ ├── api/ # API interaction functions (mocked in api.ts) │ ├── api/
│ ├── assets/ # Static assets (images, icons) │ │ └── api.ts # Functions for interacting with the backend API (SRP, data fetching)
│ ├── components/ # Reusable components │ ├── assets/ # Static assets processed by Vite (images, icons)
│ │ ├── layouts/ # Page layout components (CenteredLayout, ThreeColumnLayout, SplitColumnLayout) │ ├── components/
│ │ ├── modules/ # Modules (TopbarModule, ModalModule - to be implemented) │ │ ├── layouts/ # Page layout components
│ │ ├── widgets/ # Widget components (LoginWidget, ButtonWidget, etc.) │ │ ├── modules/ # Larger UI modules (TopbarModule, ModalModule)
│ ├── main.ts # Main entry point for the application │ │ └── widgets/ # Reusable UI widgets
│ ├── pages/ # Page components (LoginPage, DashboardPage, etc.) │ ├── main.ts # Application entry point, initializes router/app state
│ ├── styles/ # Global styles and Bootstrap import (index.css) │ ├── pages/ # Page-level components/logic
│ ├── types/ # Typescript interfaces (Widget.ts, User.ts) │ ├── styles/
│ ├── utils/ # Utility functions (api.ts, auth.ts) │ │ └── index.css # Global styles, Bootstrap import
│ ├── vite-env.d.ts # Vite environment declaration │ ├── types/ # TypeScript interfaces and type definitions
├── tsconfig.json # Typescript configuration │ ├── utils/
├── vite.config.ts # Vite configuration │ │ └── utils.ts # Utility functions (auth state management, storage)
│ └── vite-env.d.ts # Vite environment type declarations
├── tsconfig.json # TypeScript configuration
├── tsconfig.node.json # TypeScript configuration for Node contexts (e.g., Vite config)
└── vite.config.ts # Vite build tool configuration
``` ```
## Widget System ## Widget System
The frontend is built around a widget system. Widgets are independent, reusable components that display specific information or functionality. The UI is composed of reusable widgets found in `src/components/widgets/`. Each widget encapsulates specific functionality or displays data. They support different sizes (`default`, `icon`) for adaptability within various layouts.
* **Widget Sizes:**
* `default`: Full column width, suitable for most widgets.
* `icon`: Smaller size, designed for use in collapsed sidebars or icon-based menus.
* **Widget Components:** Located in `src/components/widgets/`. Examples include:
* `LoginWidget`: Login form.
* `RegisterWidget`: Registration information display.
* `ButtonWidget`: Reusable button component.
* `StudentCountWidget`, `TeacherCountWidget`, `ProfileInfoWidget`, `PostFeedWidget`, `StudentTableWidget`, `TuitionFeeWidget`: Placeholder widgets to be implemented.
## API System ## API System
The frontend is designed to interact with a backend through a global API system defined in `src/utils/api.ts`. Defined in `src/api/api.ts`, this module handles all communication with the backend API.
* **Current Implementation:** The `api.ts` file currently contains **mocked API calls** for demonstration purposes. It simulates API responses using timeouts and hardcoded data. * **Live Interaction:** Functions use `fetch` to make requests to the running backend (default: `http://localhost:8080/api`).
* **Backend Integration:** To connect to a real backend, you will need to replace the mocked API calls in `api.ts` with actual `fetch` requests to your backend endpoints. * **SRP Flow:** The `login` function implements the two-step SRP authentication handshake with the backend.
* **Expected Backend Endpoints (Example):** * **Authenticated Requests:** Other API functions automatically include the stored authentication token in the `Authorization: Bearer <token>` header for protected endpoints.
* `POST /api/login`: User login. * **Error Handling:** Includes basic error handling and detection of unauthorized (401) responses.
* `GET /api/user`: Get user data (requires authentication). * **Key Endpoints Used:**
* `POST /api/logout`: User logout. * `POST /api/auth/srp/start`
* `/api/students`, `/api/teachers`, `/api/admin/students`, etc.: Endpoints for managing students, teachers, and other LMS data. * `POST /api/auth/srp/verify`
* `POST /api/auth/logout`
* `GET /api/profile/{user_id}`
* `PUT /api/profile/settings`
* `GET /api/admin/dashboard`
* `GET /api/admin/students`
* `GET /api/admin/students/{student_id}/financials`
## Backend Considerations ## Backend Interaction Notes
* **Password Hashing (Backend Recommended):** While `argon2-browser` is included for frontend password hashing demonstration, **it is strongly recommended to implement password hashing (using Argon2id or similar) and salting on the backend for enhanced security.** The frontend should send passwords securely (HTTPS) to the backend, and the backend should handle the hashing and verification process. * **Authentication:** The backend handles the secure verification of passwords using SRP. The frontend never stores or hashes the raw password itself after the initial SRP calculation during login.
* **Authentication and Authorization:** The backend should implement robust authentication (e.g., JWT or session-based) and authorization mechanisms to secure API endpoints and protect sensitive data. * **Authorization:** Access to specific API endpoints (e.g., admin routes) is controlled by the backend based on the user's role/permissions associated with their session token.
* **Database:** The database schema outlined in the initial prompt should be implemented on the backend to store user data, course information, enrollments, etc. (Refer to the initial prompt for database schema details). * **Data Source:** All dynamic data (user info, student lists, etc.) is fetched from the backend, which interacts with the MariaDB database.
* **Backend Technology:** You can choose any suitable backend technology (Node.js, Python, Java, PHP, etc.) to build the API endpoints and connect to the database.
## Further Development ## Further Development
* **Implement Modal Module:** Create a reusable `ModalModule` component in `src/components/modules/ModalModule.ts` to handle modals for various purposes (e.g., classrooms under construction, account settings). * **Complete Widget Functionality:** Ensure all widgets fetch and display real data from the backend API.
* **Complete Widget Implementations:** Implement the remaining placeholder widgets in `src/components/widgets/` (e.g., `StudentCountWidget`, `TeacherCountWidget`, `ProfileInfoWidget`, `PostFeedWidget`, `StudentTableWidget`, `TuitionFeeWidget`) to display actual data from the backend. * **Admin Page Enhancements:** Implement full CRUD operations for student/teacher management, filtering, batch actions, and enrollment assignments.
* **Account Settings Modal:** Develop the Account Settings Modal to allow users to edit their profile information and change passwords. * **Student Table Widget:** Integrate a robust table library (e.g., Tabulator, TanStack Table) for improved sorting, filtering, and pagination.
* **Admin Page Functionality:** Implement the functionality for the Admin page, including student and teacher management, using appropriate widgets and API calls. * **Classrooms Module:** Build out the Classrooms feature beyond the placeholder modal.
* **Student Table Widget:** Integrate a table library (e.g., Tabulator, DataTables) into the `StudentTableWidget` for enhanced table features like sorting, filtering, and pagination. * **Robust Error Handling:** Implement more comprehensive error handling and user feedback mechanisms throughout the UI.
* **Classrooms Page/Modal:** Implement the Classrooms feature, potentially starting with a modal indicating "Under Construction" as initially requested, and then expanding to a full Classrooms page with relevant widgets. * **Client-Side Validation:** Add more input validation for forms (e.g., account settings).
* **Form Validation:** Add client-side form validation to login, registration (if implemented), and account settings forms for better user experience. * **UI/UX Polish:** Refine styling, transitions, and interactions for a smoother user experience.
* **Error Handling:** Improve error handling in API calls and display user-friendly error messages. * **Testing:** Implement unit and potentially end-to-end tests.
* **Styling and UI Polish:** Enhance the visual design and user interface with custom CSS and Bootstrap components to create a polished and professional LMS frontend. * **State Management:** For larger applications, consider introducing a dedicated state management library (like Zustand, Pinia, Redux Toolkit) instead of relying solely on `utils.ts`.
* **Backend Integration:** Replace the mocked API calls with real API calls to your backend endpoints to connect the frontend to the backend data and functionality.
## Contributing ## Contributing
@ -128,4 +131,4 @@ The frontend is designed to interact with a backend through a global API system
## License ## License
[View the License](./LICENSE) [View MIT License](./LICENSE)

View File

@ -4,6 +4,9 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LMS Prototype</title> <title>LMS Prototype</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="/src/style.css"> <link rel="stylesheet" href="/src/style.css">
</head> </head>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1011 KiB

BIN
src/assets/images.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -1,30 +0,0 @@
// components/MergedWidget.ts
import { Widget } from './Widget';
export class MergedWidget extends Widget {
private children: Widget[] = [];
constructor(sizeType: 'default' | 'icon' = 'default') {
super(sizeType);
}
addWidget(widget: Widget): void {
this.children.push(widget);
}
render(): HTMLElement {
// Clear current container contents (in case render is called more than once)
this.container.innerHTML = '';
for (const widget of this.children) {
const rendered = widget.render();
// Move child nodes (not the container itself)
while (rendered.firstChild) {
this.container.appendChild(rendered.firstChild);
}
}
return this.container;
}
}

View File

@ -4,11 +4,13 @@ import { globalAPI, ApiResponse, ProfileResponseData } from '../api/api'; // Imp
export class TopBar { export class TopBar {
private container: HTMLElement; private container: HTMLElement;
private menuItems: Array<HTMLElement>;
private profileDropdownVisible: boolean = false; private profileDropdownVisible: boolean = false;
private profileData: ProfileResponseData | null = null; private profileData: ProfileResponseData | null = null;
constructor() { constructor() {
this.container = createElement('nav'); this.container = createElement('nav');
this.menuItems = [];
this.container.classList.add('top-bar', 'navbar', 'navbar-expand-lg', 'navbar-light', 'bg-darker', 'mb-3', 'px-4'); this.container.classList.add('top-bar', 'navbar', 'navbar-expand-lg', 'navbar-light', 'bg-darker', 'mb-3', 'px-4');
this.fetchProfileData(); this.fetchProfileData();
} }
@ -29,6 +31,19 @@ export class TopBar {
} }
} }
addMenuItem(text: string, href: string) {
const menuItem = createElement('li');
menuItem.classList.add('nav-item');
const itemLink = createElement('a');
itemLink.classList.add('nav-link');
itemLink.href = href;
itemLink.textContent = text;
menuItem.appendChild(itemLink);
this.menuItems.push(menuItem)
}
render(): HTMLElement { render(): HTMLElement {
this.container.innerHTML = ''; this.container.innerHTML = '';
@ -54,33 +69,9 @@ export class TopBar {
const menuPages = createElement('ul'); const menuPages = createElement('ul');
menuPages.classList.add('navbar-nav', 'me-auto', 'mb-2', 'mb-lg-0'); menuPages.classList.add('navbar-nav', 'me-auto', 'mb-2', 'mb-lg-0');
const dashboardMenuItem = createElement('li'); for (const item of this.menuItems) {
dashboardMenuItem.classList.add('nav-item'); menuPages.appendChild(item);
const dashboardLink = createElement('a'); }
dashboardLink.classList.add('nav-link');
dashboardLink.href = '#/dashboard';
dashboardLink.textContent = 'Dashboard';
dashboardMenuItem.appendChild(dashboardLink);
menuPages.appendChild(dashboardMenuItem);
const classroomsMenuItem = createElement('li');
classroomsMenuItem.classList.add('nav-item');
const classroomsLink = createElement('a');
classroomsLink.classList.add('nav-link');
classroomsLink.href = '#/classrooms';
classroomsLink.textContent = 'Classrooms';
classroomsMenuItem.appendChild(classroomsLink);
menuPages.appendChild(classroomsMenuItem);
const adminMenuItem = createElement('li');
adminMenuItem.classList.add('nav-item');
const adminLink = createElement('a');
adminLink.classList.add('nav-link');
adminLink.href = '#/admin';
adminLink.textContent = 'Admin';
adminMenuItem.appendChild(adminLink);
menuPages.appendChild(adminMenuItem);
const profileSection = createElement('div'); const profileSection = createElement('div');
profileSection.classList.add('d-flex', 'align-items-center', 'ms-auto'); profileSection.classList.add('d-flex', 'align-items-center', 'ms-auto');

View File

@ -7,9 +7,9 @@ import { createElement } from '../utils/utils';
class WelcomeWidget extends Widget { class WelcomeWidget extends Widget {
render(): HTMLElement { render(): HTMLElement {
const widgetDiv = createElement('div'); const widgetDiv = createElement('div');
widgetDiv.classList.add('widget'); widgetDiv.classList.add('widget', 'welcome-widget');
widgetDiv.innerHTML = ` widgetDiv.innerHTML = `
<div class="widget-header">Welcome to the Dashboard</div> <div class="widget-header centered-header">Welcome to the Dashboard</div>
<div class="widget-body"> <div class="widget-body">
This is your dashboard. More widgets will be added here. This is your dashboard. More widgets will be added here.
</div> </div>
@ -21,7 +21,7 @@ class WelcomeWidget extends Widget {
class QuickLinksWidget extends Widget { class QuickLinksWidget extends Widget {
render(): HTMLElement { render(): HTMLElement {
const widgetDiv = createElement('div'); const widgetDiv = createElement('div');
widgetDiv.classList.add('widget'); widgetDiv.classList.add('widget', 'quick-links-widget');
widgetDiv.innerHTML = ` widgetDiv.innerHTML = `
<div class="widget-header">Quick Links</div> <div class="widget-header">Quick Links</div>
<div class="widget-body"> <div class="widget-body">
@ -36,21 +36,20 @@ class QuickLinksWidget extends Widget {
} }
} }
class PlaceholderWidget extends Widget { // Corrected placeholder widget definition class PlaceholderWidget extends Widget {
render(): HTMLElement { render(): HTMLElement {
const div = createElement('div'); const div = createElement('div');
div.classList.add('widget'); div.classList.add('widget', 'placeholder-widget');
div.textContent = 'Placeholder Widget'; div.textContent = 'Placeholder Widget';
return div; return div;
} }
} }
export const renderDashboardPage = () => { export const renderDashboardPage = () => {
const layout = new ThreeColumnLayout(); const layout = new ThreeColumnLayout();
const welcomeWidget = new WelcomeWidget(); const welcomeWidget = new WelcomeWidget();
const quickLinksWidget = new QuickLinksWidget(); const quickLinksWidget = new QuickLinksWidget();
const placeholderWidget = new PlaceholderWidget(); // Use the corrected PlaceholderWidget class const placeholderWidget = new PlaceholderWidget();
layout.setColumn1Content(welcomeWidget.render()); layout.setColumn1Content(welcomeWidget.render());
layout.setColumn2Content(quickLinksWidget.render()); layout.setColumn2Content(quickLinksWidget.render());

View File

@ -1,4 +1,4 @@
/* Global styles */ /* --- Global styles --- */
h1, h1,
h2, h2,
h3, h3,
@ -12,26 +12,14 @@ body {
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow: hidden; overflow: hidden;
font-family: sans-serif; font-family: 'Roboto', sans-serif;
}
.scrolling-background {
position: fixed;
top: 0;
left: 0;
width: 100%; /* Ensure it covers full width */
height: 100vh; /* Full viewport height */
background-image: url('./assets/bg1.jpg');
background-repeat: repeat-x;
background-size: auto 100%; /* Maintain width and fill height */
animation: scroll-bg 30s linear infinite;
z-index: -2;
} }
@keyframes scroll-bg { @keyframes scroll-bg {
from { from {
background-position: 0 0; background-position: 0 0;
} }
to { to {
background-position: -100% 0; background-position: -100% 0;
} }
@ -47,6 +35,21 @@ body {
z-index: -1; z-index: -1;
} }
.scrolling-background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background-image: url('./assets/after-sunset-minimal-4k-zm-3840x2400.jpg');
background-repeat: repeat-x;
background-size: auto 100%;
animation: scroll-bg 30s linear infinite;
z-index: -2;
}
/* --- Widget Styling --- */
.widget { .widget {
border: 1px solid rgb(0, 0, 0, 0.20); border: 1px solid rgb(0, 0, 0, 0.20);
background-color: rgb(31, 35, 39, 0.9); background-color: rgb(31, 35, 39, 0.9);
@ -55,21 +58,23 @@ body {
border-radius: 5px; border-radius: 5px;
} }
.centered-header {
font-size: 24px;
text-align: center;
}
.widget-header { .widget-header {
margin-bottom: 10px; margin-bottom: 10px;
font-weight: bold; font-weight: bold;
} }
.widget-body {
/* Widget body styles */
}
.icon-widget { .icon-widget {
padding: 10px; padding: 10px;
text-align: center; text-align: center;
} }
/* Layout Specific Styles */ /* --- Layout Specific Styles --- */
.centered-layout { .centered-layout {
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -98,7 +103,7 @@ body {
} }
/* Modal Styles */ /* --- Modal Styles --- */
.modal-overlay { .modal-overlay {
position: fixed; position: fixed;
top: 0; top: 0;
@ -125,7 +130,7 @@ body {
/* Maximum width */ /* Maximum width */
} }
/* Responsive adjustments (example) */ /* --- Responsive adjustments (example) --- */
@media (max-width: 768px) { @media (max-width: 768px) {
.three-column-layout { .three-column-layout {
grid-template-columns: 1fr; grid-template-columns: 1fr;
@ -142,3 +147,39 @@ body {
/* Stack even if collapsed */ /* Stack even if collapsed */
} }
} }
/* --- Custom styles --- */
.login-container {
background-image: url('./assets/images.jpg');
/* Replace with your logo path */
background-repeat: no-repeat;
background-position: center top;
/* Center the logo at the top */
background-size: 80x 80px;
/* Adjust the size of the logo */
padding-top: 60px;
/* Add padding to create space for the logo */
text-align: center;
/* Center text and form elements */
}
.logo {
display: block;
margin: 0 auto 20px;
/* Center the logo and add space below */
max-width: 100px;
/* Adjust the size as needed */
width: 80px;
/* Set the width of the logo */
height: 80px;
/* Set the height of the logo */
border-radius: 50%;
/* Make the logo circular */
background-image: url('./assets/images.jpg');
/* Replace with your logo path */
background-size: cover;
/* Cover the entire area */
margin: 0 auto 20px;
/* Center the logo and add space below */
}

View File

@ -11,52 +11,40 @@ export class LoginWidget extends Widget {
render(): HTMLElement { render(): HTMLElement {
this.container.innerHTML = ''; this.container.innerHTML = '';
// Create a logo container
const logoContainer = createElement('div') as HTMLElement;
logoContainer.classList.add('logo'); // Add class for circular logo
this.container.appendChild(logoContainer);
// Create header
const header = createElement('h2'); const header = createElement('h2');
header.classList.add('widget-header'); header.classList.add('widget-header');
header.textContent = 'Login'; header.textContent = 'Login';
this.container.appendChild(header);
// Create form
const form = createElement('form'); const form = createElement('form');
const userIdInputGroup = createElement('div');
userIdInputGroup.classList.add('mb-3');
const userIdLabel = createElement('label') as HTMLLabelElement; // Cast first
userIdLabel.classList.add('form-label');
userIdLabel.htmlFor = 'userId'; // Set htmlFor as property
userIdLabel.textContent = 'Student ID';
const userIdInput = createElement('input') as HTMLInputElement;
userIdInput.type = 'text';
userIdInput.classList.add('form-control');
userIdInput.id = 'userId';
userIdInput.placeholder = '123456';
userIdInputGroup.appendChild(userIdLabel);
userIdInputGroup.appendChild(userIdInput);
const passwordInputGroup = createElement('div'); // User ID input group
passwordInputGroup.classList.add('mb-3'); const userIdInputGroup = this.createInputGroup('userId', 'Student ID', 'text', 'Student ID');
const passwordLabel = createElement('label') as HTMLLabelElement; // Cast first form.appendChild(userIdInputGroup);
passwordLabel.classList.add('form-label');
passwordLabel.htmlFor = 'password'; // Set htmlFor as property
passwordLabel.textContent = 'Password';
const passwordInput = createElement('input') as HTMLInputElement;
passwordInput.type = 'password';
passwordInput.classList.add('form-control');
passwordInput.id = 'password';
passwordInput.placeholder = 'Password';
passwordInputGroup.appendChild(passwordLabel);
passwordInputGroup.appendChild(passwordInput);
// Password input group
const passwordInputGroup = this.createInputGroup('password', 'Password', 'password', 'Password');
form.appendChild(passwordInputGroup);
// Login button
const loginButton = createElement('button'); const loginButton = createElement('button');
loginButton.type = 'submit'; loginButton.type = 'submit';
loginButton.classList.add('btn', 'btn-primary'); loginButton.classList.add('btn', 'btn-primary');
loginButton.textContent = 'Login'; loginButton.textContent = 'Login';
form.appendChild(userIdInputGroup);
form.appendChild(passwordInputGroup);
form.appendChild(loginButton); form.appendChild(loginButton);
// Form submission handler
form.addEventListener('submit', async (event) => { form.addEventListener('submit', async (event) => {
event.preventDefault(); event.preventDefault();
const userId = userIdInput.value; const userId = (userIdInputGroup.querySelector('input') as HTMLInputElement).value;
const password = passwordInput.value; const password = (passwordInputGroup.querySelector('input') as HTMLInputElement).value;
try { try {
const response: ApiResponse<LoginResponseData> = await globalAPI.login({ userId, password }); const response: ApiResponse<LoginResponseData> = await globalAPI.login({ userId, password });
@ -71,8 +59,28 @@ export class LoginWidget extends Widget {
} }
}); });
this.container.appendChild(header);
this.container.appendChild(form); this.container.appendChild(form);
return this.container; return this.container;
} }
private createInputGroup(id: string, labelText: string, inputType: string, placeholder: string): HTMLElement {
const inputGroup = createElement('div');
inputGroup.classList.add('mb-3');
const label = createElement('label') as HTMLLabelElement;
label.classList.add('form-label');
label.htmlFor = id;
label.textContent = labelText;
const input = createElement('input') as HTMLInputElement;
input.type = inputType;
input.classList.add('form-control');
input.id = id;
input.placeholder = placeholder;
inputGroup.appendChild(label);
inputGroup.appendChild(input);
return inputGroup;
}
} }

View File

@ -7,11 +7,11 @@ export class RegisterWidget extends Widget {
constructor(message?: string) { constructor(message?: string) {
super(); super();
this.message = message || "For registration, please contact your department head for further instructions."; this.message = message || "";
} }
render(): HTMLElement { render(): HTMLElement {
this.container.innerHTML = ''; // Clear previous content this.container.innerHTML = '';
const header = createElement('h2'); const header = createElement('h2');
header.classList.add('widget-header'); header.classList.add('widget-header');
@ -24,7 +24,7 @@ export class RegisterWidget extends Widget {
const button = createElement('button'); const button = createElement('button');
button.classList.add('btn', 'btn-secondary'); button.classList.add('btn', 'btn-secondary');
button.textContent = 'Register'; button.textContent = 'Register';
button.disabled = true;
button.addEventListener('click', () => { button.addEventListener('click', () => {
navigateTo('/register'); navigateTo('/register');
}); });