Compare commits
No commits in common. "13fd32d038ac6bccf1a57dda32e45523a4422446" and "160a92eceb9998172c801a5cb290fb5f424236f2" have entirely different histories.
13fd32d038
...
160a92eceb
143
README.md
143
README.md
@ -1,40 +1,38 @@
|
|||||||
# 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 interacts with a dedicated backend server via a RESTful API using PAKE SRP for authentication.
|
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.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
* **Modular Architecture:** Organized into distinct modules (widgets, layouts, pages, API) for maintainability and scalability.
|
* **Modular Architecture:** Organized into modules for easy maintainability and scalability.
|
||||||
* **Responsive Design:** Uses Bootstrap 5.3 for a consistent experience across desktops, tablets, and mobiles.
|
* **Responsive Design:** Built with Bootstrap 5.3 to ensure responsiveness across various devices.
|
||||||
* **Widget System:** Extensible widgets with `default` (full-width) and `icon` (sidebar) sizes.
|
* **Widget System:** Extensible widget system with two size types (default full-width and icon-type for sidebar).
|
||||||
* **API Integration:** Connects to a live backend API (default: `http://localhost:8080/api`) for data fetching and actions.
|
* **Global API System:** Clearly defined API structure for communication with a backend (currently mocked).
|
||||||
* **Secure Authentication:** Implements Password-Authenticated Key Exchange (PAKE SRP) for secure login via the `thin-srp` library.
|
* **Layout Modules:** Pre-built layouts (Centered, Three-Column, Split-Column) for different page structures.
|
||||||
* **Token-Based Sessions:** Manages user sessions using tokens obtained from the backend upon successful login (stored in localStorage by default).
|
* **Page Components:** Includes pre-built pages for Login, Register, Dashboard, Settings, Admin, Profile, and Manage Students.
|
||||||
* **Layout Modules:** Provides pre-built layouts: `CenteredLayout`, `ThreeColumnLayout`, `SplitColumnLayout`.
|
* **Topbar Module:** Navigation and user profile dropdown.
|
||||||
* **Core Pages:** Includes pages for Login, Register (info only), Dashboard, Profile, Account Settings (Modal), Admin Dashboard, Manage Students.
|
* **Modal Module:** (To be implemented) For reusable modal components.
|
||||||
* **Topbar Module:** Features main navigation and a user profile dropdown with account actions.
|
* **Basic Authentication Flow:** Login and logout functionality with token-based session management (localStorage).
|
||||||
* **Modal Module:** Provides a reusable container for modal dialogs (e.g., Account Settings, notifications).
|
|
||||||
|
|
||||||
## Technologies Used
|
## Technologies Used
|
||||||
|
|
||||||
* **Vanilla Typescript:** For a robust, type-safe, and maintainable codebase without framework overhead.
|
* **Vanilla Typescript:** For a type-safe and maintainable codebase.
|
||||||
* **Vite:** For an extremely fast development server and optimized build process.
|
* **Vite:** For fast and efficient development and build process.
|
||||||
* **Bootstrap v5.3:** For responsive layout, styling components, and utility classes.
|
* **Bootstrap v5.3:** For responsive layout and styling.
|
||||||
* **pnpm:** Efficient package manager for dependency management.
|
* **pnpm:** Package manager for efficient dependency management.
|
||||||
* **thin-srp:** JavaScript client library for Secure Remote Password (SRP) protocol implementation.
|
* **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.
|
||||||
|
|
||||||
## 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 <your-frontend-repository-url>
|
git clone <repository_url>
|
||||||
cd lms-frontend
|
cd lms-frontend
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -44,86 +42,85 @@ This is the frontend for a modular and responsive Learner Management System (LMS
|
|||||||
pnpm install
|
pnpm install
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Configure API Base URL (Optional):**
|
3. **Start the development server:**
|
||||||
* 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 (usually `http://localhost:5173`).
|
This will start the Vite development server. Open your browser and navigate to the address provided in the console (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
|
||||||
├── package.json # Project dependencies and scripts
|
├── pnpm-lock.yaml # pnpm lock file for dependency management
|
||||||
├── pnpm-lock.yaml # pnpm lock file
|
├── pnpm-workspace.yaml # pnpm workspace configuration
|
||||||
├── public/ # Static assets served directly
|
├── public/ # Public assets (images, etc.)
|
||||||
├── README.md # This README file
|
├── README.md # This README file
|
||||||
├── src/ # Source code directory
|
├── src/ # Source code directory
|
||||||
│ ├── api/
|
│ ├── api/ # API interaction functions (mocked in api.ts)
|
||||||
│ │ └── api.ts # Functions for interacting with the backend API (SRP, data fetching)
|
│ ├── assets/ # Static assets (images, icons)
|
||||||
│ ├── assets/ # Static assets processed by Vite (images, icons)
|
│ ├── components/ # Reusable components
|
||||||
│ ├── components/
|
│ │ ├── layouts/ # Page layout components (CenteredLayout, ThreeColumnLayout, SplitColumnLayout)
|
||||||
│ │ ├── layouts/ # Page layout components
|
│ │ ├── modules/ # Modules (TopbarModule, ModalModule - to be implemented)
|
||||||
│ │ ├── modules/ # Larger UI modules (TopbarModule, ModalModule)
|
│ │ ├── widgets/ # Widget components (LoginWidget, ButtonWidget, etc.)
|
||||||
│ │ └── widgets/ # Reusable UI widgets
|
│ ├── main.ts # Main entry point for the application
|
||||||
│ ├── main.ts # Application entry point, initializes router/app state
|
│ ├── pages/ # Page components (LoginPage, DashboardPage, etc.)
|
||||||
│ ├── pages/ # Page-level components/logic
|
│ ├── styles/ # Global styles and Bootstrap import (index.css)
|
||||||
│ ├── styles/
|
│ ├── types/ # Typescript interfaces (Widget.ts, User.ts)
|
||||||
│ │ └── index.css # Global styles, Bootstrap import
|
│ ├── utils/ # Utility functions (api.ts, auth.ts)
|
||||||
│ ├── types/ # TypeScript interfaces and type definitions
|
│ ├── vite-env.d.ts # Vite environment declaration
|
||||||
│ ├── utils/
|
├── tsconfig.json # Typescript configuration
|
||||||
│ │ └── utils.ts # Utility functions (auth state management, storage)
|
├── vite.config.ts # Vite configuration
|
||||||
│ └── 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 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.
|
The frontend is built around a widget system. Widgets are independent, reusable components that display specific information or functionality.
|
||||||
|
|
||||||
|
* **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
|
||||||
|
|
||||||
Defined in `src/api/api.ts`, this module handles all communication with the backend API.
|
The frontend is designed to interact with a backend through a global API system defined in `src/utils/api.ts`.
|
||||||
|
|
||||||
* **Live Interaction:** Functions use `fetch` to make requests to the running backend (default: `http://localhost:8080/api`).
|
* **Current Implementation:** The `api.ts` file currently contains **mocked API calls** for demonstration purposes. It simulates API responses using timeouts and hardcoded data.
|
||||||
* **SRP Flow:** The `login` function implements the two-step SRP authentication handshake with the backend.
|
* **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.
|
||||||
* **Authenticated Requests:** Other API functions automatically include the stored authentication token in the `Authorization: Bearer <token>` header for protected endpoints.
|
* **Expected Backend Endpoints (Example):**
|
||||||
* **Error Handling:** Includes basic error handling and detection of unauthorized (401) responses.
|
* `POST /api/login`: User login.
|
||||||
* **Key Endpoints Used:**
|
* `GET /api/user`: Get user data (requires authentication).
|
||||||
* `POST /api/auth/srp/start`
|
* `POST /api/logout`: User logout.
|
||||||
* `POST /api/auth/srp/verify`
|
* `/api/students`, `/api/teachers`, `/api/admin/students`, etc.: Endpoints for managing students, teachers, and other LMS data.
|
||||||
* `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 Interaction Notes
|
## Backend Considerations
|
||||||
|
|
||||||
* **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.
|
* **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.
|
||||||
* **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.
|
* **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.
|
||||||
* **Data Source:** All dynamic data (user info, student lists, etc.) is fetched from the backend, which interacts with the MariaDB database.
|
* **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).
|
||||||
|
* **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
|
||||||
|
|
||||||
* **Complete Widget Functionality:** Ensure all widgets fetch and display real data from the backend API.
|
* **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).
|
||||||
* **Admin Page Enhancements:** Implement full CRUD operations for student/teacher management, filtering, batch actions, and enrollment assignments.
|
* **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.
|
||||||
* **Student Table Widget:** Integrate a robust table library (e.g., Tabulator, TanStack Table) for improved sorting, filtering, and pagination.
|
* **Account Settings Modal:** Develop the Account Settings Modal to allow users to edit their profile information and change passwords.
|
||||||
* **Classrooms Module:** Build out the Classrooms feature beyond the placeholder modal.
|
* **Admin Page Functionality:** Implement the functionality for the Admin page, including student and teacher management, using appropriate widgets and API calls.
|
||||||
* **Robust Error Handling:** Implement more comprehensive error handling and user feedback mechanisms throughout the UI.
|
* **Student Table Widget:** Integrate a table library (e.g., Tabulator, DataTables) into the `StudentTableWidget` for enhanced table features like sorting, filtering, and pagination.
|
||||||
* **Client-Side Validation:** Add more input validation for forms (e.g., account settings).
|
* **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.
|
||||||
* **UI/UX Polish:** Refine styling, transitions, and interactions for a smoother user experience.
|
* **Form Validation:** Add client-side form validation to login, registration (if implemented), and account settings forms for better user experience.
|
||||||
* **Testing:** Implement unit and potentially end-to-end tests.
|
* **Error Handling:** Improve error handling in API calls and display user-friendly error messages.
|
||||||
* **State Management:** For larger applications, consider introducing a dedicated state management library (like Zustand, Pinia, Redux Toolkit) instead of relying solely on `utils.ts`.
|
* **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.
|
||||||
|
* **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
|
||||||
|
|
||||||
@ -131,4 +128,4 @@ Defined in `src/api/api.ts`, this module handles all communication with the back
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[View MIT License](./LICENSE)
|
[View the License](./LICENSE)
|
||||||
@ -4,9 +4,6 @@
|
|||||||
<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.
|
Before Width: | Height: | Size: 1011 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB |
30
src/components/CompositeWidget.ts
Normal file
30
src/components/CompositeWidget.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,13 +4,11 @@ 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();
|
||||||
}
|
}
|
||||||
@ -31,19 +29,6 @@ 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 = '';
|
||||||
|
|
||||||
@ -69,9 +54,33 @@ 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');
|
||||||
|
|
||||||
for (const item of this.menuItems) {
|
const dashboardMenuItem = createElement('li');
|
||||||
menuPages.appendChild(item);
|
dashboardMenuItem.classList.add('nav-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');
|
||||||
|
|||||||
@ -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', 'welcome-widget');
|
widgetDiv.classList.add('widget');
|
||||||
widgetDiv.innerHTML = `
|
widgetDiv.innerHTML = `
|
||||||
<div class="widget-header centered-header">Welcome to the Dashboard</div>
|
<div class="widget-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', 'quick-links-widget');
|
widgetDiv.classList.add('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,20 +36,21 @@ class QuickLinksWidget extends Widget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlaceholderWidget extends Widget {
|
class PlaceholderWidget extends Widget { // Corrected placeholder widget definition
|
||||||
render(): HTMLElement {
|
render(): HTMLElement {
|
||||||
const div = createElement('div');
|
const div = createElement('div');
|
||||||
div.classList.add('widget', 'placeholder-widget');
|
div.classList.add('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();
|
const placeholderWidget = new PlaceholderWidget(); // Use the corrected PlaceholderWidget class
|
||||||
|
|
||||||
layout.setColumn1Content(welcomeWidget.render());
|
layout.setColumn1Content(welcomeWidget.render());
|
||||||
layout.setColumn2Content(quickLinksWidget.render());
|
layout.setColumn2Content(quickLinksWidget.render());
|
||||||
|
|||||||
113
src/style.css
113
src/style.css
@ -1,4 +1,4 @@
|
|||||||
/* --- Global styles --- */
|
/* Global styles */
|
||||||
h1,
|
h1,
|
||||||
h2,
|
h2,
|
||||||
h3,
|
h3,
|
||||||
@ -12,43 +12,40 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-family: 'Roboto', sans-serif;
|
font-family: sans-serif;
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes scroll-bg {
|
|
||||||
from {
|
|
||||||
background-position: 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
background-position: -100% 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
background-color: rgba(33, 37, 41, 0.925);
|
|
||||||
z-index: -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrolling-background {
|
.scrolling-background {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%; /* Ensure it covers full width */
|
||||||
height: 100vh;
|
height: 100vh; /* Full viewport height */
|
||||||
background-image: url('./assets/after-sunset-minimal-4k-zm-3840x2400.jpg');
|
background-image: url('./assets/bg1.jpg');
|
||||||
background-repeat: repeat-x;
|
background-repeat: repeat-x;
|
||||||
background-size: auto 100%;
|
background-size: auto 100%; /* Maintain width and fill height */
|
||||||
animation: scroll-bg 30s linear infinite;
|
animation: scroll-bg 30s linear infinite;
|
||||||
z-index: -2;
|
z-index: -2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Widget Styling --- */
|
@keyframes scroll-bg {
|
||||||
|
from {
|
||||||
|
background-position: 0 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
background-position: -100% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: rgba(33, 37, 41, 0.925);
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
.widget {
|
.widget {
|
||||||
border: 1px solid rgb(0, 0, 0, 0.20);
|
border: 1px solid rgb(0, 0, 0, 0.20);
|
||||||
@ -58,23 +55,21 @@ 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;
|
||||||
@ -103,7 +98,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* --- Modal Styles --- */
|
/* Modal Styles */
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -130,7 +125,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;
|
||||||
@ -146,40 +141,4 @@ body {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
/* 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 */
|
|
||||||
}
|
}
|
||||||
@ -11,40 +11,52 @@ 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);
|
||||||
|
|
||||||
// User ID input group
|
const passwordInputGroup = createElement('div');
|
||||||
const userIdInputGroup = this.createInputGroup('userId', 'Student ID', 'text', 'Student ID');
|
passwordInputGroup.classList.add('mb-3');
|
||||||
form.appendChild(userIdInputGroup);
|
const passwordLabel = createElement('label') as HTMLLabelElement; // Cast first
|
||||||
|
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 = (userIdInputGroup.querySelector('input') as HTMLInputElement).value;
|
const userId = userIdInput.value;
|
||||||
const password = (passwordInputGroup.querySelector('input') as HTMLInputElement).value;
|
const password = passwordInput.value;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response: ApiResponse<LoginResponseData> = await globalAPI.login({ userId, password });
|
const response: ApiResponse<LoginResponseData> = await globalAPI.login({ userId, password });
|
||||||
@ -59,28 +71,8 @@ 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -7,11 +7,11 @@ export class RegisterWidget extends Widget {
|
|||||||
|
|
||||||
constructor(message?: string) {
|
constructor(message?: string) {
|
||||||
super();
|
super();
|
||||||
this.message = message || "";
|
this.message = message || "For registration, please contact your department head for further instructions.";
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): HTMLElement {
|
render(): HTMLElement {
|
||||||
this.container.innerHTML = '';
|
this.container.innerHTML = ''; // Clear previous content
|
||||||
|
|
||||||
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');
|
||||||
});
|
});
|
||||||
@ -34,4 +34,4 @@ export class RegisterWidget extends Widget {
|
|||||||
this.container.appendChild(button);
|
this.container.appendChild(button);
|
||||||
return this.container;
|
return this.container;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user