diff --git a/README.md b/README.md index cfe2fca..c5a10fe 100644 --- a/README.md +++ b/README.md @@ -25,19 +25,43 @@ Pixel Schedule is an interactive gamification platform for school schedule. Here you can see where each teacher is and which lesson is being taught in the classroom. It is linked to the Magister app and keeps us informed of changes through fun interactive effects and notifications. -# Installation +# Access schedule data -Due to sensitive data, you need to install the API on your device and log in to allow the webapp to access your rooster. +To handle the school schedule data, we've created a small temporary workaround that connects our webapp directly to the Xedule API. +For a temporarily bypass of the standard cross-origin security, use the following command in CMD: -Here is the link where you can install: -https://github.com/bodhivb/pixel-schedule-api/ (WIP - Coming soon) + start chrome.exe --user-data-dir="c:/temp/chrome" --disable-web-security https://bodhivb.github.io/pixel-schedule/ --kiosk -After installation, you can view the Pixel Schedule at: -https://bodhivb.github.io/pixel-schedule/ +Caution: don't disable web security on other sites without understanding their source code, as it can expose you to security risks. # For developers The webapp is written in TypeScript using [PixiJS](https://github.com/pixijs/pixijs) v7. When building the project, TypeScript files are compiles to JavaScript and all code is bundled into one minified file using [Webpack](https://github.com/webpack/webpack). +Feel free to view the source code and add something fun as a pull request to make the SintLucas Pixel-Schedule even cooler! + If you have any questions? Feel free to contact me via Discord [Bodhi#0001](https://discord.com/users/151423248020537345). -Your feedbacks are welcome! :) + +# Desired outcomes + +### Real-time Schedule + +That teachers stay in the classroom where their lesson is, is not quite finished yet, when they are finished then the basic concept is finished! + +### Interactive + +However, it still lacks interactive feature, including clickable teacher profiles and random speech bubbles corresponding to their current lesson and other fun interactives. Feel free to suggest any other additions! + +### Pixel Schedule API + +Build an API that users can connect to via [SurfConext](https://wiki.surfnet.nl/display/surfconextdev/Documentation+for+Service+Providers) to access data on teachers presence, skills, and important notifications/changes. + +### Display date + +Display the date and time in the top left corner, resembling a lock screen or Wallpaper Engine. This allows for convenient access to useful default information while maintaining a visually pleasing background. + +### Weather + +Add weather integration, based on school location. Show corresponding weather conditions within the Pixel Schedule, such as rain when it's raining. + +Furthermore, the app already includes real-time background changes for daytime and evening. Thanks to [@Martijndewin](https://github.com/Martijndewin) for his contribution to the development of this feature. diff --git a/assets/css/base.css b/assets/css/base.css index 18d2256..129bdf8 100644 --- a/assets/css/base.css +++ b/assets/css/base.css @@ -4,6 +4,7 @@ body { border: 0; padding: 0; overflow: hidden; + font-family: Arial, Helvetica, sans-serif; } /* OVERLAY CSS */ diff --git a/assets/examples/teachers.json b/assets/examples/teachers.json deleted file mode 100644 index 08e4d64..0000000 --- a/assets/examples/teachers.json +++ /dev/null @@ -1,26 +0,0 @@ -[ - { - "firstName": "Bodhi", - "lastName": "van Baardewijk", - "imageKey": "bb", - "imagePath": "assets/teachers/bb.png", - "function": 1, - "team": 2, - "skills": [ - { - "title": "Unity", - "description": "5+ year experience programming with it." - }, - { - "title": "C#" - } - ] - }, - { - "firstName": "lp", - "imageKey": "lp", - "imagePath": "assets/teachers/lp.png", - "function": 2, - "team": 1 - } -] diff --git a/package-lock.json b/package-lock.json index af0e493..e7ffa31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4357,9 +4357,9 @@ } }, "node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -8724,9 +8724,9 @@ } }, "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" diff --git a/source/api/appointmentApi.ts b/source/api/appointmentApi.ts new file mode 100644 index 0000000..6a8d248 --- /dev/null +++ b/source/api/appointmentApi.ts @@ -0,0 +1,43 @@ +import { IXeduleAppointment } from "../interfaces/xedule/xeduleAppointment"; +import { XeduleApi } from "./xeduleApi"; + +export class AppointmentApi extends XeduleApi { + constructor() { + super("Appointment/Date/"); + } + + /** + * Get the appointment for the current week with selected attendee + * @returns + */ + public async getAppointment(...attendeeIds: number[]): Promise { + // Week date + const today = new Date(); + const nextWeek = new Date( + today.getFullYear(), + today.getMonth(), + today.getDate() + 7 + ); + + return super.get( + this.dateToPath(today) + + "/" + + this.dateToPath(nextWeek) + + "/Attendee?id=" + + attendeeIds.join("&id=") + ); + } + + /** + * Convert date to xedule path format + * @param date + * @returns + */ + public dateToPath(date: Date) { + const day = date.getDate(); + const month = date.getMonth() + 1; // January is 0 + const year = date.getFullYear(); + + return `${year}-${month}-${day}`; + } +} diff --git a/source/api/authenticationApi.ts b/source/api/authenticationApi.ts new file mode 100644 index 0000000..c9d61e3 --- /dev/null +++ b/source/api/authenticationApi.ts @@ -0,0 +1,163 @@ +import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios"; + +interface ForwardResponse { + errorText: string; + forwardCount: number; + lastResponse: AxiosResponse; +} + +interface LoginData { + username: string; + password: string; +} + +interface AuthenticationResponse { + isSuccess: boolean; + errorMessage?: string; + token?: string; +} + +export class AuthenticationApi { + private httpService: AxiosInstance; + private readonly authenticationUrl: string = + "https://myx-silu.xedule.nl/Authentication/sso/SSOLogin.aspx?ngsw-bypass=true"; + + constructor() { + this.httpService = axios.create({ withCredentials: true }); + } + + public async checkAuthStatus() { + return this.httpService.get(this.authenticationUrl); + } + + /** + * Authenticated via SURFConext authentication and return token + * @param body login data + * @returns response result status with token + */ + public async authentication( + body: LoginData + ): Promise { + // Setup first request + const firstRequest: AxiosRequestConfig = { + method: "get", + url: this.authenticationUrl, + }; + + // Forward the request + const { errorText, lastResponse } = await this.createForwardRequest( + this.httpService, + firstRequest, + 5, + body + ); + + // Error handling + if (errorText) { + return { isSuccess: false, errorMessage: errorText }; + } + // Error handling + if (!lastResponse.config.url?.includes("sso.xedule.nl")) { + return { isSuccess: false, errorMessage: "Result is different" }; + } + + // Get the token + let token: string = ( + lastResponse.request.path ?? lastResponse.request.responseURL + ).toString(); + + if (token) { + if (token.includes("token=")) { + token = token.substring(token.indexOf("token=") + "token=".length); + } + + if (token.includes("&ngsw-bypass=")) { + token = token.substring(0, token.indexOf("&ngsw-bypass=")); + } + + // Return token + return { isSuccess: true, token }; + } + + return { + isSuccess: false, + errorMessage: "This application is out of date", + }; + } + + /** + * createForwardRequest will be forwarded automatically if there is submit form in response + * @param axiosInstance + * @param firstRequest + * @param maxForwards + * @param loginData + * @returns The last response + */ + async createForwardRequest( + axiosInstance: AxiosInstance, + firstRequest: AxiosRequestConfig, + maxForwards: number = 5, + loginData: { username: string; password: string } + ): Promise { + let errorText = ""; + let forwardCount = 0; + let lastResponse = await axiosInstance.request(firstRequest); + const dom = new DOMParser(); + + for (; forwardCount < maxForwards; forwardCount++) { + const document = dom.parseFromString(lastResponse.data, "text/html"); + const forwardForm = document.querySelector("form"); + + // Stop forward if response has no form + if (!forwardForm) { + break; + } + + // Stop forward if there are errors + const errorElement = document.getElementById("errorText"); + if (errorElement && errorElement.innerHTML) { + errorText = errorElement.innerHTML; + break; + } + + // Create a new config request + const config: AxiosRequestConfig = {}; + config.url = forwardForm.action; + config.method = forwardForm.method ?? "post"; + config.data = {}; + + // Copy the form data into a new request + const inputs = forwardForm.querySelectorAll("input"); + inputs.forEach((input) => (config.data[input.name] = input.value)); + + // Login page + if (lastResponse.data.includes("FormsAuthentication")) { + // Fill the username and password in + config.data["UserName"] = loginData.username; + config.data["Password"] = loginData.password; + + if (config.url.startsWith(window.location.origin)) { + // Remove the origin from the url + config.url = config.url.substring(window.location.origin.length); + } + + if (!config.url.startsWith("http")) { + // Fix the url with domain name + config.url = "https://adfsproxy.sintlucas.nl:443" + config.url; + } + } + + // Stop forward if a new request is same as previous + if (lastResponse.config.url == config.url) { + errorText = "The request has been black holed."; + break; + } + + // Convert json object to http query string + config.data = new URLSearchParams(config.data); + lastResponse = await axiosInstance.request(config); + } + + return { errorText, forwardCount, lastResponse }; + } +} diff --git a/source/api/classroomApi.ts b/source/api/classroomApi.ts new file mode 100644 index 0000000..11986b9 --- /dev/null +++ b/source/api/classroomApi.ts @@ -0,0 +1,8 @@ +import { IXeduleClassroom } from "../interfaces/xedule/xeduleClassroom"; +import { XeduleApi } from "./xeduleApi"; + +export class ClassroomApi extends XeduleApi { + constructor() { + super("Attendee/Type/Classroom"); + } +} diff --git a/source/api/teacherApi.ts b/source/api/teacherApi.ts new file mode 100644 index 0000000..d074d8c --- /dev/null +++ b/source/api/teacherApi.ts @@ -0,0 +1,8 @@ +import { IXeduleTeacher } from "../interfaces/xedule/xeduleTeacher"; +import { XeduleApi } from "./xeduleApi"; + +export class TeacherApi extends XeduleApi { + constructor() { + super("Attendee/Type/Teacher"); + } +} diff --git a/source/api/xeduleApi.ts b/source/api/xeduleApi.ts new file mode 100644 index 0000000..a95623e --- /dev/null +++ b/source/api/xeduleApi.ts @@ -0,0 +1,35 @@ +import axios from "axios"; + +export class XeduleApi { + private baseUrl: string = "https://myx-silu.xedule.nl/api/"; + private basePath: string; + + public data: T[] = []; + + /** + * Abstract class + * @param basePath The path to the api endpoint + */ + constructor(basePath: string) { + this.basePath = basePath; + } + + /** + * Get the data from the xedule api + * @param path URL parameters to add to the base path + * @returns + */ + public async get(path: string = ""): Promise { + const res = await axios.get(this.baseUrl + this.basePath + path, { + headers: { Authorization: "Bearer " + localStorage.getItem("token") }, + }); + + if (res.status == 200 && res.data.result && res.data.result.length > 0) { + this.data = res.data.result; + } else { + //TODO error handling + } + + return this.data; + } +} diff --git a/source/app.ts b/source/app.ts index b93e5f8..f78b990 100644 --- a/source/app.ts +++ b/source/app.ts @@ -18,9 +18,13 @@ export default class App extends Application { // Initialize GameManager GameManager.init(this, overlay); - - this.ticker.maxFPS = 90; + this.ticker.add((dt) => this.Update(dt)); + + // call updateMinute once every minute + setInterval(function(self) { + self.ticker.addOnce((dt) => self.UpdateMinute(dt)); + }, 60 * 1000, this) } // Trigger when the browser window is resized. @@ -32,4 +36,9 @@ export default class App extends Application { private Update(dt: number) { GameManager.instance.Update(dt); } + + private UpdateMinute(dt: number) { + GameManager.instance.UpdateMinute(dt); + } + } diff --git a/source/components/loginComponent.ts b/source/components/loginComponent.ts index 58e7685..e7a4a69 100644 --- a/source/components/loginComponent.ts +++ b/source/components/loginComponent.ts @@ -1,6 +1,6 @@ -import axios from "axios"; import { createInput } from "../elements/input"; import { createLabel } from "../elements/label"; +import { authenticationService } from "../services/authenticationService"; // Login component export class LoginComponent { @@ -15,7 +15,7 @@ export class LoginComponent { // Create the login form this.form = document.createElement("form"); this.form.id = "loginForm"; - this.form.appendChild(createLabel("Log in to retrieve the data")); + this.form.appendChild(createLabel("Log in to retrieve the schedule data")); // Add the submit event this.form.addEventListener("submit", (event) => this.Submit(event)); @@ -27,7 +27,7 @@ export class LoginComponent { // Create the input fields this.form.appendChild( - createInput("email", "iemand@example.com", "username") + createInput("email", "iemand@sintlucas.nl", "username") ); this.form.appendChild( createInput("password", "Wachtwoord", "current-password") @@ -50,25 +50,19 @@ export class LoginComponent { // Send the form data to the server try { this.errorLabel.innerHTML = ""; - const res = await axios.post("http://localhost:3000/xedule", { - username: formData.get("email"), - password: formData.get("password"), - }); - - if (res.data.token) { - // Save the token - localStorage.setItem("token", res.data.token); + + const isSuccessful = await authenticationService.login( + formData.get("email")?.toString() ?? "", + formData.get("password")?.toString() ?? "" + ); + + if (isSuccessful) { window.location.reload(); } else { - this.errorLabel.innerHTML = "Unexpected error, please try again later."; + this.errorLabel.innerHTML = "Unexpected error (21)"; } } catch (ex) { - // Error handling - if (axios.isAxiosError(ex)) { - this.errorLabel.innerHTML = ex.response?.data.message ?? ex.message; - } else { - this.errorLabel.innerHTML = "Unexpected error: " + (ex as string); - } + this.errorLabel.innerHTML = (ex as string) ?? "Unexpected error (22)"; } this.setEnabledForm(true); diff --git a/source/components/uploadDataComponent.ts b/source/components/uploadDataComponent.ts index 83ef0b1..7e01946 100644 --- a/source/components/uploadDataComponent.ts +++ b/source/components/uploadDataComponent.ts @@ -16,20 +16,13 @@ export class UploadDataComponent { // Create the upload data form this.form = document.createElement("form"); this.form.id = "uploadDataForm"; - this.form.appendChild(createLabel("Upload the local data")); + this.form.appendChild(createLabel("Upload the teacher data")); // Create the error label this.errorLabel = document.createElement("div"); this.errorLabel.id = "errorLabel"; this.form.appendChild(this.errorLabel); - // Create the rooster data input field - this.form.appendChild(createLabel("Rooster data")); - const roosterInput = createInputFile((value) => - this.handleRoosterData(value) - ); - this.form.appendChild(roosterInput); - // Create the teacher data input field this.form.appendChild(createLabel("Teacher data")); const teacherInput = createInputFile((value) => @@ -38,19 +31,6 @@ export class UploadDataComponent { this.form.appendChild(teacherInput); } - /** - * Handle the rooster data. - * @param data rooster data interface - * @returns - */ - private handleRoosterData(data: any) { - if (typeof data == "string") return; - - // Check if the data is correct - if (Array.isArray(data) && data.length > 0) { - } - } - /** * Handle the teacher data. * @param data teacher data interface diff --git a/source/assetsManifest.ts b/source/constants/assetsManifest.ts similarity index 100% rename from source/assetsManifest.ts rename to source/constants/assetsManifest.ts diff --git a/source/constants/linkTeacherData.ts b/source/constants/linkTeacherData.ts new file mode 100644 index 0000000..693ed26 --- /dev/null +++ b/source/constants/linkTeacherData.ts @@ -0,0 +1,33 @@ +import { ILinkTeacher } from "../interfaces/linkTeacherInterface"; + +/** linking the Xedule teacher to their pixel images. */ +export const LinkTeacherData: ILinkTeacher[] = [ + { imageKey: "ah", teacherId: 0 }, + { imageKey: "bb", teacherId: 17877 }, + { imageKey: "bk", teacherId: 0 }, + { imageKey: "bp", teacherId: 0 }, + { imageKey: "bt", teacherId: 0 }, + { imageKey: "cs", teacherId: 0 }, + { imageKey: "db", teacherId: 0 }, + { imageKey: "dr", teacherId: 0 }, + { imageKey: "ew", teacherId: 0 }, + { imageKey: "fv", teacherId: 0 }, + { imageKey: "gk", teacherId: 0 }, + { imageKey: "jk", teacherId: 0 }, + { imageKey: "js", teacherId: 0 }, + { imageKey: "jw", teacherId: 0 }, + { imageKey: "kd", teacherId: 0 }, + { imageKey: "lf", teacherId: 0 }, + { imageKey: "lp", teacherId: 0 }, + { imageKey: "mh-1", teacherId: 0 }, + { imageKey: "mh-2", teacherId: 0 }, + { imageKey: "no", teacherId: 0 }, + { imageKey: "pg", teacherId: 0 }, + { imageKey: "pl-1", teacherId: 0 }, + { imageKey: "pl-2", teacherId: 0 }, + { imageKey: "rh", teacherId: 0 }, + { imageKey: "ss", teacherId: 0 }, + { imageKey: "tb-1", teacherId: 0 }, + { imageKey: "tb-2", teacherId: 0 }, + { imageKey: "wh", teacherId: 0 }, +]; diff --git a/source/schoolData.ts b/source/constants/schoolData.ts similarity index 90% rename from source/schoolData.ts rename to source/constants/schoolData.ts index 8aee194..6389647 100644 --- a/source/schoolData.ts +++ b/source/constants/schoolData.ts @@ -1,5 +1,5 @@ -import { IFloor } from "./interfaces/floorInterface"; -import { RoomType } from "./interfaces/roomType"; +import { IFloor } from "../interfaces/floorInterface"; +import { RoomType } from "../interfaces/roomType"; /** * SintLucas front rooms. diff --git a/source/updateAssetsManifest.js b/source/constants/updateAssetsManifest.js similarity index 96% rename from source/updateAssetsManifest.js rename to source/constants/updateAssetsManifest.js index ec11ef1..e963544 100644 --- a/source/updateAssetsManifest.js +++ b/source/constants/updateAssetsManifest.js @@ -29,7 +29,7 @@ try { bundle.assets = []; const files = fs.readdirSync( - path.resolve(__dirname, "../assets/teachers") + path.resolve(__dirname, "../../assets/teachers") ); // Push new assets to bundle diff --git a/source/elements/popup.ts b/source/elements/popup.ts index 65c8e29..fa49e4b 100644 --- a/source/elements/popup.ts +++ b/source/elements/popup.ts @@ -3,7 +3,7 @@ * @param width Popup width * @returns HTML div element */ -export const createPopup = (width: number = 700, child?: HTMLElement) => { +export const createPopup = (width: number = 500, child?: HTMLElement) => { const popup = document.createElement("div"); popup.classList.add("popup"); popup.style.maxWidth = width + "px"; diff --git a/source/elements/teacherBox.ts b/source/elements/teacherBox.ts index ea864ad..83edec0 100644 --- a/source/elements/teacherBox.ts +++ b/source/elements/teacherBox.ts @@ -12,7 +12,7 @@ import { createLabel } from "./label"; export const createTeacherBox = (teacher: ITeacher) => { // Create box const box = document.createElement("div"); - box.id = teacher.firstName; + box.id = teacher.firstName ?? "Teacher"; box.classList.add("teacherBox"); const color = getTeacherColor(teacher); @@ -27,7 +27,14 @@ export const createTeacherBox = (teacher: ITeacher) => { // Add text const text = document.createElement("div"); text.classList.add("teacherText"); - const teacherName = teacher.firstName + " " + (teacher.lastName ?? ""); + + let teacherName = ""; + if (teacher.firstName || teacher.lastName) { + teacherName = (teacher.firstName ?? "") + " " + (teacher.lastName ?? ""); + } else { + teacherName = teacher.imageKey; + } + text.appendChild(createLabel(teacherName)); if (teacher.function) diff --git a/source/interfaces/entityEvent.ts b/source/interfaces/entityEvent.ts index 862113a..a5f6f64 100644 --- a/source/interfaces/entityEvent.ts +++ b/source/interfaces/entityEvent.ts @@ -5,6 +5,9 @@ export interface IEntityEvent { /** Update is called once per frame. */ Update?(dt: number): void; + /** Update is called once per minute. */ + UpdateMinute?(dt: number): void; + /** OnDestroy is called when the object was disabled during the frame. */ OnDestroy?(): void; } diff --git a/source/interfaces/linkTeacherInterface.ts b/source/interfaces/linkTeacherInterface.ts new file mode 100644 index 0000000..dccd14a --- /dev/null +++ b/source/interfaces/linkTeacherInterface.ts @@ -0,0 +1,10 @@ +/** + * Interface for linking Xedule teachers to their pixel images. + */ +export interface ILinkTeacher { + /** Xedule teacher id. */ + teacherId: number; + + /** File name or existing teachers pixel image. */ + imageKey: string; +} diff --git a/source/interfaces/teacher/teacherInterface.ts b/source/interfaces/teacher/teacherInterface.ts index e4fb1b7..20a87df 100644 --- a/source/interfaces/teacher/teacherInterface.ts +++ b/source/interfaces/teacher/teacherInterface.ts @@ -4,11 +4,12 @@ import { ITeacherSkill } from "./teacherSkillInterface"; export interface ITeacher { // Required properties - firstName: string; imageKey: string; imagePath: string; // Optional properties + id?: number; + firstName?: string; lastName?: string; function?: TeacherFunction; team?: TeacherTeam; diff --git a/source/interfaces/xedule/xeduleAppointment.ts b/source/interfaces/xedule/xeduleAppointment.ts new file mode 100644 index 0000000..d0bf9d5 --- /dev/null +++ b/source/interfaces/xedule/xeduleAppointment.ts @@ -0,0 +1,22 @@ +import { IXeduleAttendeeIds } from "./xeduleAttendeeIds"; + +export interface IXeduleAppointment { + appointments: { + [key: number]: { + id: number; + attendeeIds: IXeduleAttendeeIds; + code: string; + name: string; + type: string; + start: string; + end: string; + }; + }; + days: { + [key: number]: { + id: number; + appointmentIds: number[]; + date: string; + }; + }; +} diff --git a/source/interfaces/xedule/xeduleAttendeeIds.ts b/source/interfaces/xedule/xeduleAttendeeIds.ts new file mode 100644 index 0000000..5312d98 --- /dev/null +++ b/source/interfaces/xedule/xeduleAttendeeIds.ts @@ -0,0 +1,7 @@ +export interface IXeduleAttendeeIds { + classroom: number[]; + group: number[]; + materials: number[]; + student: number[]; + teacher: number[]; +} diff --git a/source/interfaces/xedule/xeduleClassroom.ts b/source/interfaces/xedule/xeduleClassroom.ts new file mode 100644 index 0000000..115875e --- /dev/null +++ b/source/interfaces/xedule/xeduleClassroom.ts @@ -0,0 +1,8 @@ +import { IXeduleFacilityType } from "./xeduleFacilityType"; + +export interface IXeduleClassroom { + id: number; + code: string; + location: string; + facilityTypes: IXeduleFacilityType[]; +} diff --git a/source/interfaces/xedule/xeduleFacilityType.ts b/source/interfaces/xedule/xeduleFacilityType.ts new file mode 100644 index 0000000..b1b0338 --- /dev/null +++ b/source/interfaces/xedule/xeduleFacilityType.ts @@ -0,0 +1,4 @@ +export interface IXeduleFacilityType { + name: string; + capacity: number; +} diff --git a/source/interfaces/xedule/xeduleTeacher.ts b/source/interfaces/xedule/xeduleTeacher.ts new file mode 100644 index 0000000..cbd2aaa --- /dev/null +++ b/source/interfaces/xedule/xeduleTeacher.ts @@ -0,0 +1,8 @@ +export interface IXeduleTeacher { + id: number; + code: string; + firstName?: string; + lastName?: string; + role: string; + teams: number[]; +} diff --git a/source/managers/gameManager.ts b/source/managers/gameManager.ts index ca7f4fb..bd23d1e 100644 --- a/source/managers/gameManager.ts +++ b/source/managers/gameManager.ts @@ -1,11 +1,14 @@ import { Assets } from "pixi.js"; -import { assetsManifest } from "../assetsManifest"; +import { assetsManifest } from "../constants/assetsManifest"; import App from "../app"; import Overlay from "../overlay"; import { SceneManager } from "./sceneManager"; import { MainScene } from "../scenes/mainScene"; import { SetupView } from "../views/setupView"; import { SearchView } from "../views/searchView"; +import { teacherStore } from "../store/teacherStore"; +import { teacherService } from "../services/teacherService"; +import { LoginView } from "../views/loginView"; export class GameManager { // #region singleton @@ -49,6 +52,10 @@ export class GameManager { // Load all assets this.LoadAssets().then(() => { + // Loading assets is complete + teacherStore.resetToDefaultData(); + teacherService.fetchTeachers(); + this.OpenActiveScreen(); }); } @@ -65,7 +72,8 @@ export class GameManager { // Download all assets const assets = await Assets.loadBundle(bundleIds, (value) => - console.log(value) + //TODO Add loading bar (value increase from 0.0 to 1.0) + {} ); // Loading assets is complete @@ -76,6 +84,9 @@ export class GameManager { private OpenActiveScreen() { SceneManager.Add(new MainScene()); // Page -> View -> Component -> Element + const loginView = new LoginView(); + this.HTMLoverlay?.Add(loginView); + const setupView = new SetupView(); this.HTMLoverlay?.Add(setupView); @@ -86,4 +97,8 @@ export class GameManager { public Update(dt: number) { SceneManager.UpdateScenes(dt); } + + public UpdateMinute(dt: number) { + SceneManager.UpdateMinuteScenes(dt); + } } diff --git a/source/managers/sceneManager.ts b/source/managers/sceneManager.ts index 6834ca2..5e251e0 100644 --- a/source/managers/sceneManager.ts +++ b/source/managers/sceneManager.ts @@ -28,6 +28,14 @@ export class SceneManager { this.activeScenes[s].Update(dt); } } + + /** Update the active scene(s). */ + public static UpdateMinuteScenes(dt: number) { + // Loop over list of all opened scenes + for (let s = 0; s < this.activeScenes.length; s++) { + this.activeScenes[s].UpdateMinute(dt); + } + } /** Remove the active scene(s). */ public static RemoveScene() { diff --git a/source/objects/backgroundColor.ts b/source/objects/backgroundColor.ts index 77c6af2..b984121 100644 --- a/source/objects/backgroundColor.ts +++ b/source/objects/backgroundColor.ts @@ -2,14 +2,12 @@ import { Sprite, Texture } from "pixi.js"; import { IEntityEvent } from "../interfaces/entityEvent"; export class BackgroundColor extends Sprite implements IEntityEvent { - //TODO Add color changed by day time - constructor() { //TODO Remove c_height/c_width because these are magic code const c_height = 800 + 2; const c_width = 1920; - const gradTexture = BackgroundColor.createGradientTexture(c_height); + const gradTexture = BackgroundColor.createGradientTexture(c_height, BackgroundColor.getDaylightValue()); super(gradTexture); this.position.set(0, 0); @@ -22,11 +20,10 @@ export class BackgroundColor extends Sprite implements IEntityEvent { * @param quality adjust it if somehow you need better quality for very very big images * @returns */ - static createGradientTexture(quality: number = 500, nightValue: number = 0) { + static createGradientTexture(quality: number = 500, daylightValue: number = 0) { const canvas = document.createElement("canvas"); canvas.width = 1; canvas.height = quality; - const ctx = canvas.getContext("2d")!; const colorNight: Color = { r: 8, g: 8, b: 25 }; @@ -34,17 +31,17 @@ export class BackgroundColor extends Sprite implements IEntityEvent { const colorDayBottom: Color = { r: 222, g: 244, b: 254 }; const colorTop: Color = { - r: BackgroundColor.Lerp(colorDayTop.r, colorNight.r, nightValue), - g: BackgroundColor.Lerp(colorDayTop.g, colorNight.g, nightValue), - b: BackgroundColor.Lerp(colorDayTop.b, colorNight.b, nightValue), + r: BackgroundColor.Lerp(colorNight.r, colorDayTop.r, daylightValue), + g: BackgroundColor.Lerp(colorNight.g, colorDayTop.g, daylightValue), + b: BackgroundColor.Lerp(colorNight.b, colorDayTop.b, daylightValue), }; const colorBottom: Color = { - r: BackgroundColor.Lerp(colorDayBottom.r, colorNight.r, nightValue), - g: BackgroundColor.Lerp(colorDayBottom.g, colorNight.g, nightValue), - b: BackgroundColor.Lerp(colorDayBottom.b, colorNight.b, nightValue), + r: BackgroundColor.Lerp(colorNight.r, colorDayBottom.r, daylightValue), + g: BackgroundColor.Lerp(colorNight.g, colorDayBottom.g, daylightValue), + b: BackgroundColor.Lerp(colorNight.b, colorDayBottom.b, daylightValue), }; - //const data; + // use canvas2d API to create gradient const grd = ctx.createLinearGradient(0, 0, 0, quality); @@ -66,27 +63,10 @@ export class BackgroundColor extends Sprite implements IEntityEvent { return Texture.from(canvas); } - dayTimer = 0; - daySwitch = false; - - Update(dt: number) { - if (this.daySwitch) { - this.dayTimer -= dt / 120; - } else { - this.dayTimer += dt / 120; - } - - if (this.dayTimer >= 2) { - this.daySwitch = true; - } - - if (this.dayTimer <= -1) { - this.daySwitch = false; - } - + UpdateMinute(dt: number) { this.texture = BackgroundColor.createGradientTexture( 500, - Math.max(Math.min(this.dayTimer, 1), 0) + BackgroundColor.getDaylightValue() ); } @@ -99,6 +79,18 @@ export class BackgroundColor extends Sprite implements IEntityEvent { public static Lerp(a: number, b: number, t: number) { return (1 - t) * a + b * t; } + + /** + * Function to calculate daylight based on current time + * @return value between 0 and 1 where 1 (12:00) is full daylight + */ + public static getDaylightValue() { + const currentDay = new Date; + const totalMinutes = currentDay.getHours() * 60 + currentDay.getMinutes() + // max minutes per day = 1540 + // 720 = 12:00 + return (-Math.cos(Math.PI/770 * totalMinutes) + 1) / 2; + } } interface Color { diff --git a/source/scenes/scene.ts b/source/scenes/scene.ts index 95fa704..e8b960b 100644 --- a/source/scenes/scene.ts +++ b/source/scenes/scene.ts @@ -25,6 +25,19 @@ export abstract class Scene extends Container { } } + UpdateMinute(dt: number) { + // List of all views + for (let v = 0; v < this.children.length; v++) { + // TODO Check before if function exists. If yes > add it to event listeren + (this.children[v] as IEntityEvent).UpdateMinute?.(dt); + + // List of all entities from view + for (let e = 0; e < this.children[v].children.length; e++) { + (this.children[v].children[e] as IEntityEvent).UpdateMinute?.(dt); + } + } + } + Remove() { //List of all views for (let v = 0; v < this.children.length; v++) { diff --git a/source/services/authenticationService.ts b/source/services/authenticationService.ts new file mode 100644 index 0000000..c736273 --- /dev/null +++ b/source/services/authenticationService.ts @@ -0,0 +1,58 @@ +import axios from "axios"; +import { AuthenticationApi } from "../api/authenticationApi"; + +class AuthenticationService { + public api: AuthenticationApi; + + constructor() { + this.api = new AuthenticationApi(); + } + + public async isLoggedIn(): Promise { + // Has a token + const currentToken = localStorage.getItem("token"); + if (!currentToken) return false; + + try { + const result = await this.api.checkAuthStatus(); + + // Must be successful + if (result.status != 200) return false; + + // Login page has a length of 24324 + // Granted page has a length of 7140 + if (result.data.length > 10000) return false; + } catch (error) { + if (axios.isAxiosError(error)) { + if (!error.response) { + console.log("Network error"); + console.log("Can be caused by a CORS error"); + } + } + + return false; + } + + return true; + } + + public async login(username: string, password: string): Promise { + const result = await this.api.authentication({ + username, + password, + }); + + if (!result.isSuccess) { + console.warn("Authentication failed (21)", result.errorMessage); + return false; + } + + // Save the token + localStorage.setItem("token", result.token ?? ""); + + // TODO restart the service + return true; + } +} + +export const authenticationService = new AuthenticationService(); diff --git a/source/services/teacherService.ts b/source/services/teacherService.ts new file mode 100644 index 0000000..b3d40ea --- /dev/null +++ b/source/services/teacherService.ts @@ -0,0 +1,46 @@ +import { TeacherApi } from "../api/teacherApi"; +import { ITeacher } from "../interfaces/teacher/teacherInterface"; +import { teacherStore } from "../store/teacherStore"; + +class TeacherService { + public api: TeacherApi; + + constructor() { + this.api = new TeacherApi(); + + // TODO: Check first if user is authenticated + // Then fetch the teachers + //this.fetchTeachers(); + } + + public async fetchTeachers() { + const result = await this.api.get(); + + let teacher: ITeacher[] = Object.assign([], teacherStore.GetData()); + let isChanged = false; + + for (let data of result) { + if (!data.firstName || !data.lastName) continue; + + const fName = data.firstName.toLowerCase(); + const lName = data.lastName.toLowerCase(); + + const match = teacher.findIndex( + (e) => + e.id == data.id && + (e.firstName?.toLowerCase() != fName || + e.lastName?.toLowerCase() != lName) + ); + + if (match >= 0) { + teacher[match].firstName = fName; + teacher[match].lastName = lName; + isChanged = true; + } + } + + if (isChanged) teacherStore.SetData(teacher); + } +} + +export const teacherService = new TeacherService(); diff --git a/source/store/teacherStore.ts b/source/store/teacherStore.ts index 68ec2a1..8e41763 100644 --- a/source/store/teacherStore.ts +++ b/source/store/teacherStore.ts @@ -1,5 +1,7 @@ +import { Assets } from "pixi.js"; import { ITeacher } from "../interfaces/teacher/teacherInterface"; import { DataStore } from "./dataStore"; +import { LinkTeacherData } from "../constants/linkTeacherData"; /** Teacher Store */ class TeacherStore extends DataStore { @@ -7,6 +9,28 @@ class TeacherStore extends DataStore { super("teachers"); } + // Must be called after assets are loaded! + public async resetToDefaultData() { + const teacherAssets = await Assets.loadBundle("teachers"); + const teachers = Object.keys(teacherAssets); + + const teacherData: ITeacher[] = []; + + const linkIds = LinkTeacherData; + + for (let imageKey of teachers) { + const id = linkIds.find((e) => e.imageKey == imageKey)?.teacherId; + + teacherData.push({ + id, + imageKey, + imagePath: `assets/teachers/${imageKey}.png`, + }); + } + + this.SetData(teacherData); + } + /** Get teachers data */ public GetData(): ITeacher[] { const data = super.GetData(); diff --git a/source/views/loginView.ts b/source/views/loginView.ts new file mode 100644 index 0000000..091c915 --- /dev/null +++ b/source/views/loginView.ts @@ -0,0 +1,39 @@ +import { HTMLView } from "./htmlView"; +import { LoginComponent } from "../components/loginComponent"; +import { authenticationService } from "../services/authenticationService"; +import { createPopup } from "../elements/popup"; + +export class LoginView extends HTMLView { + private popup: HTMLElement; + private loginComponent: LoginComponent; + private isOpen: boolean = false; + + constructor() { + super(); + + this.loginComponent = new LoginComponent(); + this.loginComponent.view.classList.add("clickable"); + + // Create the setup popup + this.popup = createPopup(); + this.popup.appendChild(this.loginComponent.view); + + // Open the login popup if user is not logged in + this.openSetupIfNotLoggedIn(); + } + + private async openSetupIfNotLoggedIn() { + const isLoggedIn = await authenticationService.isLoggedIn(); + if (!isLoggedIn && !this.isOpen) this.openLogin(); + } + + private openLogin() { + this.Add(this.popup, false); + this.isOpen = true; + } + + private closeLogin() { + this.Remove(this.popup); + this.isOpen = false; + } +} diff --git a/source/views/schoolView.ts b/source/views/schoolView.ts index f779b2c..b6f251e 100644 --- a/source/views/schoolView.ts +++ b/source/views/schoolView.ts @@ -7,7 +7,7 @@ import { School } from "../objects/school"; import { View } from "./view"; import { Teacher } from "../objects/teacher"; import { teacherStore } from "../store/teacherStore"; -import { SintLucasSchoolData } from "../schoolData"; +import { SintLucasSchoolData } from "../constants/schoolData"; export class SchoolView extends View { private school: School; @@ -69,10 +69,11 @@ export class SchoolView extends View { this.addChild(sprite); this.teachers.push(sprite); - if (teacher.firstName.charAt(0) == "B") { - this.SetTeacherIntoRoom(sprite, "N.0.60"); + // Testing or it works + if (teacher.imageKey.charAt(0) == "b") { + this.SetTeacherIntoRoom(sprite, "N.3.31"); } else { - this.SetTeacherIntoRoom(sprite, "N.0.74"); + this.SetTeacherIntoRoom(sprite, "N.0.60"); } } } diff --git a/source/views/setupView.ts b/source/views/setupView.ts index c730657..1164a7f 100644 --- a/source/views/setupView.ts +++ b/source/views/setupView.ts @@ -1,34 +1,17 @@ import { HTMLView } from "./htmlView"; -import { LoginComponent } from "../components/loginComponent"; import { createButton } from "../elements/button"; import { createPopup } from "../elements/popup"; import { createLabel } from "../elements/label"; -import { UploadDataComponent } from "../components/uploadDataComponent"; import { createImage } from "../elements/image"; export class SetupView extends HTMLView { - private loginComponent: LoginComponent; - private uploadDataComponent: UploadDataComponent; - private popup: HTMLElement; - private setupButton: HTMLButtonElement; private isOpen: boolean = false; constructor() { super(); - - this.loginComponent = new LoginComponent(); - this.loginComponent.view.classList.add("clickable"); - - const spaceLine = document.createElement("div"); - spaceLine.id = "spaceLine"; - spaceLine.appendChild(createLabel("or")); - - this.uploadDataComponent = new UploadDataComponent(); - this.uploadDataComponent.view.classList.add("clickable"); - // Create the setup button this.setupButton = createButton(); this.setupButton.id = "setupButton"; @@ -40,17 +23,8 @@ export class SetupView extends HTMLView { // Create the setup popup this.popup = createPopup(); - this.popup.appendChild(createLabel("Setup data")); - - const setupData = document.createElement("div"); - setupData.style.display = "flex"; - - setupData.appendChild(this.loginComponent.view); - setupData.appendChild(spaceLine); - setupData.appendChild(this.uploadDataComponent.view); - - this.popup.appendChild(setupData); + //this.popup.appendChild(this.loginComponent.view); this.Add(this.setupButton); }