Compare commits

...

37 Commits

Author SHA1 Message Date
3fed37c189 docs(contract): Fix formatting, typos, and add website link 2025-04-09 06:11:26 +08:00
aki
d5d11f7d70 Merge pull request 'docs/CONTRACT.md' (#7) from docs/CONTRACT.md into main
Reviewed-on: #7
2025-04-08 23:09:41 +08:00
aki
13fd32d038 Merge branch 'main' into docs/CONTRACT.md 2025-04-08 23:08:43 +08:00
aki
160a92eceb Add ARTICLE 1: DEFINITIONS 2025-04-08 23:07:51 +08:00
aki
a5e8ff49d1 Update ARTICLE 11-15 to be more PACIFIC 2025-04-08 23:07:23 +08:00
aki
00a7e8c488 Updated ARTICLE 5-10 to be more pacific 2025-04-08 23:05:20 +08:00
aki
1445ec83db Updated ARTICLE 3 & 4 to be more pacific 2025-04-08 23:03:48 +08:00
aki
5505adc040 Update ARTICLE 2 to be more pacific 2025-04-08 23:02:41 +08:00
aki
673f8bb24f Updated Parties/Recitals/Agreement 2025-04-08 22:59:35 +08:00
28435c2f3b Updated nigga 2025-04-08 19:08:34 +08:00
6ffdbeadb5 Updated CONTRACT 2025-04-08 18:48:42 +08:00
f816d3eef4 UPDATED CONTRACT 2025-04-08 15:02:51 +08:00
aki
64f30aa363 Fix Recitals 2025-04-08 12:07:22 +08:00
aki
2a8d75294f Fix formatting 2025-04-08 12:06:20 +08:00
aki
16a706e366 Change the mock contract to look more like an actual contract 2025-04-08 11:58:24 +08:00
aki
ff9fdc7982 style: Different styled headers 2025-04-08 11:50:18 +08:00
aki
4923ccf30c feat: Preparations for TopBar modifications 2025-04-08 11:48:56 +08:00
aki
ae285ad84e docs: Update README.md to reflect the new project specifications 2025-04-08 11:30:53 +08:00
aki
8178806075 Update graphs 2025-04-08 11:26:03 +08:00
aki
2b059d089a Update docs/CONTRACT.md 2025-04-08 05:33:52 +08:00
aki
3522448a7f style: Bigger headers 2025-04-08 03:34:18 +08:00
aki
cb82f80e8e Merge branch 'main' of ssh://gitea.opossum-arcturus.ts.net:10000/CellTech/lms-frontend 2025-04-08 03:29:41 +08:00
aki
d4ae3569ed Merge pull request 'style: new theme,layout tweaks etc.' (#5) from style/brand-restyle into main
Reviewed-on: #5
2025-04-08 03:28:06 +08:00
aki
e030c11894 refactor: Cleanup CSS, second pass 2025-04-08 03:25:53 +08:00
aki
6c23b77056 style: Clean up and improve styling 2025-04-08 03:21:14 +08:00
58f7f09fbd style: new theme,layout tweaks etc. 2025-04-08 02:10:06 +08:00
aki
31ecf0d022 docs: Delete docs/CONTRACT.md 2025-04-08 01:45:49 +08:00
aki
043c4d5aa1 chore: Unnecessary (at the moment) 2025-04-08 00:25:01 +08:00
aki
35378d17d0 Version 1.0 upload 2025-04-08 00:05:15 +08:00
aki
2eff273486 chore: add ESLint and update deps 2025-04-07 03:25:28 +08:00
aki
99a2bc20de refactor(add): Use TypeScript 2025-04-07 02:19:57 +08:00
aki
47db9cb3ce fix: Vite template location 2025-04-07 02:17:25 +08:00
aki
288e7ed626 build: Update package.json 2025-04-06 22:30:16 +08:00
aki
0da9b8df7d docs: Update CONTRACT.md 2025-04-06 22:26:28 +08:00
aki
9454987361 docs: Add CONTRACT.md 2025-04-06 22:13:55 +08:00
aki
8dfa40efbd docs: Add CONTRIBUTION.md 2025-04-06 22:13:42 +08:00
aki
cde93e0a4c docs: Update README.md 2025-04-06 22:13:35 +08:00
49 changed files with 4045 additions and 263 deletions

54
CONTRIBUTION.md Normal file
View File

@@ -0,0 +1,54 @@
# Contribution Guidelines
Thank you for your interest in contributing to the Learner Management System Frontend! While this project is currently open-source as a proof-of-concept, and may transition to a closed-source model in the future, we welcome contributions during this open phase to help improve and refine the system.
Please take a moment to review these guidelines before contributing.
## Ways to Contribute
There are several ways you can contribute to this project:
* **Reporting Issues:** If you encounter bugs, errors, or unexpected behavior while using the frontend, please open an issue on the GitHub repository. When reporting issues, please be as detailed as possible, including:
* A clear and descriptive title.
* Steps to reproduce the issue.
* Your browser and operating system.
* Screenshots or error messages if applicable.
* **Suggesting Features:** If you have ideas for new features, improvements, or enhancements to the frontend, please open an issue on the GitHub repository with a "feature request" label. Describe your suggestion clearly and explain why you think it would be valuable.
* **Code Contributions (Pull Requests):** If you'd like to contribute code directly, you are welcome to submit pull requests. Please follow these steps:
1. **Fork the repository.**
2. **Create a new branch** for your contribution (e.g., `feature/new-widget` or `bugfix/login-error`).
3. **Make your changes** in your forked repository.
4. **Ensure your code adheres to the project's coding style** (see "Coding Style" below).
5. **Test your changes thoroughly.**
6. **Submit a pull request** to the main repository's `main` branch.
## Development Setup
To set up your development environment and work on the frontend, please refer to the [README.md](README.md) file for detailed instructions on:
* Prerequisites (Node.js, pnpm)
* Installation
* Running the development server
Make sure you can run the project locally and understand the basic project structure before making significant code contributions.
## Coding Style
We aim for clean, readable, and maintainable code. Please follow these guidelines when contributing code:
* **Typescript:** This project is written in Typescript. Ensure your code is properly typed and utilizes Typescript features effectively.
* **Vanilla Javascript Principles:** While using Typescript, the core is Vanilla Javascript. Avoid unnecessary dependencies and keep the code as lightweight as possible when appropriate.
* **Bootstrap:** Utilize Bootstrap classes and components for styling and layout where applicable, maintaining responsiveness.
* **Modularity:** Keep components and widgets modular and reusable.
* **Code Comments:** Add comments to explain complex logic or non-obvious parts of your code.
* **Consistency:** Try to maintain consistency with the existing codebase in terms of code style and structure.
## Licensing
This project is currently open-source as a proof-of-concept. The licensing terms are not yet formally defined for this open phase. However, by contributing, you agree that your contributions may be used and incorporated into the project. Please be aware that the licensing and open-source nature of this project may change in the future.
## Questions and Contact
If you have any questions about contributing or the project itself, please open an issue on the GitHub repository or reach out to [Your Name/Contact Information - e.g., your email or GitHub profile link].
We appreciate your contributions to making this Learner Management System Frontend better!

145
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,90 +44,91 @@ 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
[Optional: Add contribution guidelines if you plan to make this project open source or collaborative.] [Link to Contribution Guidelines](./CONTRIBUTION.md)
## License ## License
[Optional: Specify the license under which this project is distributed, e.g., MIT License.] [View MIT License](./LICENSE)

467
docs/CONTRACT.md Normal file
View File

@@ -0,0 +1,467 @@
# **SOFTWARE DEVELOPMENT AND IMPLEMENTATION AGREEMENT**
**Parties:**
(Term: 3-5 Years, To be finalized based on Support/Maintenance period post-delivery)
This Software Development and Implementation Agreement (the "**Agreement**") is entered into effective as of **[Date - e.g., August 5, 2024]** (the "**Effective Date**"), by and between:
**CellTech**, a Partnership organized and existing under the laws of the Republic of the Philippines, with its principal place of business at Funda Dalipe, San Jose, Antique (hereinafter referred to as "**Developer**"), and online at https://gitea.opossum-arcturus.ts.net/CellTech.
AND
**Western Institute of Technology**, a Partnership organized and existing under the laws of the Republic of the Philippines, with its principal place of business at Lapaz, Iloilo City (hereinafter referred to as "**Client**").
(Developer and Client may be referred to individually as a "**Party**" and collectively as the "**Parties**").
**Recitals:**
A. Client requires the design, development, implementation, testing, and delivery of a custom Learner Management System (hereinafter referred to as the "**System**" or "**LMS**") as further specified herein, to meet its operational and educational requirements.
B. Developer represents that it possesses the necessary expertise, personnel, skills, and resources to develop and deliver the System in accordance with the specifications, timelines, and conditions outlined in this Agreement.
C. Client desires to engage Developer to perform such software development and implementation services ("**Services**"), and Developer desires to provide such Services to Client, subject to the terms and conditions set forth in this Agreement.
**Agreement:**
**NOW, THEREFORE**, in consideration of the mutual covenants, promises, and agreements contained herein, and other good and valuable consideration, the receipt and sufficiency of which are hereby acknowledged, the Parties hereto agree as follows:
## ARTICLE 1: DEFINITIONS
For the purposes of this Agreement, the following terms shall have the meanings ascribed to them below:
1.1 **"Acceptance Criteria"** means the specific, measurable criteria defined in this Agreement or mutually agreed upon test plans, which the System must meet to be formally accepted by the Client.
1.2 **"Agile"** refers to the iterative and incremental software development methodology described in Section 2.2.
1.3 **"Confidential Information"** has the meaning set forth in Article 10.
1.4 **"Deliverables"** means the specific items, including software code, documentation, reports, and training materials, that Developer is obligated to provide to Client under this Agreement, as further detailed in Section 4.3.
1.5 **"Intellectual Property"** means any and all patents, copyrights, trademarks, trade secrets, database rights, design rights, and other proprietary rights, whether registered or unregistered.
1.6 **"LMS"** or **"System"** means the custom Learner Management System software to be developed and implemented by Developer for Client pursuant to this Agreement.
1.7 **"OPAQUE"** refers to the Oblivious Pseudo-Random Function (OPRF) based Asymmetric Password-Authenticated Key Exchange protocol intended for user authentication within the System.
1.8 **"PAKE"** means Password-Authenticated Key Exchange, a class of cryptographic protocols allowing two parties to establish a shared cryptographic key based on a user's password without transmitting the password itself. OPAQUE is a type of PAKE protocol.
1.9 **"Services"** means the software design, development, implementation, testing, training, documentation, and delivery services related to the System to be performed by Developer under this Agreement.
1.10 **"Sprint"** means a time-boxed iteration (typically 2-4 weeks) within the Agile development process during which a defined amount of work is completed and made ready for review.
1.11 **"Source Code"** means the human-readable programming language instructions used to create the System software.
1.12 **"UAT"** means User Acceptance Testing, the process by which Client validates that the System meets the agreed-upon requirements and Acceptance Criteria.
## ARTICLE 2: SCOPE OF SERVICES
2.1 **General Scope:** Developer shall perform the Services necessary to design, develop, test, implement, and deliver the System as described in this Agreement and its Exhibits. The System is intended to function as a comprehensive Learner Management System providing functionalities for learners, instructors, and administrators of Western Institute of Technology.
2.2 **Development Methodology:** Developer shall utilize an Agile (Iterative and Incremental) Software Development Life Cycle (SDLC) methodology for the performance of the Services.
> a. **Process:** The project will be broken down into Sprints. Each Sprint will generally include planning, design, development, testing, and stakeholder review, aiming to produce a potentially shippable increment of the System. Sprint duration will be mutually agreed upon, typically **[e.g., two (2)]** weeks.
>
> b. **Rationale:** This methodology is selected to provide adaptability to evolving requirements, facilitate early and frequent feedback from Client, foster collaboration and transparency, manage risks effectively, and maintain focus on user needs.
>
> c. **Project Management:** Developer shall employ project management practices consistent with the Agile methodology. Developer shall provide Client with **[e.g., weekly progress summaries via email and bi-weekly Sprint review meetings]**. Project tracking and backlog management will utilize **[Specify Tool, e.g., Trello, Jira, Asana, GitHub Projects - requires agreement]**, to which Client representatives will be granted appropriate access.
2.3 **Key System Features and Characteristics:** The System developed under this Agreement shall aim to possess the following features and characteristics:
> a. **Technology Stack:**
>
> > i. Frontend: Vite utilizing Vanilla TypeScript and Bootstrap v5.3.
> >
> > ii. Backend: Rust utilizing the Actix framework.
> >
> > iii. Database: MariaDB (version 10 or later compatible version).
> >
> > iv. Deployment Environment: Containerized using Docker and orchestrated via Docker Compose.
>
> b. **Performance & Scalability:** The architecture is designed for efficient performance under expected load conditions (to be reasonably defined) and to accommodate future growth in user base and data volume anticipated by the Client over the Agreement term.
>
> c. **Security:** Development practices will incorporate security considerations, including secure authentication mechanisms (specifically, the **OPAQUE** PAKE protocol), secure session management, input validation, protection against common web vulnerabilities (e.g., Cross-Site Scripting, SQL Injection), and adherence to standard secure coding practices. The Parties shall mutually agree upon any specific additional security standards or penetration testing requirements, if necessary.
>
> d. **User Interface (UI) and User Experience (UX):** The System shall feature an intuitive, responsive user interface adaptable to various screen sizes (desktop, tablet, mobile) and designed according to the principles outlined in Section 3.1 and Exhibit A. Adherence to Web Content Accessibility Guidelines (WCAG) **[Specify required level, e.g., 2.1 Level AA]** will be pursued where reasonably practicable within the scope and budget.
>
> e. **Modularity:** The System architecture, including the frontend widget system and backend service structure, promotes modularity to facilitate future enhancements, maintenance, and potential integrations.
2.4 **Video Demonstration:** As part of the Deliverables, Developer shall provide Client with a video demonstration (e.g., screen recording with narration) showcasing core System functionalities, user roles (learner, instructor, administrator), and key workflows, substantially covering the items listed below:
> a. Platform Navigation and Layout.
>
> b. Key User Journeys (Learner Course Interaction, Instructor Course Management, Administrator User/Course Management).
>
> c. Core Feature Highlights (Dashboard, Profiles, Course Interaction, Admin Interfaces).
>
> d. Interface Responsiveness.
2.5 **Excluded Services:** Unless otherwise explicitly agreed upon in a written Change Order (Article 6), the Services under this Agreement **do not** include:
> a. Ongoing hosting services, server maintenance, or domain name registration/renewal fees beyond the initial deployment and stabilization period defined in Phase 4 of Exhibit C.
>
> b. Creation or curation of educational content (course materials, quizzes, etc.) to be loaded into the LMS.
>
> c. Extensive data migration services from legacy systems beyond [Define scope, e.g., "basic assistance with data mapping and import validation for user data provided in a pre-agreed format" or "data migration services as detailed in a separate Statement of Work"]. Client is responsible for data extraction and cleansing from source systems unless otherwise agreed.
>
> d. Procurement or management of hardware infrastructure required by Client outside the scope of the development and deployment process.
>
> e. Licenses for any third-party software required by Client for its own operations that may interact with the LMS, unless such software is directly embedded by Developer as part of the System Deliverable and its licensing terms are passed through.
## ARTICLE 3: SYSTEM SPECIFICATIONS AND ARCHITECTURE
3.1 **User Interface (UI) Layout and Flow:** The general layout principles and high-level component relationships for the System's user interface are conceptually depicted in **Exhibit A (User Interface Flow Diagram)**, incorporated herein by reference. Key layout components include a persistent Header, a contextual Sidebar (where applicable), a Main Content Area, and a Footer. Specific screen designs and detailed UI specifications will be developed and refined during the Sprints, subject to Client review and feedback during Sprint Reviews.
3.2 **Backend Architecture:** The high-level backend architecture, illustrating the interaction between the Actix framework, MariaDB database, OPAQUE authentication flow, and other core components within the planned containerized environment, is conceptually depicted in **Exhibit B (Backend Architecture Diagram)**, incorporated herein by reference.
3.3 **Technical Specifications:** Detailed technical specifications, including specific API endpoint definitions, data model schemas, and performance guidelines, may be documented collaboratively by the Parties during the project lifecycle, potentially in a shared repository or document store ([Specify if needed, e.g., "maintained in the project's shared Confluence space"]), and referenced herein upon mutual agreement. Initial database schema is based on requirements outlined during project initiation.
## ARTICLE 4: PROJECT TIMELINE AND DELIVERABLES
4.1 **Estimated Timeline:** The estimated timeline for the completion of the Services is approximately **Thirty-Five (35) weeks**, commencing from the agreed-upon project start date. A detailed breakdown of phases, estimated task durations, and dependencies is illustrated in the Gantt chart provided as **Exhibit C (Project Timeline Gantt Chart)**, incorporated herein by reference.
4.2 **Acknowledgement of Estimates:** Client acknowledges that the timeline provided in Exhibit C is an estimate based on the initial Scope of Services, assumptions about requirements stability, and the Agile methodology. Timelines may be adjusted based on the outcomes of Sprints, the complexity of features prioritized, Client feedback responsiveness, unforeseen technical challenges, or mutually agreed-upon Change Requests processed via the Change Management procedure (Article 6). Developer shall promptly communicate any anticipated significant deviations from the estimated timeline.
4.3 **Key Deliverables:** Major deliverables under this Agreement include:
> a. Access to functional System increments for review and feedback at the conclusion of relevant Sprints (typically via a staging environment).
>
> b. The final, deployed System software meeting the Acceptance Criteria (Article 5), delivered to the agreed production environment.
>
> c. The Video Demonstration (as per Section 2.4).
>
> d. System Documentation, comprising:
> > i. User Manuals: Guides for administrators, instructors, and learners covering core functionalities.
> >
> > ii. Technical Documentation: Including deployment instructions, system architecture overview, API endpoint documentation (e.g., generated OpenAPI spec), and database schema description, sufficient to enable technically skilled personnel to operate, maintain, and potentially extend the System. The specific level of detail shall be **[e.g., "standard industry practice for systems of similar complexity" or specify further detail if required]**.
>
> e. Source Code for the custom-developed portions of the System delivered under this Agreement, provided via **[Specify method, e.g., access to a Git repository, digital media transfer]**. Source code escrow requirements, if any, must be separately agreed upon in writing.
>
> f. Training materials and delivery of training sessions as specified in Section 4.4.
4.4 **Training and Handover:** Upon successful deployment (Go-Live milestone in Exhibit C), Developer shall provide:
> a. Training for Client's designated administrators and instructors, covering system administration, course management, user management, and key instructional features. This training shall consist of **[Specify format, duration, number of sessions, e.g., "up to two (2) remote training sessions, each lasting approximately three (3) hours, for a maximum of ten (10) Client attendees per session"]**.
>
> b. Onboarding materials suitable for learners, such as a concise user guide document and potentially referencing the Video Demonstration.
>
> c. A formal handover meeting and documentation transfer concluding the development and deployment phases outlined in Exhibit C.
## ARTICLE 5: TESTING AND ACCEPTANCE
5.1 **Testing:** Developer shall perform internal testing throughout the development process, appropriate to the Agile methodology. This includes developer testing (e.g., unit tests where applicable), integration testing of components, and functional testing against requirements defined for each Sprint.
5.2 **User Acceptance Testing (UAT):** Client shall be responsible for conducting UAT. Developer shall notify Client when specific features, modules, or System increments are ready for UAT (typically at the end of Sprints or designated UAT phases as shown in Exhibit C). Client shall perform UAT in accordance with mutually agreed-upon test plans or use cases based on the Acceptance Criteria. Client shall have **[Specify duration, e.g., ten (10) business days]** from such notification to conduct UAT for the provided increment and report any identified material defects or non-conformities ("**Defects**") to Developer in writing via the agreed project tracking tool ([Tool name from 2.2.c]). Failure to report Defects within the specified period may be deemed acceptance of that increment for the purpose of proceeding with development, without prejudice to addressing Defects identified later within the Warranty Period.
5.3 **Acceptance Criteria:** The System (or relevant increment) shall be deemed formally accepted by Client upon the earliest occurrence of:
> (a) Client providing Developer with written notice of acceptance; or
>
> (b) Client using the delivered System or increment in a live production environment for its intended operational purposes (excluding UAT activities); or
>
> (c) The expiration of the final UAT period for the complete System (as per the timeline in Exhibit C) without Client providing written notice of material Defects that prevent acceptance according to the agreed Acceptance Criteria.
>
> Acceptance Criteria shall primarily be based on the functional requirements defined in the project backlog and specifications developed during the Sprints, demonstrating that the System operates substantially as intended.
5.4 **Defect Resolution:** Developer shall use commercially reasonable efforts to correct any material Defects identified during UAT and properly reported by Client within the agreed timeframe. Defect prioritization and resolution timelines will be managed as part of the Agile backlog grooming and sprint planning process. Resolution of minor defects or cosmetic issues may be deferred to subsequent Sprints or the Warranty Period by mutual agreement.
## ARTICLE 6: CHANGE MANAGEMENT
6.1 **Change Request Process:** Both Parties acknowledge that requirements may evolve. Any proposed change to the agreed-upon Scope of Services, features, specifications, or Deliverables after the initial baseline established during planning ("**Change Request**") must be submitted in writing by either Party to the other Party's designated project contact.
6.2 **Impact Assessment:** Upon receipt of a Change Request, Developer shall promptly assess its potential impact on the project scope, technical feasibility, estimated timeline, resource allocation, and overall project cost. Developer will provide Client with a written impact analysis, including any proposed adjustments to fees or schedule, within **[e.g., five (5) business days]**, or a longer period if mutually agreed for complex requests.
6.3 **Approval:** Developer shall not proceed with implementing any Change Request until both Parties have formally agreed in writing (e.g., through a signed Change Order document referencing this Agreement) to the Change Request itself, its assessed impact, and any associated adjustments to the Agreement's terms, including fees and timeline. Approved Change Orders shall become part of this Agreement.
## ARTICLE 7: PROJECT CONSIDERATIONS AND RISK MITIGATION
7.1 **Potential Scope Evolution:** Client acknowledges that the flexibility inherent in the Agile process necessitates active participation and decisive feedback from Client representatives to manage scope effectively. Both Parties agree to utilize the Change Management process (Article 6) diligently to ensure that scope adjustments are intentional, documented, and their impacts understood and agreed upon, thereby mitigating risks to project timelines and budget.
7.2 **Initial Setup and Data Migration:**
> a. Client Responsibilities: Client is responsible for providing necessary access to its infrastructure (if applicable), timely feedback, subject matter expertise, and ensuring user readiness for the System implementation. Client shall designate key personnel to participate in project meetings, reviews, and UAT.
>
> b. Data Migration: **[Select ONE option based on agreement, requires confirmation]:**
> * **Option 1 (Migration Not Included/Basic Assistance):** Data migration from Client's existing systems is not included in the scope of Services under the base Fees. Developer may provide reasonable assistance with defining data formats or validating imported data, subject to separate agreement or Change Order if significant effort is required. Client is primarily responsible for extracting, cleansing, formatting, and importing its data.
> * **Option 2 (Migration Included - Define Scope):** Data migration services for [Specify data types, e.g., user profiles, basic course structures] from [Specify source system(s)] are included in the Scope of Services. A detailed Data Migration Plan outlining responsibilities, formats, timelines, and validation procedures shall be developed collaboratively by the Parties early in the project. Client remains responsible for the accuracy and completeness of source data provided. Complexities discovered during migration may necessitate a Change Request.
>
> c. Support and Training: Developer commits to providing the onboarding support, training, and documentation outlined in Articles 2, 4, and associated Exhibits to mitigate challenges associated with system transition and user adoption.
## ARTICLE 8: FEES AND PAYMENT SCHEDULE
**8.1 Fees and Payment Terms:** **The total fees, billing rates (if applicable), invoicing procedures, and payment schedule for the Services rendered under this Agreement shall be detailed in a separate Payment Schedule document (designated as Exhibit D), which shall be mutually agreed upon by the Parties in writing and incorporated herein by reference upon execution.** Exhibit D shall specify currency (Philippine Peso - PHP, unless otherwise agreed), payment milestones or frequency, and payment terms (e.g., Net 30 days from invoice date).
8.2 **Expenses:** Unless otherwise specified in Exhibit D, Developer shall bear its own general overhead costs. Any direct, out-of-pocket expenses reasonably incurred by Developer specifically for the project (e.g., pre-approved travel, specific third-party software licenses necessary for the deliverable) shall be reimbursable by Client only if pre-approved by Client in writing. Invoices for approved expenses shall include supporting documentation.
8.3 **Taxes:** Each Party shall be responsible for its own taxes imposed by relevant authorities. Fees specified in Exhibit D are exclusive of any applicable Value Added Tax (VAT) or other sales taxes, which, if applicable, shall be added to Developer's invoices and paid by Client.
## ARTICLE 9: INTELLECTUAL PROPERTY RIGHTS
9.1 **Ownership of Custom Developed System:** Subject to Client's full payment of all fees due under this Agreement and Developer's reservation of rights in Pre-Existing IP (Section 9.2), Developer hereby assigns to Client all right, title, and interest in and to the custom Source Code and associated Deliverables specifically created by Developer for Client under this Agreement (the "**Custom Developed IP**").
9.2 **Developer's Pre-Existing Intellectual Property:** Developer shall retain all right, title, and interest in and to any software, code, libraries, tools, methodologies, know-how, or other intellectual property owned or licensed by Developer prior to the Effective Date or developed independently of this Agreement ("**Developer Pre-Existing IP**"), even if incorporated into the System or used in performing the Services.
9.3 **License to Developer Pre-Existing IP:** To the extent Developer Pre-Existing IP is incorporated into the System Deliverables, Developer hereby grants Client a perpetual, irrevocable, non-exclusive, royalty-free, worldwide license to use, reproduce, modify, and create derivative works of such Developer Pre-Existing IP solely as necessary for Client to use, operate, maintain, and enhance the System for its internal educational and administrative purposes. This license is non-transferable except to a successor of Client's entire business related to the System.
9.4 **Third-Party Materials:** Any third-party software or materials, including open-source software components, incorporated into the System shall be subject to the terms and conditions of their respective licenses. Developer shall identify any significant third-party components and their licenses to Client upon request or as part of the documentation Deliverable. Client's use of the System is subject to compliance with such third-party licenses.
## ARTICLE 10: CONFIDENTIALITY
10.1 **Definition:** "**Confidential Information**" means any non-public information disclosed by one Party ("**Disclosing Party**") to the other Party ("**Receiving Party**") under this Agreement, whether orally or in writing, that is designated as confidential or that reasonably should be understood to be confidential given the nature of the information and the circumstances of disclosure. This includes, but is not limited to, business plans, financial information, customer lists, personnel information, technical data, trade secrets, know-how, source code (for Developer's pre-existing IP), and the terms of this Agreement.
10.2 **Obligations:** The Receiving Party agrees to: (a) use the Confidential Information solely for the purpose of performing its obligations or exercising its rights under this Agreement; (b) not disclose the Confidential Information to any third party without the prior written consent of the Disclosing Party, except to its employees, contractors, or legal/financial advisors who have a need to know and are bound by confidentiality obligations at least as restrictive as those herein; and (c) protect the Confidential Information from unauthorized use or disclosure using at least the same degree of care that it uses to protect its own confidential information of like importance, but not less than a reasonable degree of care.
10.3 **Exclusions:** The obligations under this Article shall not apply to information that the Receiving Party can demonstrate: (a) was already lawfully known to the Receiving Party at the time of disclosure, free of any obligation of confidentiality; (b) is or becomes publicly known through no wrongful act of the Receiving Party; (c) is rightfully received from a third party without restriction and without breach of this Agreement; or (d) was independently developed by the Receiving Party without reference to or use of the Disclosing Party's Confidential Information.
10.4 **Required Disclosure:** If the Receiving Party is compelled by law, regulation, or court order to disclose any Confidential Information, it shall provide the Disclosing Party with prompt prior written notice (to the extent legally permissible) to allow the Disclosing Party an opportunity to seek a protective order or other appropriate remedy.
10.5 **Duration:** The confidentiality obligations set forth herein shall survive the termination or expiration of this Agreement for a period of **[e.g., five (5)]** years. Obligations related to trade secrets shall survive indefinitely as long as they remain trade secrets under applicable law.
## ARTICLE 11: WARRANTIES AND DISCLAIMERS
11.1 **Developer Warranties:** Developer warrants to Client that:
> a. **Performance Warranty:** For a period of **[Specify duration, e.g., ninety (90) days]** following the date of final Acceptance of the complete System by Client ("**Warranty Period**"), the System will perform substantially in accordance with the material functional specifications agreed upon by the Parties under this Agreement when operated in the intended environment. Client's sole and exclusive remedy, and Developer's entire liability, for breach of this warranty shall be for Developer to use commercially reasonable efforts to correct or provide a workaround for any reproducible, material non-conformity reported by Client in writing during the Warranty Period. This warranty does not cover defects arising from misuse, modification by Client or third parties not authorized by Developer, accidents, or failure to operate the System in accordance with documentation or specified requirements.
>
> b. **Authority:** Developer has the full right, power, and authority to enter into this Agreement and perform its obligations hereunder.
>
> c. **Service Performance:** The Services will be performed in a professional and workmanlike manner, consistent with generally accepted industry standards.
>
> d. **Non-Infringement:** To Developer's knowledge, the Custom Developed IP delivered under this Agreement does not infringe upon any valid patent, copyright, or trade secret of any third party existing under the laws of the Republic of the Philippines as of the Effective Date. Developer makes no warranty regarding infringement related to Developer Pre-Existing IP or any third-party materials.
11.2 **Disclaimers:** **EXCEPT FOR THE EXPRESS WARRANTIES SET FORTH IN SECTION 11.1, THE SYSTEM, SERVICES, AND DELIVERABLES ARE PROVIDED "AS IS." DEVELOPER HEREBY DISCLAIMS ALL OTHER WARRANTIES, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHERWISE, INCLUDING BUT NOT LIMITED TO ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, AND NON-INFRINGEMENT WITH RESPECT TO THE SYSTEM AS A WHOLE OR ANY THIRD-PARTY COMPONENTS. DEVELOPER DOES NOT WARRANT THAT THE SYSTEM WILL BE ERROR-FREE, UNINTERRUPTED, OR MEET ALL OF CLIENT'S REQUIREMENTS.**
## ARTICLE 12: LIMITATION OF LIABILITY
12.1 **Exclusion of Indirect Damages:** **IN NO EVENT SHALL EITHER PARTY BE LIABLE TO THE OTHER PARTY OR ANY THIRD PARTY FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, PUNITIVE, OR EXEMPLARY DAMAGES (INCLUDING, BUT NOT LIMITED TO, LOST PROFITS, LOST REVENUE, LOST DATA, LOSS OF GOODWILL, OR BUSINESS INTERRUPTION) ARISING OUT OF OR IN CONNECTION WITH THIS AGREEMENT OR THE USE OR PERFORMANCE OF THE SYSTEM OR SERVICES, REGARDLESS OF THE THEORY OF LIABILITY (CONTRACT, TORT, OR OTHERWISE), EVEN IF SUCH PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.**
12.2 **Cap on Direct Damages:** **EXCEPT FOR LIABILITY ARISING FROM A PARTY'S BREACH OF ITS CONFIDENTIALITY OBLIGATIONS (ARTICLE 10), INDEMNIFICATION OBLIGATIONS (IF ANY - NOT CURRENTLY INCLUDED, CONSIDER ADDING), OR INFRINGEMENT OF THE OTHER PARTY'S INTELLECTUAL PROPERTY RIGHTS, EACH PARTY'S TOTAL AGGREGATE LIABILITY TO THE OTHER PARTY FOR ALL CLAIMS FOR DIRECT DAMAGES ARISING OUT OF OR RELATING TO THIS AGREEMENT, WHETHER IN CONTRACT, TORT, OR OTHERWISE, SHALL NOT EXCEED THE TOTAL AMOUNT OF FEES ACTUALLY PAID BY CLIENT TO DEVELOPER UNDER THIS AGREEMENT DURING THE TWELVE (12) MONTH PERIOD PRECEDING THE EVENT GIVING RISE TO THE CLAIM.** *(Note: This cap is often heavily negotiated. Alternatives include a fixed amount or excluding certain types of direct damages).*
12.3 **Basis of Bargain:** The Parties acknowledge that the limitations of liability and disclaimers of warranties set forth in this Agreement reflect the agreed-upon allocation of risk between the Parties and form an essential basis of the bargain, without which Developer would not have entered into this Agreement on the terms provided.
## ARTICLE 13: TERM AND TERMINATION
13.1 **Term:** This Agreement shall commence on the Effective Date and shall continue until all Services are completed, the System is Accepted, final payment has been made, and the Warranty Period has expired, unless terminated earlier pursuant to the terms of this Article 13. The initial intended development and deployment term is estimated in Exhibit C. The overall Agreement duration might extend based on mutually agreed support terms beyond the initial delivery (as suggested by the "3-5 Year Term" note, which requires separate definition, likely in a Support Agreement).
13.2 **Termination for Cause:** Either Party may terminate this Agreement upon written notice if the other Party materially breaches any provision of this Agreement and fails to cure such breach within **[Specify cure period, e.g., thirty (30) calendar days]** after receiving written notice specifying the breach in reasonable detail. Material breaches include, but are not limited to, failure to make timely payments (by Client) or failure to meet key milestones or Deliverable requirements after reasonable opportunity to cure (by Developer).
13.3 **Termination for Convenience:** **[Choose ONE option or delete if not applicable]:**
* **Option A (Client Only):** Client may terminate this Agreement without cause at any time upon **[Specify notice period, e.g., thirty (30) days']** prior written notice to Developer.
* **Option B (Mutual):** Either Party may terminate this Agreement without cause at any time upon **[Specify notice period, e.g., sixty (60) days']** prior written notice to the other Party.
* **Option C (No Convenience Termination):** (No clause added).
13.4 **Effect of Termination:** Upon termination or expiration of this Agreement for any reason:
> a. Developer shall cease performing Services and shall promptly deliver to Client all completed Deliverables and work-in-progress, including Source Code for Custom Developed IP up to the date of termination.
>
> b. Client shall pay Developer for all Services performed and accepted Deliverables provided up to the effective date of termination, plus any pre-approved, non-cancelable expenses incurred. If termination is by Client for convenience (if Option A or B is chosen), Client shall also pay **[Specify additional amount, e.g., "a termination fee equivalent to X% of the remaining estimated project fees" or "for the work completed during the notice period"]**.
>
> c. Each Party shall promptly return or, at the Disclosing Party's request, destroy all Confidential Information of the other Party in its possession or control.
>
> d. Any provisions of this Agreement that by their nature should survive termination (including, but not limited to, Articles 9, 10, 11.2, 12, 14, and payment obligations accrued prior to termination) shall survive.
## ARTICLE 14: MISCELLANEOUS
14.1 **Governing Law:** This Agreement and any disputes arising out of or relating to it shall be governed by and construed in accordance with the laws of the **Republic of the Philippines**, without regard to its conflict of laws principles.
14.2 **Dispute Resolution:** The Parties agree to attempt to resolve any dispute, controversy, or claim arising out of or relating to this Agreement amicably through good faith negotiation between authorized representatives. If negotiation fails within **[e.g., thirty (30) days]**, the Parties agree **[Choose ONE: e.g., "to submit the dispute to mediation under the rules of [Specify Mediation Body in the Philippines] before resorting to litigation" OR "that the exclusive venue for any legal action shall be the competent courts of [Specify City, e.g., Iloilo City], Philippines"]**.
14.3 **Notices:** All notices, requests, demands, and other communications required or permitted under this Agreement shall be in writing and shall be deemed effectively given: (a) upon personal delivery; (b) upon transmission by electronic mail to the addresses specified below (provided confirmation of receipt is obtained); or (c) three (3) business days after deposit with a reputable overnight courier or registered mail, postage prepaid, addressed to the Parties at their respective addresses first set forth above, or to such other address as a Party may designate by notice.
> **To Developer:** Attn: [Name/Title], Email: [Email Address]
> **To Client:** Attn: [Name/Title], Email: [Email Address]
14.4 **Entire Agreement:** This Agreement, including all Exhibits attached hereto (which are incorporated herein by reference), constitutes the entire agreement between the Parties with respect to the subject matter hereof and supersedes all prior or contemporaneous proposals, understandings, representations, warranties, covenants, and agreements, whether written or oral, relating thereto.
14.5 **Amendments:** No amendment, modification, or waiver of any provision of this Agreement shall be effective unless in writing and signed by duly authorized representatives of both Parties.
14.6 **Assignment:** Neither Party may assign or transfer this Agreement or any of its rights or obligations hereunder, without the prior written consent of the other Party, which consent shall not be unreasonably withheld or delayed. Notwithstanding the foregoing, either Party may assign this Agreement without consent in connection with a merger, acquisition, or sale of all or substantially all of its assets related to this Agreement, provided the assignee agrees in writing to be bound by the terms hereof.
14.7 **Severability:** If any provision of this Agreement is held by a court of competent jurisdiction to be invalid, illegal, or unenforceable, the validity, legality, and enforceability of the remaining provisions shall not in any way be affected or impaired thereby, and such provision shall be deemed modified to the minimum extent necessary to make it valid, legal, and enforceable.
14.8 **Force Majeure:** Neither Party shall be liable for any failure or delay in performing its obligations hereunder (except for payment obligations) if such failure or delay is caused by circumstances beyond its reasonable control, including but not limited to acts of God, war, terrorism, strikes, labor disputes, pandemics, epidemics, government orders, fire, flood, earthquake, or internet service provider failures ("**Force Majeure Event**"). The affected Party shall provide prompt notice to the other Party and shall use reasonable efforts to resume performance as soon as practicable. If a Force Majeure Event continues for more than **[e.g., sixty (60) days]**, the non-affected Party may terminate this Agreement upon written notice.
14.9 **Relationship of Parties:** Developer is an independent contractor, and nothing in this Agreement shall be construed as creating an employment, partnership, joint venture, or agency relationship between Developer and Client. Neither Party has the authority to bind the other Party in any respect.
## ARTICLE 15: EXHIBITS
The following Exhibits are attached hereto and incorporated by reference into this Agreement:
* **Exhibit A:** User Interface Flow Diagram
* **Exhibit B:** Backend Architecture Diagram
* **Exhibit C:** Project Timeline Gantt Chart
* **Exhibit D:** Payment Schedule *(To be mutually agreed upon and attached)*
**IN WITNESS WHEREOF,** the Parties hereto have caused this Software Development and Implementation Agreement to be executed by their duly authorized representatives as of the Effective Date.
**[CLIENT: Western Institute of Technology]**
By: \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
Name: Mark Glen C. Guides
Title: [Client Representative Title]
Date: \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
**[DEVELOPER: Cell Tech]**
By: \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
Name: Jose Daniel G. Percy
Title: [Partner / Authorized Representative]
Date: \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
By: \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
Name: Rekcel M. Endencia
Title: [Partner / Authorized Representative]
Date: \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
---
### EXHIBIT A: User Interface Flow Diagram
```mermaid
---
config:
theme: neo-dark
---
flowchart LR
subgraph Layouts["Layouts"]
Centered["🎯 Centered Layout (Login, Register)"]
ThreeCol["🏛️ Three-Column Layout (Admin Dash)"]
SplitCol["| Sidebar | Content | (Split Column Layout)"]
end
Header["🧭 Header (Logo, Menu, User Profile Dropdown)"]
Sidebar["📚 Sidebar (Optional, Collapsible)"]
MainContent["🖥️ Main Content Area (Hosts Widgets/Pages)"]
Footer["📎 Footer (Links, Copyright)"]
Modal["P M Modal Container"]
LoginPage["🔑 Login Page"]
LoginWidget["🧩 Login Widget (SRP)"]
RegisterButtonWidget["🧩 Button Widget -> Register"]
RegisterPage["✍️ Register Page"]
RegisterInfoWidget["🧩 Register Info Widget"]
LoginButtonWidget["🧩 Button Widget -> Login"]
DashboardPage["📊 Dashboard Page"]
VariousWidgets["🧩 Various Dashboard Widgets"]
ProfilePage["👤 Profile Page"]
ProfileInfoWidget["🧩 Profile Info Widget (Pic, Name, ID, DOB)"]
PostFeedWidget["🧩 Post Feed Widget"]
AdminPage["⚙️ Admin Dashboard"]
StudentsWidget["🧩 Students Widget"]
TeachersWidget["🧩 Teachers Widget"]
ManageStudentsPage["📋 Manage Students Page"]
StudentTableWidget["🧩 Student Table Widget"]
LoginPage --> Centered
RegisterPage --> Centered
DashboardPage --> SplitCol
ProfilePage --> SplitCol
AdminPage --> ThreeCol
ManageStudentsPage --> SplitCol
LoginPage --> LoginWidget
LoginPage --> RegisterButtonWidget
RegisterPage --> RegisterInfoWidget
RegisterPage --> LoginButtonWidget
DashboardPage --> VariousWidgets
ProfilePage --> ProfileInfoWidget
ProfilePage --> PostFeedWidget
AdminPage --> StudentsWidget
AdminPage --> TeachersWidget
ManageStudentsPage --> StudentTableWidget
Header --> Sidebar
Sidebar --> MainContent
MainContent --> Footer
UserProfile["User Profile"] --> Modal
Modal --> AccountSettingsWidget["🧩 Account Settings Widget"]
MenuLinks["Menu Links"] --> Modal
Modal --> UnderConstructionWidget["🧩 'Under Construction' Widget (e.g., Classrooms)"]
```
*End of Exhibit A*
---
### EXHIBIT B: Backend Architecture Diagram
```mermaid
flowchart TD
subgraph A["🌐 Client (Browser)"]
direction LR
Frontend["Vite App"]
end
subgraph B["🐳 Docker Environment"]
direction TB
subgraph C["🚦 Actix Backend Container (lms-backend)"]
direction TB
ActixServer["🚀 Actix HTTP Server"] --> Middleware["🛡️ Middleware (CORS, Log, Auth*)"]
Middleware --> Router["🗺️ Router (handlers.rs)"]
Router --> Handlers["⚙️ Route Handlers"]
Handlers -- Uses --> DBPool["💾 DB Pool (SQLx)"]
Handlers -- Uses --> Models["📝 Data Models (models.rs)"]
Handlers -- Uses --> Errors["❗ Error Handling (errors.rs)"]
Handlers -- Uses --> AppState["🧠 App State (Temp SRP/Session*)"]
Handlers -- Calls --> DBModule["🗃️ DB Logic (db.rs)"]
DBModule -- Uses --> DBPool
DBModule -- Uses --> Models
end
subgraph D["🗄️ MariaDB Container (db)"]
MariaDB["MariaDB Server"] -- Stores --> LMSData["LMS Database (lms_db)"]
end
C -- Connects via Network --> D
end
A --> B
```
<!--
note for C "(*) Auth Middleware & Production-Ready AppState (e.g., Redis/DB Session) are TODOs - INTERNAL NOTE"
note for D "Data persisted via Docker Volume - INTERNAL NOTE"
-->
*End of Exhibit B*
---
### EXHIBIT C: Project Timeline Gantt Chart
```mermaid
gantt
title LMS Development Timeframe (Estimated)
dateFormat YYYY-MM-DD
axisFormat %Y-%m-%d
todayMarker stroke-width:3px,stroke:#E67E22,stroke-opacity:0.8
%% Define Sections based on Phases
section Phase 1: Planning & Design (Approx. 5 Weeks)
Requirements Gathering & Analysis :req, 2024-01-15, 10d
System Architecture Design :design, after req, 10d
UI/UX Wireframing & Prototyping :wireframe, after design, 10d
Tech Stack Finalization & Setup :setup, after design, 5d
Phase 1 Review & Approval :p1review, after wireframe, 5d
section Phase 2: Core Development (Approx. 18 Weeks)
Database Schema Implementation :dbdev, after setup, 15d
Backend Core & Auth Dev (Rust) :backend, after setup, 50d
Frontend Foundational Setup :frontend_setup, after setup, 15d
API Development & Integration :api, after backend, 30d
Frontend UI Development :frontend_ui, after frontend_setup, 40d
Widget System Implementation :frontend_widget, after frontend_setup, 20d
Module Development (Iterative) :modules, after api, 60d
Initial Dev Testing & Integration :devtest, after modules, 15d
section Phase 3: Testing & Deployment (Approx. 8 Weeks)
Comprehensive QA Testing :qatest, after devtest, 20d
User Acceptance Testing (UAT) :uat, after qatest, 10d
Feedback Implementation & Bug Fixing :fixes, after uat, 10d
Deployment Preparation :deployprep, after fixes, 5d
Production Deployment & Go-Live :deploy, after deployprep, 5d
section Phase 4: Post-Launch (Approx. 4 Weeks)
System Monitoring & Stabilization :monitor, after deploy, 10d
Admin & Instructor Training :admintrain, after deploy, 10d
Learner Onboarding Materials :learnertrain, after admintrain, 10d
Final Documentation & Handover :handover, after admintrain, 10d
%% Milestones (Aligned with phase ends where logical)
milestone Phase 1 Complete : 2025-02-16
milestone Core Backend Complete : 2025-04-19
milestone Core Frontend Complete : 2025-05-31
milestone Development Complete : 2025-07-26
milestone Testing Complete : 2025-08-23
milestone Project Go-Live : 2025-08-30
milestone Project Handover : 2025-09-27
```
*Note: Dates are estimates and subject to refinement based on detailed sprint planning and potential scope adjustments defined via the Change Management process (Article 6).*
*End of Exhibit C*

12
eslint.config.js Normal file
View File

@@ -0,0 +1,12 @@
import { defineConfig } from "eslint/config";
import globals from "globals";
import js from "@eslint/js";
import tseslint from "typescript-eslint";
export default defineConfig([
{ files: ["**/*.{js,mjs,cjs,ts}"] },
{ files: ["**/*.{js,mjs,cjs,ts}"], languageOptions: { globals: globals.browser } },
{ files: ["**/*.{js,mjs,cjs,ts}"], plugins: { js }, extends: ["js/recommended"] },
tseslint.configs.recommended,
]);

20
index.html Normal file
View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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="/src/style.css">
</head>
<body>
<div class="scrolling-background">
<div class="color-overlay"></div>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</div>
</body>
</html>

View File

@@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + TS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -1,15 +0,0 @@
{
"name": "learner-management-system",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"devDependencies": {
"typescript": "~5.7.2",
"vite": "^6.2.0"
}
}

View File

@@ -1,9 +0,0 @@
export function setupCounter(element: HTMLButtonElement) {
let counter = 0
const setCounter = (count: number) => {
counter = count
element.innerHTML = `count is ${counter}`
}
element.addEventListener('click', () => setCounter(counter + 1))
setCounter(0)
}

View File

@@ -1,24 +0,0 @@
import './style.css'
import typescriptLogo from './typescript.svg'
import viteLogo from '/vite.svg'
import { setupCounter } from './counter.ts'
document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
<div>
<a href="https://vite.dev" target="_blank">
<img src="${viteLogo}" class="logo" alt="Vite logo" />
</a>
<a href="https://www.typescriptlang.org/" target="_blank">
<img src="${typescriptLogo}" class="logo vanilla" alt="TypeScript logo" />
</a>
<h1>Vite + TypeScript</h1>
<div class="card">
<button id="counter" type="button"></button>
</div>
<p class="read-the-docs">
Click on the Vite and TypeScript logos to learn more
</p>
</div>
`
setupCounter(document.querySelector<HTMLButtonElement>('#counter')!)

View File

@@ -1,96 +0,0 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vanilla:hover {
filter: drop-shadow(0 0 2em #3178c6aa);
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@@ -1,6 +1,22 @@
{ {
"dependencies": { "name": "learner-management-system",
"argon2-browser": "^1.18.0", "private": true,
"bootstrap": "5.3" "version": "0.0.0",
} "type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"devDependencies": {
"@eslint/js": "^9.24.0",
"eslint": "^9.24.0",
"globals": "^16.0.0",
"typescript": "~5.7.2",
"typescript-eslint": "^8.29.0",
"vite": "^6.2.0"
},
"dependencies": {
"bootstrap": "^5.3.5"
}
} }

1536
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

157
src/api/api.ts Normal file
View File

@@ -0,0 +1,157 @@
// api/api.ts
import { updateLoggedInState } from '../utils/utils';
// --- Define API Response Interfaces ---
export interface LoginResponseData { // Exported
userId: string;
fullName: string;
profilePicture: string;
schoolId: string;
birthdate: string;
token: string;
}
export interface ApiResponse<T> { // Exported
success: boolean;
data?: T;
message?: string;
}
export interface ProfileResponseData extends LoginResponseData { // Exported
posts: { id: number; content: string }[];
}
export interface AdminDashboardData { // Exported
studentsCount: number;
teachersCount: number;
}
export interface StudentListData { // Exported
id: number;
name: string;
yearLevel: string;
courses: string[];
}
export interface StudentFinancialData {
tuitionFee: string;
miscellaneousFee: string;
labFee: string;
currentAccount: string;
downPayment: string;
midtermPayment: string;
prefinalPayment: string;
finalPayment: string;
}
// --- Mock API Endpoints ---
const mockAPI = {
login: async (credentials: any): Promise<ApiResponse<LoginResponseData>> => { // Explicit return type, parameter type remains 'any' as per original spec
return new Promise((resolve, reject) => {
setTimeout(() => {
if (credentials.userId === '123456' && credentials.password === 'password') {
const userData: LoginResponseData = {
userId: generateMockUserId(), // Mock User ID
fullName: 'John Doe',
profilePicture: 'path/to/profile.jpg', // Placeholder
schoolId: '123456',
birthdate: '2000-01-01',
token: 'mock-user-token-123', // Mock token
};
resolve({ success: true, data: userData });
updateLoggedInState(true, userData.token); // Update login state on successful login
} else {
reject({ success: false, message: 'Invalid credentials' });
}
}, 1000); // Simulate API latency
});
},
logout: async (): Promise<ApiResponse<void>> => {
return new Promise(resolve => {
setTimeout(() => {
updateLoggedInState(false, null);
resolve({ success: true });
}, 500);
});
},
getProfile: async (userId: string): Promise<ApiResponse<ProfileResponseData>> => {
return new Promise(resolve => {
setTimeout(() => {
const profileData: ProfileResponseData = {
userId: userId,
fullName: 'John Doe',
profilePicture: 'path/to/profile.jpg', // Placeholder
schoolId: '123456',
birthdate: '2000-01-01',
token: 'mock-user-token-123', // Added token here to match ProfileResponseData interface
posts: [
{ id: 1, content: 'First post on profile!' },
{ id: 2, content: 'Another profile post.' }
]
};
resolve({ success: true, data: profileData });
}, 500);
});
},
updateAccountSettings: async (userId: string, settings: any): Promise<ApiResponse<void>> => { // parameter type remains 'any'
return new Promise(resolve => {
setTimeout(() => {
console.log('Account settings updated for user:', userId, settings);
resolve({ success: true });
}, 500);
});
},
getAdminDashboardData: async (): Promise<ApiResponse<AdminDashboardData>> => {
return new Promise(resolve => {
setTimeout(() => {
const dashboardData: AdminDashboardData = {
studentsCount: 150,
teachersCount: 30,
};
resolve({ success: true, data: dashboardData });
}, 500);
});
},
getStudentList: async (): Promise<ApiResponse<StudentListData[]>> => {
return new Promise(resolve => {
setTimeout(() => {
const studentList: StudentListData[] = [
{ id: 1, name: 'Alice Smith', yearLevel: '1', courses: ['Math', 'Science'] },
{ id: 2, name: 'Bob Johnson', yearLevel: '2', courses: ['History', 'English'] },
];
resolve({ success: true, data: studentList });
}, 500);
});
},
getStudentFinancialData: async (_studentId: string): Promise<ApiResponse<StudentFinancialData>> => { // parameter type remains 'string', unused parameter prefixed with _
return new Promise(resolve => {
setTimeout(() => {
const financialData: StudentFinancialData = {
tuitionFee: '5000',
miscellaneousFee: '500',
labFee: '200',
currentAccount: '-100',
downPayment: '2000',
midtermPayment: '1500',
prefinalPayment: '1000',
finalPayment: '500',
};
resolve({ success: true, data: financialData });
}, 500);
});
}
};
let mockUserIdCounter = 100000;
const generateMockUserId = () => {
return String(mockUserIdCounter++);
};
declare global {
interface Window {
globalAPI: typeof mockAPI;
}
}
export const setupGlobalAPI = () => {
window.globalAPI = mockAPI;
};
export const globalAPI = mockAPI;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1011 KiB

BIN
src/assets/bg1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

BIN
src/assets/bg2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 KiB

BIN
src/assets/images.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

56
src/components/Modal.ts Normal file
View File

@@ -0,0 +1,56 @@
// components/Modal.ts
import { createElement } from '../utils/utils';
import { Widget } from './Widget';
export class Modal {
private modalOverlay: HTMLDivElement;
private modalContent: HTMLDivElement;
private widget: Widget | null = null;
private isVisible: boolean = false;
constructor() {
this.modalOverlay = createElement('div') as HTMLDivElement;
this.modalOverlay.classList.add('modal-overlay');
this.modalOverlay.style.display = 'none'; // Initially hidden
this.modalOverlay.addEventListener('click', (event) => {
if (event.target === this.modalOverlay) {
this.hide(); // Close modal if clicked outside content
}
});
this.modalContent = createElement('div') as HTMLDivElement;
this.modalContent.classList.add('modal-content');
this.modalOverlay.appendChild(this.modalContent);
document.body.appendChild(this.modalOverlay); // Append to body once
}
setWidget(widget: Widget): void {
this.widget = widget;
this.modalContent.innerHTML = ''; // Clear previous content
this.modalContent.appendChild(widget.render());
}
show(): void {
if (this.widget) {
this.modalOverlay.style.display = 'flex';
this.isVisible = true;
}
}
hide(): void {
this.modalOverlay.style.display = 'none';
this.isVisible = false;
}
isVisibleModal(): boolean {
return this.isVisible;
}
getContainer(): HTMLDivElement {
return this.modalOverlay;
}
getModalContentContainer(): HTMLDivElement {
return this.modalContent;
}
}

167
src/components/TopBar.ts Normal file
View File

@@ -0,0 +1,167 @@
// components/TopBar.ts
import { createElement, navigateTo, isLoggedInUser } from '../utils/utils';
import { globalAPI, ApiResponse, ProfileResponseData } from '../api/api'; // Import API and types (now exported)
export class TopBar {
private container: HTMLElement;
private menuItems: Array<HTMLElement>;
private profileDropdownVisible: boolean = false;
private profileData: ProfileResponseData | null = null;
constructor() {
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.fetchProfileData();
}
async fetchProfileData() {
if (isLoggedInUser()) {
try {
const response: ApiResponse<ProfileResponseData> = await globalAPI.getProfile('mockUserId');
if (response.success && response.data) {
this.profileData = response.data;
this.render();
} else {
console.error("Failed to fetch profile data");
}
} catch (error) {
console.error("Error fetching profile data:", error);
}
}
}
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 {
this.container.innerHTML = '';
const navbarBrand = createElement('a');
navbarBrand.classList.add('navbar-brand');
navbarBrand.href = '#/dashboard';
navbarBrand.textContent = 'LMS';
const navbarToggler = createElement('button');
navbarToggler.classList.add('navbar-toggler');
navbarToggler.type = 'button';
navbarToggler.setAttribute('data-bs-toggle', 'collapse');
navbarToggler.setAttribute('data-bs-target', '#navbarNav');
navbarToggler.setAttribute('aria-controls', 'navbarNav');
navbarToggler.setAttribute('aria-expanded', 'false');
navbarToggler.setAttribute('aria-label', 'Toggle navigation');
navbarToggler.innerHTML = '<span class="navbar-toggler-icon"></span>';
const navbarCollapse = createElement('div');
navbarCollapse.classList.add('collapse', 'navbar-collapse');
navbarCollapse.id = 'navbarNav';
const menuPages = createElement('ul');
menuPages.classList.add('navbar-nav', 'me-auto', 'mb-2', 'mb-lg-0');
for (const item of this.menuItems) {
menuPages.appendChild(item);
}
const profileSection = createElement('div');
profileSection.classList.add('d-flex', 'align-items-center', 'ms-auto');
if (this.profileData) {
const profileButton = createElement('button');
profileButton.classList.add('btn', 'btn-dark', 'dropdown-toggle');
profileButton.type = 'button';
profileButton.id = 'profileDropdownButton';
profileButton.setAttribute('data-bs-toggle', 'dropdown');
profileButton.setAttribute('aria-expanded', String(this.profileDropdownVisible));
const profileImage = createElement('img'); // Placeholder image, replace with actual profile picture logic
profileImage.src = this.profileData.profilePicture || 'src/assets/vite.svg'; // Default placeholder if no picture
profileImage.alt = 'Profile Picture';
profileImage.style.width = '30px';
profileImage.style.height = '30px';
profileImage.style.borderRadius = '50%';
profileImage.style.marginRight = '5px';
const fullNameSpan = createElement('span');
fullNameSpan.textContent = this.profileData.fullName;
profileButton.appendChild(profileImage);
profileButton.appendChild(fullNameSpan);
const dropdownMenu = createElement('ul');
dropdownMenu.classList.add('dropdown-menu', 'dropdown-menu-end');
dropdownMenu.setAttribute('aria-labelledby', 'profileDropdownButton');
const userIdItem = createElement('li');
userIdItem.innerHTML = `<span class="dropdown-item-text">ID: ${this.profileData.schoolId}</span>`;
dropdownMenu.appendChild(userIdItem);
const divider = createElement('li');
divider.innerHTML = '<hr class="dropdown-divider">';
dropdownMenu.appendChild(divider);
const profileMenuItem = createElement('li');
const profileLink = createElement('a');
profileLink.classList.add('dropdown-item');
profileLink.href = '#/profile';
profileLink.textContent = 'Profile';
profileMenuItem.appendChild(profileLink);
dropdownMenu.appendChild(profileMenuItem);
const accountSettingsMenuItem = createElement('li');
const accountSettingsLink = createElement('a');
accountSettingsLink.classList.add('dropdown-item');
accountSettingsLink.href = '#/profile'; // Same profile page, modal will be triggered there
accountSettingsLink.setAttribute('data-action', 'open-account-settings'); // Custom attribute to trigger modal
accountSettingsLink.textContent = 'Account settings';
accountSettingsMenuItem.appendChild(accountSettingsLink);
dropdownMenu.appendChild(accountSettingsMenuItem);
const logoutMenuItem = createElement('li');
const logoutButton = createElement('button');
logoutButton.classList.add('dropdown-item');
logoutButton.textContent = 'Log out';
logoutButton.addEventListener('click', async () => {
await globalAPI.logout();
navigateTo('/login'); // Redirect to login page after logout
});
logoutMenuItem.appendChild(logoutButton);
dropdownMenu.appendChild(logoutMenuItem);
profileSection.appendChild(profileButton);
profileSection.appendChild(dropdownMenu);
} else {
// Fallback if profile data is not loaded (or not logged in, though TopBar is for logged-in users)
const loginLink = createElement('a');
loginLink.classList.add('btn', 'btn-primary');
loginLink.href = '#/login';
loginLink.textContent = 'Login';
profileSection.appendChild(loginLink);
}
navbarCollapse.appendChild(menuPages);
navbarCollapse.appendChild(profileSection);
this.container.appendChild(navbarBrand);
this.container.appendChild(navbarToggler);
this.container.appendChild(navbarCollapse);
return this.container;
}
getContainer(): HTMLElement {
return this.container;
}
}

26
src/components/Widget.ts Normal file
View File

@@ -0,0 +1,26 @@
// components/Widget.ts
import { createElement } from '../utils/utils';
export abstract class Widget {
protected container: HTMLElement;
protected sizeType: 'default' | 'icon';
constructor(sizeType: 'default' | 'icon' = 'default') {
this.container = createElement('div');
this.container.classList.add('widget');
this.sizeType = sizeType;
if (sizeType === 'icon') {
this.container.classList.add('icon-widget');
}
}
abstract render(): HTMLElement;
getSizeType(): 'default' | 'icon' {
return this.sizeType;
}
getContainer(): HTMLElement {
return this.container;
}
}

View File

@@ -0,0 +1,23 @@
// layouts/CenteredLayout.ts
import { createElement } from '../utils/utils';
export class CenteredLayout {
private container: HTMLElement;
constructor() {
this.container = createElement('div');
this.container.classList.add('centered-layout', 'container-fluid');
}
addContent(content: HTMLElement | string): void {
if (typeof content === 'string') {
this.container.innerHTML = content;
} else {
this.container.appendChild(content);
}
}
render(): HTMLElement {
return this.container;
}
}

View File

@@ -0,0 +1,55 @@
// layouts/SplitColumnLayout.ts
import { createElement } from '../utils/utils';
import { TopBar } from '../components/TopBar';
export class SplitColumnLayout {
private main: HTMLElement;
private container: HTMLElement;
private sidebar: HTMLElement;
private contentArea: HTMLElement;
private topBar: TopBar;
private isSidebarCollapsed: boolean = false;
constructor() {
this.main = createElement('div');
this.container = createElement('div');
this.container.classList.add('split-column-layout', 'container-fluid');
this.topBar = new TopBar();
this.main.appendChild(this.topBar.render());
this.sidebar = createElement('div');
this.sidebar.classList.add('sidebar'); // Bootstrap column classes for sidebar
this.contentArea = createElement('div');
this.contentArea.classList.add('content'); // Bootstrap column classes for content
this.container.appendChild(this.sidebar);
this.container.appendChild(this.contentArea);
this.main.appendChild(this.container);
}
setSidebarContent(content: HTMLElement): void {
this.sidebar.innerHTML = ''; // Clear existing content
this.sidebar.appendChild(content);
}
setContentAreaContent(content: HTMLElement): void {
this.contentArea.innerHTML = '';
this.contentArea.appendChild(content);
}
render(): HTMLElement {
return this.main;
}
toggleSidebar(): void {
this.isSidebarCollapsed = !this.isSidebarCollapsed;
if (this.isSidebarCollapsed) {
this.container.classList.add('collapsed-sidebar');
} else {
this.container.classList.remove('collapsed-sidebar');
}
}
}

View File

@@ -0,0 +1,55 @@
// layouts/ThreeColumnLayout.ts
import { createElement } from '../utils/utils';
import { TopBar } from '../components/TopBar';
export class ThreeColumnLayout {
private main: HTMLElement;
private container: HTMLElement;
private column1: HTMLElement;
private column2: HTMLElement;
private column3: HTMLElement;
private topBar: TopBar;
constructor() {
this.main = createElement('div');
this.container = createElement('div');
this.container.classList.add('three-column-layout', 'container-fluid');
this.topBar = new TopBar();
this.main.appendChild(this.topBar.render());
this.column1 = createElement('div');
this.column1.classList.add('column'); // Bootstrap column classes
this.column2 = createElement('div');
this.column2.classList.add('column');
this.column3 = createElement('div');
this.column3.classList.add('column');
this.container.appendChild(this.column1);
this.container.appendChild(this.column2);
this.container.appendChild(this.column3);
this.main.appendChild(this.container);
}
setColumn1Content(content: HTMLElement): void {
this.column1.innerHTML = ''; // Clear existing content
this.column1.appendChild(content);
}
setColumn2Content(content: HTMLElement): void {
this.column2.innerHTML = '';
this.column2.appendChild(content);
}
setColumn3Content(content: HTMLElement): void {
this.column3.innerHTML = '';
this.column3.appendChild(content);
}
render(): HTMLElement {
return this.main;
}
}

53
src/main.ts Normal file
View File

@@ -0,0 +1,53 @@
import './style.css';
import 'bootstrap/dist/js/bootstrap.bundle.min.js'; // Bootstrap JS
import { renderLoginPage } from './pages/LoginPage';
import { renderRegisterPage } from './pages/RegisterPage';
import { renderDashboardPage } from './pages/DashboardPage';
import { renderProfilePage } from './pages/ProfilePage';
import { renderClassroomsPage } from './pages/ClassroomsPage';
import { renderAdminPage } from './pages/AdminPage';
import { renderManageStudentPage } from './pages/ManageStudentPage';
import { setupGlobalAPI } from './api/api';
import { updateLoggedInState } from './utils/utils';
const app = document.querySelector<HTMLDivElement>('#app');
if (!app) {
throw new Error("App root element not found.");
}
// Initialize Global API
setupGlobalAPI();
// --- Routing and Page Rendering ---
const routes: { [key: string]: () => void } = {
'/': renderLoginPage,
'/login': renderLoginPage,
'/register': renderRegisterPage,
'/dashboard': renderDashboardPage,
'/profile': renderProfilePage,
'/classrooms': renderClassroomsPage,
'/admin': renderAdminPage,
'/manage-students': renderManageStudentPage,
};
const renderRoute = () => {
const path = window.location.hash.slice(1) || '/';
const routeRenderer = routes[path];
if (routeRenderer) {
app.innerHTML = ''; // Clear current content
routeRenderer();
} else {
app.innerHTML = '<div><h1>404 Not Found</h1></div>';
}
};
window.addEventListener('hashchange', renderRoute);
renderRoute(); // Initial render
// --- Mock Login for Prototype ---
// For demonstration, automatically log in on dashboard page load
if (window.location.hash === '#/dashboard' || window.location.hash === '#/profile' || window.location.hash === '#/admin' || window.location.hash === '#/manage-students') {
updateLoggedInState(true); // Simulate logged in state
} else {
updateLoggedInState(false);
}

17
src/pages/AdminPage.ts Normal file
View File

@@ -0,0 +1,17 @@
// pages/AdminPage.ts
import { ThreeColumnLayout } from '../layouts/ThreeColumnLayout';
import { StudentsWidget } from '../widgets/StudentsWidget';
import { TeachersWidget } from '../widgets/TeachersWidget';
export const renderAdminPage = () => {
const layout = new ThreeColumnLayout();
const studentsWidget = new StudentsWidget();
const teachersWidget = new TeachersWidget();
// Column 3 is optional for future features as per requirements
layout.setColumn1Content(studentsWidget.render());
layout.setColumn2Content(teachersWidget.render());
// Column 3 is intentionally left empty in this prototype
document.querySelector<HTMLDivElement>('#app')?.appendChild(layout.render());
};

View File

@@ -0,0 +1,12 @@
// pages/ClassroomsPage.ts
import { CenteredLayout } from '../layouts/CenteredLayout';
import { UnderConstructionWidget } from '../widgets/UnderConstructionWidget';
export const renderClassroomsPage = () => {
const layout = new CenteredLayout();
const underConstructionWidget = new UnderConstructionWidget("Classrooms is currently not yet ready for production.");
layout.addContent(underConstructionWidget.render());
document.querySelector<HTMLDivElement>('#app')?.appendChild(layout.render());
};

View File

@@ -0,0 +1,59 @@
// pages/DashboardPage.ts
import { ThreeColumnLayout } from '../layouts/ThreeColumnLayout';
import { Widget } from '../components/Widget';
import { createElement } from '../utils/utils';
// Dummy Widgets for Dashboard
class WelcomeWidget extends Widget {
render(): HTMLElement {
const widgetDiv = createElement('div');
widgetDiv.classList.add('widget', 'welcome-widget');
widgetDiv.innerHTML = `
<div class="widget-header centered-header">Welcome to the Dashboard</div>
<div class="widget-body">
This is your dashboard. More widgets will be added here.
</div>
`;
return widgetDiv;
}
}
class QuickLinksWidget extends Widget {
render(): HTMLElement {
const widgetDiv = createElement('div');
widgetDiv.classList.add('widget', 'quick-links-widget');
widgetDiv.innerHTML = `
<div class="widget-header">Quick Links</div>
<div class="widget-body">
<ul class="list-unstyled">
<li><a href="#/profile">View Profile</a></li>
<li><a href="#/classrooms">Go to Classrooms</a></li>
<li><a href="#/admin">Admin Panel</a></li>
</ul>
</div>
`;
return widgetDiv;
}
}
class PlaceholderWidget extends Widget {
render(): HTMLElement {
const div = createElement('div');
div.classList.add('widget', 'placeholder-widget');
div.textContent = 'Placeholder Widget';
return div;
}
}
export const renderDashboardPage = () => {
const layout = new ThreeColumnLayout();
const welcomeWidget = new WelcomeWidget();
const quickLinksWidget = new QuickLinksWidget();
const placeholderWidget = new PlaceholderWidget();
layout.setColumn1Content(welcomeWidget.render());
layout.setColumn2Content(quickLinksWidget.render());
layout.setColumn3Content(placeholderWidget.render());
document.querySelector<HTMLDivElement>('#app')?.appendChild(layout.render());
};

21
src/pages/LoginPage.ts Normal file
View File

@@ -0,0 +1,21 @@
// pages/LoginPage.ts
import { CenteredLayout } from '../layouts/CenteredLayout';
import { LoginWidget } from '../widgets/LoginWidget';
import { RegisterWidget } from '../widgets/RegisterWidget';
import { createElement } from '../utils/utils';
export const renderLoginPage = () => {
const layout = new CenteredLayout();
const loginWidget = new LoginWidget();
const registerWidget = new RegisterWidget();
const container = createElement('div');
container.classList.add('d-flex', 'flex-column', 'gap-3', 'p-4', 'rounded', 'shadow', 'blur'); // Bootstrap flex and styling
container.appendChild(loginWidget.render());
container.appendChild(registerWidget.render());
layout.addContent(container);
document.querySelector<HTMLDivElement>('#app')?.appendChild(layout.render());
};

View File

@@ -0,0 +1,16 @@
// pages/ManageStudentPage.ts
import { SplitColumnLayout } from '../layouts/SplitColumnLayout'; // Or ThreeColumnLayout or full width, depending on desired layout
import { StudentTableWidget } from '../widgets/StudentTableWidget';
import { createElement } from '../utils/utils';
export const renderManageStudentPage = () => {
const layout = new SplitColumnLayout(); // Using SplitColumnLayout, can be changed to ThreeColumnLayout or full width
const studentTableWidget = new StudentTableWidget();
layout.setContentAreaContent(studentTableWidget.render());
// Sidebar can be used for filters or additional admin options if needed
layout.setSidebarContent(createElement('div')); // Empty sidebar for now, or add sidebar widgets
document.querySelector<HTMLDivElement>('#app')?.appendChild(layout.render());
};

47
src/pages/ProfilePage.ts Normal file
View File

@@ -0,0 +1,47 @@
// pages/ProfilePage.ts
import { SplitColumnLayout } from '../layouts/SplitColumnLayout';
import { ProfileWidget } from '../widgets/ProfileWidget';
import { StudentFinancialWidget } from '../widgets/StudentFinancialWidget';
import { PostFeedWidget } from '../widgets/PostFeedWidget';
import { AccountSettingsWidget } from '../widgets/AccountSettingsWidget';
import { Modal } from '../components/Modal';
import { createElement } from '../utils/utils';
let accountSettingsModalInstance: Modal | null = null; // Keep track of modal instance
export const renderProfilePage = () => {
const layout = new SplitColumnLayout();
const sidebar = createElement('div');
const profileWidget = new ProfileWidget();
const financesWidget = new StudentFinancialWidget('mockUserId');
const postFeedWidget = new PostFeedWidget();
sidebar.appendChild(profileWidget.render());
sidebar.appendChild(financesWidget.render());
layout.setSidebarContent(sidebar);
layout.setContentAreaContent(postFeedWidget.render());
const appElement = document.querySelector<HTMLDivElement>('#app');
if (appElement) {
appElement.innerHTML = ''; // Clear existing content
appElement.appendChild(layout.render());
// Handle 'Account settings' link click from TopBar or ProfileWidget
const accountSettingsLinks = document.querySelectorAll('[data-action="open-account-settings"]');
accountSettingsLinks.forEach(link => {
link.addEventListener('click', (event) => {
event.preventDefault(); // Prevent default link behavior
if (!accountSettingsModalInstance) {
accountSettingsModalInstance = new Modal();
const settingsWidget = new AccountSettingsWidget(() => {
accountSettingsModalInstance?.hide(); // Callback to hide modal after settings update
});
accountSettingsModalInstance.setWidget(settingsWidget);
}
accountSettingsModalInstance.show();
});
});
}
};

21
src/pages/RegisterPage.ts Normal file
View File

@@ -0,0 +1,21 @@
// pages/RegisterPage.ts
import { CenteredLayout } from '../layouts/CenteredLayout';
import { RegisterWidget } from '../widgets/RegisterWidget';
import { BackButtonWidget } from '../widgets/BackButtonWidget';
import { createElement } from '../utils/utils';
export const renderRegisterPage = () => {
const layout = new CenteredLayout();
const registerMessageWidget = new RegisterWidget("Please contact your department head to register.");
const backButtonWidget = new BackButtonWidget('Back to Login', '#/login');
const container = createElement('div');
container.classList.add('d-flex', 'flex-column', 'gap-3', 'p-4', 'rounded', 'shadow'); // Bootstrap flex and styling
container.appendChild(registerMessageWidget.render());
container.appendChild(backButtonWidget.render());
layout.addContent(container);
document.querySelector<HTMLDivElement>('#app')?.appendChild(layout.render());
};

185
src/style.css Normal file
View File

@@ -0,0 +1,185 @@
/* --- Global styles --- */
h1,
h2,
h3,
h4,
h5,
h6 {
margin-bottom: 0px;
}
body {
margin: 0;
padding: 0;
overflow: hidden;
font-family: 'Roboto', 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 {
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 {
border: 1px solid rgb(0, 0, 0, 0.20);
background-color: rgb(31, 35, 39, 0.9);
padding: 15px;
margin-bottom: 15px;
border-radius: 5px;
}
.centered-header {
font-size: 24px;
text-align: center;
}
.widget-header {
margin-bottom: 10px;
font-weight: bold;
}
.icon-widget {
padding: 10px;
text-align: center;
}
/* --- Layout Specific Styles --- */
.centered-layout {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.three-column-layout {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
padding: 20px;
}
.split-column-layout {
display: grid;
grid-template-columns: 250px 1fr;
/* Sidebar and content */
gap: 20px;
padding: 20px;
}
.split-column-layout.collapsed-sidebar {
grid-template-columns: 80px 1fr;
/* Collapsed sidebar width */
}
/* --- Modal Styles --- */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
/* Semi-transparent overlay */
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
/* Ensure modal is on top */
}
.modal-content {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
width: 80%;
/* Adjust as needed */
max-width: 800px;
/* Maximum width */
}
/* --- Responsive adjustments (example) --- */
@media (max-width: 768px) {
.three-column-layout {
grid-template-columns: 1fr;
/* Stack columns on smaller screens */
}
.split-column-layout {
grid-template-columns: 1fr;
/* Stack sidebar and content */
}
.split-column-layout.collapsed-sidebar {
grid-template-columns: 1fr;
/* 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

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

32
src/utils/utils.ts Normal file
View File

@@ -0,0 +1,32 @@
// utils.ts
let isLoggedIn = false; // Global state for login status. In a real app, use more robust state management.
let userToken: string | null = null; // Store user token
export const updateLoggedInState = (loggedIn: boolean, token: string | null = null) => {
isLoggedIn = loggedIn;
userToken = token;
// You can add more logic here, like redirecting based on login state
};
export const isLoggedInUser = () => {
return isLoggedIn;
};
export const getUserToken = () => {
return userToken;
};
// Function to generate a random 6-digit ID
export const generateUserId = (): string => {
return String(Math.floor(100000 + Math.random() * 900000));
};
// Function to navigate to a different page (prototype routing)
export const navigateTo = (path: string) => {
window.location.hash = path;
};
// Helper function to create HTML elements
export const createElement = <K extends keyof HTMLElementTagNameMap>(tagName: K, options?: ElementCreationOptions): HTMLElementTagNameMap[K] => {
return document.createElement(tagName, options);
};

View File

@@ -0,0 +1,119 @@
// widgets/AccountSettingsWidget.ts
import { Widget } from '../components/Widget';
import { createElement } from '../utils/utils';
import { globalAPI, ApiResponse } from '../api/api';
export class AccountSettingsWidget extends Widget {
private onSettingsUpdated: () => void;
constructor(onSettingsUpdated: () => void) {
super();
this.onSettingsUpdated = onSettingsUpdated;
}
render(): HTMLElement {
this.container.innerHTML = '';
const header = createElement('div');
header.classList.add('widget-header');
header.textContent = 'Account Settings';
this.container.appendChild(header);
const widgetBody = createElement('div');
widgetBody.classList.add('widget-body');
const form = createElement('form');
// Profile Picture Section
const profilePictureSection = createElement('div');
profilePictureSection.classList.add('mb-3');
const profilePictureLabel = createElement('label') as HTMLLabelElement; // Cast first
profilePictureLabel.classList.add('form-label');
profilePictureLabel.htmlFor = 'profilePicture'; // Set htmlFor as property
profilePictureLabel.textContent = 'Profile Picture';
const profilePictureInput = createElement('input') as HTMLInputElement;
profilePictureInput.type = 'file';
profilePictureInput.classList.add('form-control');
profilePictureInput.id = 'profilePicture';
profilePictureSection.appendChild(profilePictureLabel);
profilePictureSection.appendChild(profilePictureInput);
form.appendChild(profilePictureSection);
// Student Details Section (Read-only in prototype)
const studentDetailsSection = createElement('div');
studentDetailsSection.classList.add('mb-3');
studentDetailsSection.innerHTML = `
<div class="mb-2"><strong>Student Details</strong> <span class="text-muted">(Contact department head for corrections)</span></div>
<div class="mb-2">
<label for="fullName" class="form-label">Full Name</label>
<input type="text" class="form-control" id="fullName" value="John Doe" readonly disabled>
</div>
<div>
<label for="birthdate" class="form-label">Birthdate</label>
<input type="text" class="form-control" id="birthdate" value="2000-01-01" readonly disabled>
</div>
`;
form.appendChild(studentDetailsSection);
// Password Change Section
const passwordChangeSection = createElement('div');
passwordChangeSection.classList.add('mb-3');
passwordChangeSection.innerHTML = `
<div class="mb-2"><strong>Change Password</strong></div>
<div class="mb-2">
<label for="newPassword" class="form-label">New Password</label>
<input type="password" class="form-control" id="newPassword" placeholder="New Password">
</div>
<div>
<label for="confirmPassword" class="form-label">Confirm New Password</label>
<input type="password" class="form-control" id="confirmPassword" placeholder="Confirm New Password">
</div>
`;
form.appendChild(passwordChangeSection);
const saveButton = createElement('button');
saveButton.type = 'submit';
saveButton.classList.add('btn', 'btn-primary');
saveButton.textContent = 'Save Changes';
form.appendChild(saveButton);
form.addEventListener('submit', async (event) => {
event.preventDefault();
// In a real application, you would collect and validate form data,
// then send it to the backend API for updateAccountSettings.
// For this prototype, we'll just simulate a successful update.
const newPassword = (document.getElementById('newPassword') as HTMLInputElement).value;
const confirmPassword = (document.getElementById('confirmPassword') as HTMLInputElement).value;
if (newPassword && newPassword !== confirmPassword) {
alert('New password and confirm password do not match.');
return;
}
const settingsData = {
// profilePicture: ..., // Handle profile picture upload in real app
password: newPassword || undefined, // Send password only if changed
// ... other settings to update
};
try {
const response: ApiResponse<void> = await globalAPI.updateAccountSettings('mockUserId', settingsData); // Typed response
if (response.success) {
alert('Account settings updated successfully!');
this.onSettingsUpdated(); // Call the callback to hide the modal
} else {
alert('Failed to update account settings.');
}
} catch (error) {
console.error('Error updating account settings:', error);
alert('Error occurred while updating account settings.');
}
});
widgetBody.appendChild(form);
this.container.appendChild(widgetBody);
return this.container;
}
}

View File

@@ -0,0 +1,28 @@
// widgets/BackButtonWidget.ts
import { Widget } from '../components/Widget';
import { createElement, navigateTo } from '../utils/utils';
export class BackButtonWidget extends Widget {
private buttonText: string;
private navigatePath: string;
constructor(buttonText: string, navigatePath: string) {
super();
this.buttonText = buttonText;
this.navigatePath = navigatePath;
}
render(): HTMLElement {
this.container.innerHTML = ''; // Clear previous content
const button = createElement('button');
button.classList.add('btn', 'btn-secondary');
button.textContent = this.buttonText;
button.addEventListener('click', () => {
navigateTo(this.navigatePath);
});
this.container.appendChild(button);
return this.container;
}
}

View File

@@ -0,0 +1,86 @@
// widgets/LoginWidget.ts
import { Widget } from '../components/Widget';
import { createElement, navigateTo } from '../utils/utils';
import { globalAPI, ApiResponse, LoginResponseData } from '../api/api';
export class LoginWidget extends Widget {
constructor() {
super();
}
render(): HTMLElement {
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');
header.classList.add('widget-header');
header.textContent = 'Login';
this.container.appendChild(header);
// Create form
const form = createElement('form');
// User ID input group
const userIdInputGroup = this.createInputGroup('userId', 'Student ID', 'text', 'Student ID');
form.appendChild(userIdInputGroup);
// Password input group
const passwordInputGroup = this.createInputGroup('password', 'Password', 'password', 'Password');
form.appendChild(passwordInputGroup);
// Login button
const loginButton = createElement('button');
loginButton.type = 'submit';
loginButton.classList.add('btn', 'btn-primary');
loginButton.textContent = 'Login';
form.appendChild(loginButton);
// Form submission handler
form.addEventListener('submit', async (event) => {
event.preventDefault();
const userId = (userIdInputGroup.querySelector('input') as HTMLInputElement).value;
const password = (passwordInputGroup.querySelector('input') as HTMLInputElement).value;
try {
const response: ApiResponse<LoginResponseData> = await globalAPI.login({ userId, password });
if (response.success) {
navigateTo('/dashboard');
} else {
alert('Login failed: ' + response.message);
}
} catch (error) {
console.error('Login error:', error);
alert('Login error occurred.');
}
});
this.container.appendChild(form);
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

@@ -0,0 +1,53 @@
// widgets/PostFeedWidget.ts
import { Widget } from '../components/Widget';
import { createElement } from '../utils/utils';
import { globalAPI, ApiResponse, ProfileResponseData } from '../api/api'; // Import API and types (now exported)
export class PostFeedWidget extends Widget {
private posts: { id: number; content: string }[] = [];
constructor() {
super();
this.fetchPosts();
}
async fetchPosts() {
try {
const response: ApiResponse<ProfileResponseData> = await globalAPI.getProfile('mockUserId');
if (response.success && response.data && response.data.posts) {
this.posts = response.data.posts;
this.render();
} else {
console.error("Failed to fetch posts");
}
} catch (error) {
console.error("Error fetching posts:", error);
}
}
render(): HTMLElement {
this.container.innerHTML = '';
const header = createElement('div');
header.classList.add('widget-header');
header.textContent = 'Post Feed';
this.container.appendChild(header);
const widgetBody = createElement('div');
widgetBody.classList.add('widget-body');
if (this.posts.length === 0) {
widgetBody.textContent = 'No posts yet.';
} else {
this.posts.forEach(post => {
const postDiv = createElement('div');
postDiv.classList.add('mb-2', 'p-2', 'border', 'rounded');
postDiv.textContent = post.content;
widgetBody.appendChild(postDiv);
});
}
this.container.appendChild(widgetBody);
return this.container;
}
}

View File

@@ -0,0 +1,110 @@
// widgets/ProfileWidget.ts
import { Widget } from '../components/Widget';
import { createElement } from '../utils/utils';
import { globalAPI, ApiResponse, ProfileResponseData } from '../api/api';
export class ProfileWidget extends Widget {
private profileData: ProfileResponseData | null = null;
constructor() {
super();
this.fetchProfileData();
}
async fetchProfileData() {
try {
const response: ApiResponse<ProfileResponseData> = await globalAPI.getProfile('mockUserId');
if (response.success && response.data) {
this.profileData = response.data;
this.render();
} else {
console.error("Failed to fetch profile data");
}
} catch (error) {
console.error("Error fetching profile data:", error);
}
}
render(): HTMLElement {
this.container.innerHTML = '';
if (!this.profileData) { // Null check here
this.container.textContent = 'Loading profile...';
return this.container;
}
const header = createElement('div');
header.classList.add('widget-header', 'd-flex', 'justify-content-between', 'align-items-center');
const headerText = createElement('div');
headerText.classList.add('widget-title');
headerText.textContent = 'Profile Information';
header.appendChild(headerText);
const kebabMenuButton = createElement('button');
kebabMenuButton.classList.add('btn', 'btn-outline-secondary', 'btn-sm', 'dropdown-toggle');
kebabMenuButton.type = 'button';
kebabMenuButton.id = 'profileKebabMenu';
kebabMenuButton.setAttribute('data-bs-toggle', 'dropdown');
kebabMenuButton.setAttribute('aria-expanded', 'false');
kebabMenuButton.innerHTML = '⋮'; // Kebab menu icon (vertical ellipsis)
header.appendChild(kebabMenuButton);
const dropdownMenu = createElement('ul');
dropdownMenu.classList.add('dropdown-menu', 'dropdown-menu-end');
dropdownMenu.setAttribute('aria-labelledby', 'profileKebabMenu');
const accountSettingsMenuItem = createElement('li');
const accountSettingsLink = createElement('a');
accountSettingsLink.classList.add('dropdown-item');
accountSettingsLink.href = '#/profile';
accountSettingsLink.setAttribute('data-action', 'open-account-settings');
accountSettingsLink.textContent = 'Account settings';
accountSettingsMenuItem.appendChild(accountSettingsLink);
dropdownMenu.appendChild(accountSettingsMenuItem);
header.appendChild(dropdownMenu);
this.container.appendChild(header);
const widgetBody = createElement('div');
widgetBody.classList.add('widget-body', 'row', 'g-3');
const profileImageCol = createElement('div');
profileImageCol.classList.add('col-md-4');
const profileImage = createElement('img');
if (this.profileData.profilePicture) { // Null check before accessing properties
profileImage.src = this.profileData.profilePicture;
} else {
profileImage.src = 'src/assets/vite.svg'; // Default image if no profile picture
}
profileImage.alt = 'Profile Picture';
profileImage.classList.add('img-fluid', 'rounded-circle');
profileImageCol.appendChild(profileImage);
widgetBody.appendChild(profileImageCol);
const detailsCol = createElement('div');
detailsCol.classList.add('col-md-8');
const fullNamePara = createElement('p');
if (this.profileData.fullName) { // Null check before accessing properties
fullNamePara.innerHTML = `${this.profileData.fullName}`;
} else {
fullNamePara.innerHTML = `<small>Full Name:</small><br>N/A`; // Or some default text
}
const schoolIdPara = createElement('p');
if (this.profileData.schoolId) { // Null check before accessing properties
schoolIdPara.innerHTML = `<small><strong>School ID:</small></strong><br>${this.profileData.schoolId}`;
} else {
schoolIdPara.innerHTML = `<small><strong>School ID:</strong></small><br>N/A`;
}
const birthdatePara = createElement('p');
birthdatePara.innerHTML = `<small><strong>Birthdate:</strong></small><br><span class="text-muted">(Hidden from others)</span>`;
detailsCol.appendChild(fullNamePara);
detailsCol.appendChild(schoolIdPara);
detailsCol.appendChild(birthdatePara);
widgetBody.appendChild(detailsCol);
this.container.appendChild(widgetBody);
return this.container;
}
}

View File

@@ -0,0 +1,37 @@
// widgets/RegisterMessageWidget.ts
import { Widget } from '../components/Widget';
import { createElement, navigateTo } from '../utils/utils';
export class RegisterWidget extends Widget {
private message: string;
constructor(message?: string) {
super();
this.message = message || "";
}
render(): HTMLElement {
this.container.innerHTML = '';
const header = createElement('h2');
header.classList.add('widget-header');
header.textContent = 'Register';
const messageParagraph = createElement('p');
messageParagraph.classList.add('widget-body');
messageParagraph.textContent = this.message;
const button = createElement('button');
button.classList.add('btn', 'btn-secondary');
button.textContent = 'Register';
button.addEventListener('click', () => {
navigateTo('/register');
});
this.container.appendChild(header);
this.container.appendChild(messageParagraph);
this.container.appendChild(button);
return this.container;
}
}

View File

@@ -0,0 +1,99 @@
// widgets/StudentFinancialWidget.ts
import { Widget } from '../components/Widget';
import { createElement } from '../utils/utils';
import { globalAPI, ApiResponse, StudentFinancialData } from '../api/api'; // Import API and types
export class StudentFinancialWidget extends Widget {
private studentId: string;
private financialData: StudentFinancialData | null = null;
private isLoading: boolean = true;
private hasError: boolean = false;
constructor(studentId: string) {
super();
this.studentId = studentId;
this.fetchFinancialData();
}
async fetchFinancialData() {
this.isLoading = true;
this.hasError = false;
this.render(); // Re-render to show loading state
try {
const response: ApiResponse<StudentFinancialData> = await globalAPI.getStudentFinancialData(this.studentId);
if (response.success && response.data) {
this.financialData = response.data;
} else {
this.hasError = true;
console.error("Failed to fetch student financial data:", response.message);
}
} catch (error) {
this.hasError = true;
console.error("Error fetching student financial data:", error);
} finally {
this.isLoading = false;
this.render(); // Re-render with fetched data or error state
}
}
render(): HTMLElement {
this.container.innerHTML = ''; // Clear previous content
const widgetBody = createElement('div');
widgetBody.classList.add('widget-body');
if (this.isLoading) {
widgetBody.textContent = 'Loading financial data...';
} else if (this.hasError) {
widgetBody.textContent = 'Failed to load financial data.';
} else if (this.financialData) {
const header = createElement('div');
header.classList.add('widget-header');
header.textContent = 'Payment Information';
this.container.appendChild(header);
this.container.appendChild(header);
widgetBody.innerHTML = `
<div class="row mb-2">
<div class="col-sm-6">Tuition Fee:</div>
<div class="col-sm-6">₱ ${this.financialData.tuitionFee}</div>
</div>
<div class="row mb-2">
<div class="col-sm-6">Miscellaneous:</div>
<div class="col-sm-6">₱ ${this.financialData.miscellaneousFee}</div>
</div>
<div class="row mb-2">
<div class="col-sm-6">Lab Fee:</div>
<div class="col-sm-6">₱ ${this.financialData.labFee}</div>
</div>
<div class="row mb-2">
<div class="col-sm-6">Current:</div>
<div class="col-sm-6">₱ ${this.financialData.currentAccount}</div>
</div>
<div class="row mb-2">
<div class="col-sm-6">Down:</div>
<div class="col-sm-6">₱ ${this.financialData.downPayment}</div>
</div>
<div class="row mb-2">
<div class="col-sm-6">Midterms:</div>
<div class="col-sm-6">₱ ${this.financialData.midtermPayment}</div>
</div>
<div class="row mb-2">
<div class="col-sm-6">Prefinals:</div>
<div class="col-sm-6">₱ ${this.financialData.prefinalPayment}</div>
</div>
<div class="row mb-2">
<div class="col-sm-6">Finals:</div>
<div class="col-sm-6">₱ ${this.financialData.finalPayment}</div>
</div>
`;
} else {
widgetBody.textContent = 'No financial data available.'; // Should not usually reach here if no error but no data
}
this.container.appendChild(widgetBody);
return this.container;
}
}

View File

@@ -0,0 +1,191 @@
// widgets/StudentTableWidget.ts
import { Widget } from '../components/Widget';
import { createElement, navigateTo } from '../utils/utils';
import { globalAPI, ApiResponse, StudentListData } from '../api/api';
export class StudentTableWidget extends Widget {
private students: StudentListData[] = [];
private yearFilter: string = 'all';
private courseFilter: string = 'all';
private availableYears: string[] = ['all', '1', '2', '3', '4'];
private availableCourses: string[] = ['all', 'Math', 'Science', 'History', 'English'];
constructor() {
super();
this.fetchStudentData();
}
async fetchStudentData() {
try {
const response: ApiResponse<StudentListData[]> = await globalAPI.getStudentList();
if (response.success && response.data) {
this.students = response.data;
this.render();
} else {
console.error("Failed to fetch student list");
}
} catch (error) {
console.error("Error fetching student list:", error);
}
}
render(): HTMLElement {
this.container.innerHTML = '';
const header = createElement('div');
header.classList.add('widget-header');
header.textContent = 'Manage Students';
this.container.appendChild(header);
const widgetBody = createElement('div');
widgetBody.classList.add('widget-body');
// Filters
const filtersRow = createElement('div');
filtersRow.classList.add('row', 'mb-3', 'g-3', 'align-items-center');
// Year Level Filter
const yearFilterCol = createElement('div');
yearFilterCol.classList.add('col-auto');
const yearFilterLabel = createElement('label') as HTMLLabelElement; // Cast first
yearFilterLabel.classList.add('col-form-label', 'me-2');
yearFilterLabel.htmlFor = 'yearLevelFilter'; // Set htmlFor as property
yearFilterLabel.textContent = 'Year Level:';
const yearFilterSelect = createElement('select') as HTMLSelectElement;
yearFilterSelect.classList.add('form-select');
yearFilterSelect.id = 'yearLevelFilter';
this.availableYears.forEach(year => {
const option = createElement('option');
option.value = year;
option.textContent = year === 'all' ? 'All Years' : `Year ${year}`;
yearFilterSelect.appendChild(option);
});
yearFilterSelect.value = this.yearFilter;
yearFilterSelect.addEventListener('change', (e) => {
this.yearFilter = (e.target as HTMLSelectElement).value;
this.renderTable(widgetBody);
});
yearFilterCol.appendChild(yearFilterLabel);
yearFilterCol.appendChild(yearFilterSelect);
filtersRow.appendChild(yearFilterCol);
// Course Filter
const courseFilterCol = createElement('div');
courseFilterCol.classList.add('col-auto');
const courseFilterLabel = createElement('label') as HTMLLabelElement; // Cast first
courseFilterLabel.classList.add('col-form-label', 'me-2');
courseFilterLabel.htmlFor = 'courseFilter'; // Set htmlFor as property
courseFilterLabel.textContent = 'Course:';
const courseFilterSelect = createElement('select') as HTMLSelectElement;
courseFilterSelect.classList.add('form-select');
courseFilterSelect.id = 'courseFilter';
this.availableCourses.forEach(course => {
const option = createElement('option');
option.value = course;
option.textContent = course === 'all' ? 'All Courses' : course;
courseFilterSelect.appendChild(option);
});
courseFilterSelect.value = this.courseFilter;
courseFilterSelect.addEventListener('change', (e) => {
this.courseFilter = (e.target as HTMLSelectElement).value;
this.renderTable(widgetBody);
});
courseFilterCol.appendChild(courseFilterLabel);
courseFilterCol.appendChild(courseFilterSelect);
filtersRow.appendChild(courseFilterCol);
widgetBody.appendChild(filtersRow);
// Table Container
const tableContainer = createElement('div');
widgetBody.appendChild(tableContainer);
this.renderTable(tableContainer);
// "Add" button and Batch Add
const addButtonRow = createElement('div');
addButtonRow.classList.add('row', 'mt-3', 'justify-content-between', 'align-items-center');
const addButtonCol = createElement('div');
addButtonCol.classList.add('col-auto');
const addButton = createElement('button');
addButton.classList.add('btn', 'btn-success', 'btn-sm', 'me-2');
addButton.textContent = 'Add Student';
addButtonCol.appendChild(addButton);
addButtonRow.appendChild(addButtonCol);
const batchAddCol = createElement('div');
batchAddCol.classList.add('col-auto');
const batchAddLink = createElement('a');
batchAddLink.href = '#/batch-add-students';
batchAddLink.textContent = 'Batch Add Students';
batchAddCol.appendChild(batchAddLink);
addButtonRow.appendChild(batchAddCol);
widgetBody.appendChild(addButtonRow);
this.container.appendChild(widgetBody);
return this.container;
}
private renderTable(container: HTMLElement) {
container.innerHTML = '';
const table = createElement('table');
table.classList.add('table', 'table-striped', 'table-bordered');
const thead = createElement('thead');
thead.innerHTML = `
<tr>
<th>ID</th>
<th>Name</th>
<th>Year Level</th>
<th>Courses</th>
<th>Actions</th>
</tr>
`;
table.appendChild(thead);
const tbody = createElement('tbody');
const filteredStudents = this.students.filter(student => {
const yearMatch = this.yearFilter === 'all' || student.yearLevel === this.yearFilter;
const courseMatch = this.courseFilter === 'all' || student.courses.includes(this.courseFilter);
return yearMatch && courseMatch;
});
filteredStudents.forEach(student => {
const row = createElement('tr');
row.innerHTML = `
<td>${student.id}</td>
<td>${student.name}</td>
<td>${student.yearLevel}</td>
<td>${student.courses.join(', ')}</td>
<td>
<button class="btn btn-info btn-sm view-profile-btn" data-student-id="${student.id}">View Profile</button>
<button class="btn btn-secondary btn-sm enroll-btn" data-student-id="${student.id}">Enroll</button>
</td>
`;
tbody.appendChild(row);
});
table.appendChild(tbody);
container.appendChild(table);
this.attachTableEventListeners(table);
}
private attachTableEventListeners(table: HTMLTableElement) {
table.querySelectorAll('.view-profile-btn').forEach(button => {
button.addEventListener('click', (event) => {
const studentId = (event.target as HTMLElement).dataset.studentId;
if (studentId) {
navigateTo(`/profile?studentId=${studentId}`);
}
});
});
table.querySelectorAll('.enroll-btn').forEach(button => {
button.addEventListener('click', (event) => {
const studentId = (event.target as HTMLElement).dataset.studentId;
if (studentId) {
alert(`Enroll functionality for student ID ${studentId} - Not fully implemented in prototype.`);
}
});
});
}
}

View File

@@ -0,0 +1,54 @@
// widgets/StudentsWidget.ts
import { Widget } from '../components/Widget';
import { createElement, navigateTo } from '../utils/utils';
import { globalAPI, ApiResponse, AdminDashboardData } from '../api/api'; // Import API and types (now exported)
export class StudentsWidget extends Widget {
private studentCount: number = 0;
constructor() {
super();
this.fetchStudentCount();
}
async fetchStudentCount() {
try {
const response: ApiResponse<AdminDashboardData> = await globalAPI.getAdminDashboardData();
if (response.success && response.data) {
this.studentCount = response.data.studentsCount || 0;
this.render();
} else {
console.error("Failed to fetch admin dashboard data");
}
} catch (error) {
console.error("Error fetching admin dashboard data:", error);
}
}
render(): HTMLElement {
this.container.innerHTML = '';
const header = createElement('div');
header.classList.add('widget-header');
header.textContent = 'Students';
this.container.appendChild(header);
const widgetBody = createElement('div');
widgetBody.classList.add('widget-body', 'text-center');
const countDisplay = createElement('h3');
countDisplay.textContent = String(this.studentCount);
widgetBody.appendChild(countDisplay);
const manageButton = createElement('button');
manageButton.classList.add('btn', 'btn-primary', 'btn-sm');
manageButton.textContent = 'Manage Students';
manageButton.addEventListener('click', () => {
navigateTo('/manage-students');
});
widgetBody.appendChild(manageButton);
this.container.appendChild(widgetBody);
return this.container;
}
}

View File

@@ -0,0 +1,52 @@
// widgets/TeachersWidget.ts
import { Widget } from '../components/Widget';
import { createElement } from '../utils/utils';
import { globalAPI, ApiResponse, AdminDashboardData } from '../api/api'; // Import API and types (now exported)
export class TeachersWidget extends Widget {
private teacherCount: number = 0;
constructor() {
super();
this.fetchTeacherCount();
}
async fetchTeacherCount() {
try {
const response: ApiResponse<AdminDashboardData> = await globalAPI.getAdminDashboardData();
if (response.success && response.data) {
this.teacherCount = response.data.teachersCount || 0;
this.render();
} else {
console.error("Failed to fetch admin dashboard data");
}
} catch (error) {
console.error("Error fetching admin dashboard data:", error);
}
}
render(): HTMLElement {
this.container.innerHTML = '';
const header = createElement('div');
header.classList.add('widget-header');
header.textContent = 'Teachers';
this.container.appendChild(header);
const widgetBody = createElement('div');
widgetBody.classList.add('widget-body', 'text-center');
const countDisplay = createElement('h3');
countDisplay.textContent = String(this.teacherCount);
widgetBody.appendChild(countDisplay);
// In this prototype, teacher management page is not specified, so button is disabled or can link to a placeholder page.
const manageButton = createElement('button');
manageButton.classList.add('btn', 'btn-secondary', 'btn-sm', 'disabled'); // Disabled for prototype
manageButton.textContent = 'Manage Teachers (Coming Soon)';
widgetBody.appendChild(manageButton);
this.container.appendChild(widgetBody);
return this.container;
}
}

View File

@@ -0,0 +1,38 @@
// widgets/UnderConstructionWidget.ts
import { Widget } from '../components/Widget';
import { createElement } from '../utils/utils';
export class UnderConstructionWidget extends Widget {
private message: string;
constructor(message: string) {
super();
this.message = message;
}
render(): HTMLElement {
this.container.innerHTML = ''; // Clear previous content
const header = createElement('div');
header.classList.add('widget-header', 'text-center');
header.textContent = 'Under Construction';
this.container.appendChild(header);
const widgetBody = createElement('div');
widgetBody.classList.add('widget-body', 'text-center');
widgetBody.textContent = this.message;
this.container.appendChild(widgetBody);
const icon = createElement('i');
icon.classList.add('bi', 'bi-tools', 'd-block', 'mx-auto', 'my-3'); // Bootstrap Icons class, needs Bootstrap Icons CSS if you want to use icons.
icon.style.fontSize = '2em'; // Example icon style, can be customized.
// Note: Bootstrap Icons would need to be included in index.html if used.
// For prototype simplicity, we'll skip including Bootstrap Icons CSS.
// If you want to add Bootstrap Icons, include this in index.html <head>:
// <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
// widgetBody.appendChild(icon); // Uncomment if you decide to include Bootstrap Icons.
return this.container;
}
}