├── .github
└── CODEOWNERS
├── .gitignore
├── LICENSE
├── README.md
├── package.json
├── public
├── asset-manifest.json
├── assets
│ ├── SCREEN.png
│ └── announce.png
├── icons
│ ├── favicon.ico
│ ├── logo_128x128.png
│ ├── logo_16x16.png
│ └── logo_48x48.png
├── index.html
├── inject.css
├── inject.js
├── logo192.png
├── logo512.png
├── manifest.json
└── scripts
│ ├── background.js
│ ├── classes
│ ├── ApiData.js
│ ├── EpitechData.js
│ └── XPHubApi.js
│ ├── data
│ └── epitech_data.js
│ ├── index.js
│ ├── retrieveData
│ ├── ModuleHandler.js
│ ├── TEpitech.js
│ ├── XPHub.js
│ ├── retrieveData.js
│ ├── roadBlock.js
│ └── timeline.js
│ └── utils
│ ├── crypto.js
│ ├── updateFrontend.js
│ └── webStorage.js
└── src
├── App.css
├── App.js
├── App.test.js
├── Layout.js
├── Pages
├── Dashboard.js
├── Hub.js
├── Roadblock.js
├── TEPitech.css
├── TEPitech.js
└── TimelineBox.js
├── components
├── AboutModal.js
├── AnnouncementHandler.js
├── ChartComponent.js
├── CustomToolTip.js
├── HubCard.js
├── IconButton.js
├── RoadBlockCard.js
├── RowGenerator.js
├── Sidebar.js
└── SummaryCard.js
├── contexts
└── DataContext.js
├── index.css
├── index.js
├── logo.svg
├── reportWebVitals.js
├── setupTests.js
├── styles.js
└── utils
└── listeners.js
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @RedBoardDev @OoscarFrank
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | package-lock.json
2 | node_modules/
3 | build/
4 | *.zip
5 | **/SCREEN.png
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 RedBoard
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
EpitechIntranetStatistics
4 |
Enhance your Epitech Intranet experience with advanced statistics and visualization.
5 |
6 |
7 |
8 |
9 |
10 |
11 | ---
12 |
13 | ## ⚠️ Disclaimer
14 |
15 | The extension is currently out of date, as there is no reliable way to retrieve the information contained in [this file](https://github.com/RedBoardDev/EpitechIntranetStatistics/blob/main/public/scripts/data/epitech_data.js). I remain highly motivated to revive the project once a viable solution is found. If you have any leads or suggestions, please open an issue.
16 |
17 | ---
18 |
19 | ## 📍 Overview
20 |
21 | EpitechIntranetStatistics is a web application providing statistics and information related to the Epitech Intranet.
22 | It offers functionalities for updating user data, retrieving API information, managing XP points, and displaying charts.
23 | The project aims to enhance the user experience by providing customizable information in an interactive dashboard, facilitating efficient tracking of progress and engagement.
24 |
25 | ## 📥 Download
26 |
27 | [Download from the Chrome Web Store](https://chrome.google.com/webstore/detail/fhelhbblcnpdfkiefkanbjjpkpejgodj)
28 |
29 | ---
30 |
31 | ### 🔧 Installation
32 |
33 | 1. Clone the EpitechIntranetStatistics repository:
34 |
35 | ```sh
36 | git clone https://github.com/RedBoardDev/EpitechIntranetStatistics
37 | ```
38 |
39 | 2. Change to the project directory:
40 |
41 | ```sh
42 | cd EpitechIntranetStatistics
43 | ```
44 |
45 | 3. Install the dependencies:
46 |
47 | ```sh
48 | npm install
49 | ```
50 |
51 | 4. Running EpitechIntranetStatistics
52 |
53 | ```sh
54 | npm start
55 | ```
56 |
57 | ---
58 |
59 | ## 🚧 Want to fix a module code ?
60 |
61 | - Create a Pull Request modifying the file `public/scripts/data/sortedModules.json` with the correct key and value
62 |
63 | ## 🤝 Contributing
64 |
65 | Contributions are always welcome! Please follow these steps:
66 |
67 | 1. Fork the project repository. This creates a copy of the project on your account that you can modify without affecting the original project.
68 | 2. Clone the forked repository to your local machine using a Git client like Git or GitHub Desktop.
69 | 3. Create a new branch with a descriptive name (e.g., `new-feature-branch` or `bugfix-issue-123`).
70 |
71 | ```sh
72 | git checkout -b new-feature-branch
73 | ```
74 |
75 | 4. Make changes to the project's codebase.
76 | 5. Commit your changes to your local branch with a clear commit message that explains the changes you've made.
77 |
78 | ```sh
79 | git commit -m 'Implemented new feature.'
80 | ```
81 |
82 | 6. Push your changes to your forked repository on GitHub using the following command
83 |
84 | ```sh
85 | git push origin new-feature-branch
86 | ```
87 |
88 | 7. Create a new pull request to the original project repository. In the pull request, describe the changes you've made and why they're necessary.
89 | The project maintainers will review your changes and provide feedback or merge them into the main branch.
90 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "EpitechIntranetStatistics",
3 | "version": "1.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@emotion/react": "^11.11.3",
7 | "@emotion/styled": "^11.11.0",
8 | "@mui/icons-material": "^5.11.16",
9 | "@mui/material": "^5.15.6",
10 | "@testing-library/jest-dom": "^5.16.5",
11 | "@testing-library/react": "^13.4.0",
12 | "@testing-library/user-event": "^13.5.0",
13 | "html-react-parser": "^3.0.16",
14 | "interactjs": "^1.10.17",
15 | "moment": "^2.29.4",
16 | "react": "^18.2.0",
17 | "react-apexcharts": "^1.4.1",
18 | "react-calendar-timeline": "^0.28.0",
19 | "react-dom": "^18.2.0",
20 | "react-router-dom": "^6.11.2",
21 | "react-scripts": "5.0.1",
22 | "recharts": "^2.6.2",
23 | "web-vitals": "^2.1.4"
24 | },
25 | "scripts": {
26 | "start": "react-scripts start",
27 | "build": "react-scripts build",
28 | "test": "react-scripts test",
29 | "eject": "react-scripts eject"
30 | },
31 | "eslintConfig": {
32 | "extends": [
33 | "react-app",
34 | "react-app/jest"
35 | ]
36 | },
37 | "browserslist": {
38 | "production": [
39 | ">0.2%",
40 | "not dead",
41 | "not op_mini all"
42 | ],
43 | "development": [
44 | "last 1 chrome version",
45 | "last 1 firefox version",
46 | "last 1 safari version"
47 | ]
48 | },
49 | "devDependencies": {
50 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/public/asset-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": {
3 | "main.js": "/static/js/main.12345678.js",
4 | "main.css": "/static/css/main.12345678.css",
5 | "logo.svg": "/static/media/logo.12345678.svg"
6 | },
7 | "entrypoints": [
8 | "static/js/main.12345678.js"
9 | ],
10 | "mapFiles": {}
11 | }
12 |
--------------------------------------------------------------------------------
/public/assets/SCREEN.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RedBoardDev/EpitechIntranetStatistics/c0cc78e7e699747b2c760c7a8c0d8e12605c92e9/public/assets/SCREEN.png
--------------------------------------------------------------------------------
/public/assets/announce.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RedBoardDev/EpitechIntranetStatistics/c0cc78e7e699747b2c760c7a8c0d8e12605c92e9/public/assets/announce.png
--------------------------------------------------------------------------------
/public/icons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RedBoardDev/EpitechIntranetStatistics/c0cc78e7e699747b2c760c7a8c0d8e12605c92e9/public/icons/favicon.ico
--------------------------------------------------------------------------------
/public/icons/logo_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RedBoardDev/EpitechIntranetStatistics/c0cc78e7e699747b2c760c7a8c0d8e12605c92e9/public/icons/logo_128x128.png
--------------------------------------------------------------------------------
/public/icons/logo_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RedBoardDev/EpitechIntranetStatistics/c0cc78e7e699747b2c760c7a8c0d8e12605c92e9/public/icons/logo_16x16.png
--------------------------------------------------------------------------------
/public/icons/logo_48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RedBoardDev/EpitechIntranetStatistics/c0cc78e7e699747b2c760c7a8c0d8e12605c92e9/public/icons/logo_48x48.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | You need to enable JavaScript to run this app.
8 |
9 |
--------------------------------------------------------------------------------
/public/inject.css:
--------------------------------------------------------------------------------
1 | .popup-extension {
2 | z-index: 43000;
3 | width: 100%;
4 | height: 100%;
5 | position: fixed;
6 | top: 0;
7 | left: 0;
8 | border: none;
9 | box-shadow: none;
10 | background-color: none;
11 | font-family: sans-serif;
12 | }
13 |
14 | .button-stats {
15 | background-image: url("https://i.imgur.com/9Aqd0j4.png");
16 | background-repeat: no-repeat;
17 | background-size: 64%;
18 | background-position: center;
19 | }
20 |
21 | .button-stats:hover {
22 | background-image: url("https://i.imgur.com/QPXs7TI.png");
23 | }
24 |
25 | .button-stats:active {
26 | background-image: url("https://i.imgur.com/QPXs7TI.png");
27 | }
--------------------------------------------------------------------------------
/public/inject.js:
--------------------------------------------------------------------------------
1 | function createButton(document, type, className, text) {
2 | var button = document.createElement(type);
3 | button.className = className;
4 |
5 | var label = document.createElement("span");
6 | label.classList.add("label");
7 | label.innerText = text;
8 |
9 | button.appendChild(label);
10 |
11 | return button;
12 | }
13 |
14 | const insertAfter = (newNode, referenceNode) => {
15 | if (referenceNode) {
16 | referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
17 | }
18 | };
19 |
20 | function getElementByXpath(path) {
21 | return document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
22 | }
23 |
24 | /* global chrome */
25 | function createPopup() {
26 | var iframe = document.createElement("iframe");
27 | iframe.className = "popup-extension";
28 | iframe.src = chrome.runtime.getURL("index.html");
29 | document.body.appendChild(iframe);
30 |
31 | document.addEventListener('click', (event) => {
32 | if (iframe && iframe.contains(event.target)) {
33 | iframe.style.display = 'none';
34 | if (document.body.contains(iframe)) {
35 | document.body.removeChild(iframe);
36 | }
37 | }
38 | });
39 |
40 | window.addEventListener('message', function (event) {
41 | if (event.data.type === 'outsideClick') {
42 | iframe.style.display = 'none';
43 |
44 | if (document.body.contains(iframe)) {
45 | document.body.removeChild(iframe);
46 | }
47 | }
48 | });
49 | }
50 |
51 | function addButtonToPage() {
52 | const neartag = getElementByXpath('//*[@id="header"]/div[2]/div/div[6]/table/tbody/tr/td[4]');
53 | let newButton = createButton(document, "a", "button", " Statistics");
54 | newButton.title = " Statistics";
55 | var td = document.createElement("td");
56 | td.className = "button-stats";
57 |
58 | td.classList.add("Statistics");
59 | td.appendChild(newButton);
60 | insertAfter(td, neartag);
61 | newButton.addEventListener('click', () => {
62 | setTimeout(() => {
63 | createPopup();
64 | }, 0);
65 | });
66 | }
67 |
68 | window.addEventListener('load', () => {
69 | const navbar = document.querySelector("#sidebar");
70 | if (navbar) {
71 | navbar.style.zIndex = "950";
72 | }
73 | addButtonToPage();
74 | });
75 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RedBoardDev/EpitechIntranetStatistics/c0cc78e7e699747b2c760c7a8c0d8e12605c92e9/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RedBoardDev/EpitechIntranetStatistics/c0cc78e7e699747b2c760c7a8c0d8e12605c92e9/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "name": "Epitech Intranet Statistics",
4 | "version": "1.1.0",
5 | "description": "Graphical interface for Epitech intranet with more statistics",
6 | "icons": {
7 | "16": "icons/logo_16x16.png",
8 | "48": "icons/logo_48x48.png",
9 | "128": "icons/logo_128x128.png"
10 | },
11 | "permissions": [
12 | "tabs",
13 | "cookies",
14 | "activeTab",
15 | "storage"
16 | ],
17 | "host_permissions": [
18 | "https://intra.epitech.eu/*",
19 | "https://*.microsoftonline.com/*",
20 | "https://*.live.com/*"
21 | ],
22 | "background": {
23 | "service_worker": "scripts/background.js"
24 | },
25 | "content_scripts": [
26 | {
27 | "matches": [
28 | "https://intra.epitech.eu/*"
29 | ],
30 | "js": [
31 | "inject.js"
32 | ],
33 | "css": [
34 | "inject.css"
35 | ]
36 | }
37 | ],
38 | "web_accessible_resources": [
39 | {
40 | "resources": [
41 | "inject.js",
42 | "index.html"
43 | ],
44 | "matches": [
45 | "https://intra.epitech.eu/*"
46 | ]
47 | }
48 | ]
49 | }
--------------------------------------------------------------------------------
/public/scripts/background.js:
--------------------------------------------------------------------------------
1 | /* global chrome */
2 |
3 | function getCookiesForEpitech() {
4 | return new Promise((resolve, reject) => {
5 | chrome.cookies.getAll({}, (cookies) => {
6 | const epitechCookies = cookies.filter((cookie) => {
7 | return cookie.domain === "intra.epitech.eu" && cookie.name === "user";
8 | });
9 |
10 | if (epitechCookies && epitechCookies.length > 0) {
11 | resolve(epitechCookies);
12 | } else {
13 | reject(new Error("No Epitech user cookie found."));
14 | }
15 | });
16 | });
17 | }
18 |
19 | function handleGetToken(sendResponse) {
20 | getCookiesForEpitech()
21 | .then((cookies) => {
22 | const data = {
23 | refresh_token: cookies[0]['value'],
24 | status: true
25 | };
26 | sendResponse(data);
27 | })
28 | .catch((error) => {
29 | const data = {
30 | status: false,
31 | error: error.message
32 | };
33 | sendResponse(data);
34 | });
35 | return true;
36 | }
37 |
38 |
39 | chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
40 | switch (request.command) {
41 | case "GET_TOKEN":
42 | return handleGetToken(sendResponse);
43 | default:
44 | return false;
45 | }
46 | });
--------------------------------------------------------------------------------
/public/scripts/classes/ApiData.js:
--------------------------------------------------------------------------------
1 | import { generateIdFromStr } from '../utils/crypto.js';
2 | import { storeData, getData } from '../utils/webStorage.js';
3 | class ApiData {
4 | #scolarYear;
5 | #location;
6 | #studentYear;
7 |
8 | #preLoad_generalUserData;
9 | #preLoad_generalNotesData;
10 | #preLoad_generalCourseData;
11 |
12 | constructor() {
13 | this.#scolarYear = undefined;
14 | this.#location = undefined;
15 | this.#studentYear = undefined;
16 |
17 | this.#preLoad_generalUserData = {};
18 | this.#preLoad_generalNotesData = {};
19 | this.#preLoad_generalCourseData = {};
20 | }
21 |
22 | async init(refreshToken, email) {
23 | if (!refreshToken || !email) return undefined;
24 | storeData('refresh_token', refreshToken);
25 | storeData('user_email', email);
26 |
27 | const userData = await this.#callApi('GET', `user/${email}?format=json`)
28 | this.#setScolarYear(userData['scolaryear'])
29 | this.#setUserLocation(userData['location']);
30 | this.#preLoad_generalUserData = userData;
31 | this.#studentYear = userData['studentyear'];
32 |
33 | const notesData = await this.#callApi('GET', `user/${email}/notes?format=json`);
34 | this.#preLoad_generalNotesData = notesData;
35 |
36 | const dataCursus = await this.#callApi('GET', `course/filter?format=json`);
37 | const filteredDataCursus = dataCursus
38 | // voir si c'est pas active_promo qu'il faut garder au lieu de check scolarYear
39 | .filter(item => item.id && Number(item.scolaryear) === Number(this.getScolarYear()))
40 | .map(item => {
41 | return {
42 | id: (item.id ?? undefined),
43 | semester: (item.semester ?? undefined),
44 | num: (item.num ?? undefined),
45 | begin: (item.begin ?? undefined),
46 | end: (item.end ?? undefined),
47 | end_register: (item.end_register ?? undefined),
48 | scolaryear: (item.scolaryear ?? undefined),
49 | code: (item.code ?? undefined),
50 | codeinstance: (item.codeinstance ?? undefined),
51 | location_title: (item.location_title ?? undefined),
52 | instance_location: (item.instance_location ?? undefined),
53 | flags: (item.flags ?? undefined),
54 | credits: (item.credits ?? undefined),
55 | status: (item.status ?? undefined),
56 | active_promo: (item.active_promo ?? undefined),
57 | open: (item.open ?? undefined),
58 | title: (item.title ?? undefined),
59 | complete_data: undefined,
60 | for_timeline: 0
61 | };
62 | });
63 | this.#preLoad_generalCourseData = filteredDataCursus;
64 | }
65 |
66 | // getter / setter function
67 |
68 | getStudentYear() {
69 | return this.#studentYear;
70 | }
71 |
72 | #getUserToken() {
73 | return getData('refresh_token');
74 | }
75 |
76 | getUserEmail() {
77 | return getData('user_email');
78 | }
79 |
80 | getScolarYear() {
81 | return this.#scolarYear.toString();
82 | }
83 |
84 | #setScolarYear(scolarYear) {
85 | this.#scolarYear = scolarYear;
86 | }
87 |
88 | getUserLocation() {
89 | return this.#location.toString().split('/');
90 | }
91 |
92 | #setUserLocation(location) {
93 | this.#location = location;
94 | }
95 |
96 | getGeneralUserData() {
97 | return this.#preLoad_generalUserData;
98 | }
99 |
100 | getGeneralNotesData() {
101 | return this.#preLoad_generalNotesData;
102 | }
103 |
104 | // general_course - getter / setter function
105 | getGeneralCourseData() {
106 | const courseData = this.#preLoad_generalCourseData;
107 |
108 | if (!courseData) return null;
109 | return courseData;
110 | }
111 | getNodeOnCourseCompleteData(criteria) {
112 | const courseData = this.#preLoad_generalCourseData;
113 | if (!courseData) return null;
114 |
115 | // voir la ligne d'en dessous
116 | // return courseData.find(item => Object.keys(criteria).every(key => item[key] === criteria[key]))?.complete_data;
117 | for (let item of courseData) {
118 | if (Object.keys(criteria).every(key => item[key] === criteria[key])) {
119 | return item.complete_data;
120 | }
121 | }
122 | return null;
123 | }
124 |
125 | getNodeOnCourseData(criteria) {
126 | const courseData = this.#preLoad_generalCourseData;
127 |
128 | if (!courseData) return null;
129 | return courseData.filter(item => item.code === criteria.code && item.semester === criteria.semester);
130 | }
131 |
132 | setNodeCourseCompleteData(criteria, newCompleteData) {
133 | const courseData = this.#preLoad_generalCourseData;
134 | if (!courseData) return;
135 |
136 | // courseData.find(item => Object.keys(criteria).every(key => item[key] === criteria[key])).complete_data = newCompleteData;
137 | for (let item of courseData) {
138 | if (Object.keys(criteria).every(key => item[key] === criteria[key])) {
139 | item.complete_data = newCompleteData;
140 | }
141 | }
142 | this.#preLoad_generalCourseData = courseData;
143 | }
144 |
145 | // // general API function
146 |
147 | async getCurrent() {
148 | const general = await this.#callApi('GET', `?format=json`);
149 | if (!general) return null;
150 | const current = general['current'] ?? null;
151 | return current;
152 | }
153 |
154 | async getCompleteDataFromApi(codeModule, codeInstance) {
155 | return await this.#callApi('GET', `module/${this.getScolarYear()}/${codeModule}/${codeInstance}/?format=json`);
156 | }
157 |
158 | // // preload data function
159 |
160 | async #callApi(method, endpoint) {
161 | if (endpoint === undefined || endpoint === null)
162 | return null;
163 | var config = {
164 | method: method,
165 | headers: {
166 | Cookies: this.#getUserToken()
167 | }
168 | };
169 | const apiBase = 'https://intra.epitech.eu/';
170 | var request = new Request(apiBase + endpoint, config);
171 |
172 | try {
173 | const response = await fetch(request, config);
174 | return response.json();
175 | } catch (error) {
176 | return null;
177 | }
178 | }
179 |
180 | async sendTracking() {
181 | if (!this.getUserEmail()) return;
182 | const storedTrackingId = getData('tracker_id')
183 |
184 | let encryptedEmail;
185 | if (storedTrackingId) {
186 | encryptedEmail = storedTrackingId;
187 | } else {
188 | encryptedEmail = await generateIdFromStr(this.getUserEmail());
189 | storeData('tracker_id', encryptedEmail);
190 | }
191 | var config = {
192 | method: 'POST',
193 | headers: {
194 | 'Content-Type': 'application/json',
195 | },
196 | body: JSON.stringify({
197 | "userId": encryptedEmail,
198 | })
199 | };
200 |
201 | try {
202 | var request = new Request('https://tracker.thomasott.fr/api/track', config);
203 | const response = await fetch(request);
204 | return (!response.ok && response.status !== 429);
205 | } catch (error) {
206 | return 1;
207 | }
208 | }
209 | }
210 |
211 | export { ApiData };
212 |
--------------------------------------------------------------------------------
/public/scripts/classes/EpitechData.js:
--------------------------------------------------------------------------------
1 | import { epitechData } from '../data/epitech_data.js';
2 |
3 | class EpitechData {
4 | #data;
5 |
6 | constructor() {
7 | this.#data = epitechData;
8 | }
9 |
10 | getUpdateDate() {
11 | return this.#data?.update_date ?? '1942-04-02';
12 | }
13 |
14 | getRoadblocksNames() {
15 | return this.#data?.roadblocks_name ?? null;
16 | }
17 |
18 | getRoadblocksNameByType(type) {
19 | return this.#data?.roadblocks_name?.[type] ?? null;
20 | }
21 |
22 | getRoadblocksRequirements(schoolyear) {
23 | const yearKey = `pge${schoolyear}`;
24 | return this.#data?.promo_requirements?.[yearKey]?.roadblocks ?? null;
25 | }
26 |
27 | getRoadblocksRequirementsByType(schoolyear, type) {
28 | const yearKey = `pge${schoolyear}`;
29 | return this.#data?.promo_requirements?.[yearKey]?.roadblocks?.[type] ?? null;
30 | }
31 |
32 | getUnits(schoolyear) {
33 | const yearKey = `pge${schoolyear}`;
34 | return this.#data?.promo_requirements?.[yearKey]?.units ?? null;
35 | }
36 |
37 | getUnitsByType(schoolyear, type) {
38 | const yearKey = `pge${schoolyear}`;
39 | return this.#data?.promo_requirements?.[yearKey]?.units?.[type] ?? null;
40 | }
41 |
42 | getCreditsRequirements(schoolyear) {
43 | const yearKey = `pge${schoolyear}`;
44 | return this.#data?.promo_requirements?.[yearKey]?.roadblocks?.total_credits ?? 0;
45 | }
46 |
47 | getHubUnit() {
48 | return this.#data?.hub?.unit ?? null;
49 | }
50 |
51 | getHubMaxCredits(schoolyear) {
52 | const yearKey = `pge${schoolyear}`;
53 | return this.#data?.hub?.max_credits?.[yearKey] ?? 0;
54 | }
55 |
56 | getHubActivities() {
57 | return this.#data?.hub?.activities ?? null;
58 | }
59 |
60 | getHubActivitiesByType(type) {
61 | const activities = this.#data?.hub?.activities;
62 |
63 | if (!activities) return null;
64 | const matchingActivity = activities.find(activity => {
65 | return activity.name === type || (activity.alias && activity.alias.includes(type));
66 | });
67 |
68 | return matchingActivity || null;
69 | }
70 | }
71 |
72 | export { EpitechData };
73 |
--------------------------------------------------------------------------------
/public/scripts/classes/XPHubApi.js:
--------------------------------------------------------------------------------
1 | class XPHub {
2 | #xpAct;
3 | #me;
4 |
5 | constructor(activityData) {
6 | this.#me = { nbXps: 0, nbXpsSoon: 0, nbXpsLost: 0, activList: [] };
7 | this.#xpAct = activityData.map(activity => {
8 | return {
9 | ...activity,
10 | nbPart: 0,
11 | nbOrg: 0,
12 | nbXPTotal: 0,
13 | };
14 | });
15 | }
16 |
17 | dateIsPassed = (targetDate) => {
18 | const actualDate = new Date();
19 | return targetDate < actualDate;
20 | };
21 |
22 | addProject = (everyNotes, codeacti, date_begin, date_end) => {
23 | const projects = everyNotes.filter((note) => note.codeacti === codeacti);
24 |
25 | projects.forEach((project) => {
26 | const beginDate = new Date(date_begin);
27 | const endDate = new Date(date_end);
28 |
29 | const dateDifference = ((endDate.getTime() - beginDate.getTime()) / (1000 * 60 * 60 * 24) + 1);
30 |
31 | const findAct = this.#xpAct.find((act) => act.name === 'Project' || act.alias.includes('Project'));
32 | findAct.nbPart += 1;
33 |
34 | if (this.dateIsPassed(endDate)) {
35 | const XP = (dateDifference * 2) * project.final_note / 100;
36 | this.#me.nbXps += XP;
37 | findAct.nbXPTotal += XP;
38 | this.#me.activList.push({ title: project.title, type: "Project", status: 'present', date: date_begin, XPEarn: XP });
39 | } else {
40 | const XP = (dateDifference * 2);
41 | this.#me.nbXpsSoon += XP;
42 | this.#me.activList.push({ title: project.title, type: "Project", status: 'present', date: date_begin, XPEarn: 0 });
43 | }
44 | });
45 | };
46 |
47 | addActivite = (title, type, status, date) => {
48 | const findAct = this.#xpAct.find((act) => act.name === type || act.alias.includes(type));
49 | if (!findAct) return;
50 | const { limitPart, xpWinPart, xpWinOrg, nbPart, xpLostPart, nbOrg, limitOrg } = findAct;
51 |
52 | switch (status) {
53 | case 'present':
54 | if (limitPart === -1 || nbPart < limitPart) {
55 | this.#me.nbXps += xpWinPart;
56 | findAct.nbXPTotal += xpWinPart;
57 | findAct.nbPart += 1
58 | }
59 | this.#me.activList.push({ title, type, status: 'present', date, XPEarn: xpWinPart });
60 | break;
61 | case 'absent':
62 | this.#me.nbXps -= xpLostPart;
63 | this.#me.nbXpsLost -= xpLostPart;
64 | findAct.nbXPTotal -= xpLostPart;
65 | this.#me.activList.push({ title, type, status: 'absent', date, XPEarn: xpLostPart * (-1) });
66 | break;
67 | case 'organisateur':
68 | if (limitOrg === -1 || nbOrg < limitOrg) {
69 | this.#me.nbXps += xpWinOrg;
70 | findAct.nbXPTotal += xpWinOrg;
71 | findAct.nbOrg += 1;
72 | }
73 | this.#me.activList.push({ title, type, status: 'organisateur', date, XPEarn: xpWinOrg });
74 | break;
75 | case 'soon':
76 | this.#me.activList.push({ title, type, status: 'inscrit', date, XPEarn: 0 });
77 | break;
78 | default:
79 | break;
80 | }
81 | };
82 |
83 | countXpSoon = () => {
84 | const registerActivity = (this.#me.activList).filter((act) => act.status === 'inscrit');
85 | registerActivity.map((act) => {
86 | const findAct = this.#xpAct.find((elem) => elem.name === act.type || elem.alias.includes(act.type));
87 | const { xpWinPart, limitPart, nbPart } = findAct;
88 | (limitPart === -1 || nbPart <= limitPart) && (this.#me.nbXpsSoon += xpWinPart) && findAct.nbPart++;
89 | });
90 | };
91 |
92 | getMeVariable = () => {
93 | return this.#me;
94 | }
95 |
96 | getnbXps = () => {
97 | return this.#me.nbXps;
98 | }
99 |
100 | getxpAct = () => {
101 | return this.#xpAct;
102 | }
103 | }
104 |
105 | export { XPHub };
106 |
--------------------------------------------------------------------------------
/public/scripts/data/epitech_data.js:
--------------------------------------------------------------------------------
1 | const epitechData = {
2 | "update_date": "2024-02-05",
3 | "roadblocks_name": {
4 | "professional_writings": "Professional Communication",
5 | "technical_foundation": "Technical Foundation",
6 | "technical_supplement": "Technical Supplement",
7 | "innovation": "Innovation & Professionalization",
8 | "softskills": "Soft Skills"
9 | },
10 | "promo_requirements": {
11 | "pge1": {
12 | "units": {
13 | "professional_writings": ["B-PRO-100", "B-PRO-200"],
14 | "technical_foundation": ["B-CPE-100", "B-CPE-101", "B-CPE-110", "B-PSU-100", "B-CPE-200", "B-PSU-200"],
15 | "technical_supplement": ["B-MUL-100", "B-MAT-100", "B-MUL-200", "B-AIA-200", "B-NSA-100", "B-WEB-200", "B-MAT-200", "B-DOP-200", "B-SEC-200"],
16 | "innovation": ["G-JAM-001", "B-INN-200" ,"G-CUS-001" ,"G-CUS-002" ,"G-CUS-003" ,"G-CUS-004" ,"G-CUS-005" ,"G-CUS-006" ,"G-CUS-007" ,"G-CUS-008"],
17 | "softskills": ["B-PRO-100", "B-PRO-200", "B-PMP-100", "B-PMP-200", "B-PCP-000"],
18 | "solo_stumper": ["B-CPE-210"],
19 | "duo_stumper": ["B-CPE-210"]
20 | },
21 | "roadblocks": {
22 | "technical_foundation": 20,
23 | "technical_supplement": 8,
24 | "innovation": 3,
25 | "softskills": 3,
26 | "tepitech": 600,
27 | "solo_stumper": 15,
28 | "duo_stumper": 15,
29 | "professional_writings": 0,
30 | "total_credits": 60
31 | }
32 | },
33 | "pge2": {
34 | "units": {
35 | "professional_writings": ["B-PRO-100", "B-PRO-200", "B-PRO-400"],
36 | "technical_foundation": ["B-CCP-400", "B-NWP-400", "B-OOP-400", "B-PDG-300", "B-YEP-400"],
37 | "technical_supplement": ["B-CNA-410", "B-MAT-400", "B-ASM-400", "B-FUN-400", "B-DOP-400", "B-PSU-400", "B-SEC-400"],
38 | "innovation": ["G-JAM-001", "B-INN-400" ,"G-CUS-001" ,"G-CUS-002" ,"G-CUS-003" ,"G-CUS-004" ,"G-CUS-005" ,"G-CUS-006" ,"G-CUS-007" ,"G-CUS-008"],
39 | "softskills": ["B-PRO-400", "B-PCP-000", "B-PMP-400"]
40 | },
41 | "roadblocks": {
42 | "technical_foundation": 13,
43 | "technical_supplement": 6,
44 | "innovation": 4,
45 | "softskills": 3,
46 | "tepitech": 700,
47 | "professional_writings": 1,
48 | "total_credits": 120
49 | }
50 | },
51 | "pge3": {
52 | "units": {
53 | "professional_writings": ["B-PRO-100", "B-PRO-200", "B-PRO-400", "B-PRO-510"],
54 | "technical_foundation": ["B-CPP-500", "B-DEV-500", "B-FUN-500"],
55 | "technical_supplement": ["B-AIA-500", "B-DOP-500", "B-MAT-500", "B-SEC-500", "B-CNA-500"],
56 | "innovation": ["G-JAM-001", "B-INN-500" ,"G-CUS-001" ,"G-CUS-002" ,"G-CUS-003" ,"G-CUS-004" ,"G-CUS-005" ,"G-CUS-006" ,"G-CUS-007" ,"G-CUS-008", "B-PRO-500"],
57 | "softskills": ["B-DES-500", "B-SVR-500", "B-PRO-510", "B-PCP-000"]
58 | },
59 | "roadblocks": {
60 | "technical_foundation": 8,
61 | "technical_supplement": 6,
62 | "innovation": 6,
63 | "softskills": 4,
64 | "tepitech": 750,
65 | "professional_writings": 2,
66 | "total_credits": 180
67 | }
68 | }
69 | },
70 | "hub": {
71 | "unit": "B-INN-000",
72 | "max_credits": {
73 | "pge1": 5,
74 | "pge2": 8,
75 | "pge3": 8
76 | },
77 | "activities": [
78 | {
79 | "name": "Talk",
80 | "alias": [
81 | "Meetup"
82 | ],
83 | "xpWinPart": 1,
84 | "xpWinOrg": 4,
85 | "xpLostPart": 1,
86 | "limitPart": 15,
87 | "limitOrg": 6
88 | },
89 | {
90 | "name": "Workshop",
91 | "alias": [],
92 | "xpWinPart": 2,
93 | "xpWinOrg": 7,
94 | "xpLostPart": 2,
95 | "limitPart": 10,
96 | "limitOrg": 3
97 | },
98 | {
99 | "name": "Hackathon",
100 | "alias": [],
101 | "xpWinPart": 6,
102 | "xpWinOrg": 15,
103 | "xpLostPart": 6,
104 | "limitPart": -1,
105 | "limitOrg": -1
106 | },
107 | {
108 | "name": "Experience",
109 | "alias": [],
110 | "xpWinPart": 3,
111 | "xpWinOrg": 0,
112 | "xpLostPart": 0,
113 | "limitPart": 8,
114 | "limitOrg": 0
115 | },
116 | {
117 | "name": "Project",
118 | "alias": [],
119 | "xpWinPart": -1,
120 | "xpLostPart": -1,
121 | "limitPart": -1,
122 | "limitOrg": -1
123 | }
124 | ]
125 | }
126 | }
127 |
128 | export { epitechData };
129 |
--------------------------------------------------------------------------------
/public/scripts/index.js:
--------------------------------------------------------------------------------
1 | import { ApiData } from "./classes/ApiData.js";
2 | import { EpitechData } from "./classes/EpitechData.js";
3 | import { XPHub } from "./classes/XPHubApi.js";
4 | import { parseJwtToken } from "./utils/crypto.js";
5 | import { retrieveData } from "./retrieveData/retrieveData.js";
6 | import { getData } from "./utils/webStorage.js";
7 |
8 | /* global chrome */
9 |
10 | async function initApiData() {
11 | let refresh_token = getData('refresh_token');
12 |
13 | if (!refresh_token) {
14 | const data = await new Promise(resolve => {
15 | chrome.runtime.sendMessage({ command: 'GET_TOKEN' }, resolve);
16 | });
17 | if (!data || data['status'] !== true) {
18 | console.error(data['error']);
19 | return undefined;
20 | }
21 | refresh_token = data['refresh_token'];
22 | }
23 |
24 | const jwtToken = parseJwtToken(refresh_token);
25 | if (!jwtToken['login']) {
26 | console.error("Retrieve email failed.");
27 | return undefined;
28 | }
29 |
30 | const api = new ApiData();
31 | await api.init(refresh_token, jwtToken['login']);
32 | return api;
33 | }
34 |
35 | window.addEventListener('load', async () => {
36 | const epitechData = new EpitechData();
37 | const XPHubData = new XPHub(epitechData.getHubActivities());
38 | const apiData = await initApiData();
39 |
40 | if (!epitechData || !XPHubData || !apiData) return;
41 | apiData.sendTracking();
42 | await retrieveData(epitechData, XPHubData, apiData);
43 | });
44 |
--------------------------------------------------------------------------------
/public/scripts/retrieveData/ModuleHandler.js:
--------------------------------------------------------------------------------
1 | async function getModuleInformation(apiData, codeInstance, codeSemester) { //check si on bien inscrit au module + roadblock + projet sinon mettre en gris dans le front
2 | try {
3 | let nodeCompleteData = await apiData.getNodeOnCourseCompleteData({ code: `${codeInstance}`, semester: Number(codeSemester) });
4 | if (nodeCompleteData === null) {
5 | return null;
6 | }
7 | if (nodeCompleteData === undefined) {
8 | const nodeInfo = await apiData.getNodeOnCourseData({ code: `${codeInstance}`, semester: Number(codeSemester) });
9 | let nodeData;
10 | nodeData = nodeInfo.find((item) => item.status !== "notregistered") || nodeInfo[0];
11 | nodeCompleteData = await apiData.getCompleteDataFromApi(nodeData.code, nodeData.codeinstance);
12 | apiData.setNodeCourseCompleteData({ code: codeInstance, semester: codeSemester }, nodeCompleteData);
13 | }
14 | if (!nodeCompleteData.error) {
15 | let moduleInfo = {
16 | name: `${nodeCompleteData?.['title'] ?? null}`,
17 | codeInstance: codeInstance,
18 | user_credits: nodeCompleteData?.['user_credits'] ?? null,
19 | credits: nodeCompleteData?.['credits'] ?? null,
20 | student_registered: nodeCompleteData?.['student_registered'] ?? 0,
21 | student_grade: nodeCompleteData?.['student_grade'] ?? null,
22 | student_credits: nodeCompleteData?.['student_credits'] ?? 0,
23 | color: nodeCompleteData?.['color'] ?? null
24 | };
25 | return moduleInfo;
26 | }
27 | } catch (error) {
28 | return false;
29 | }
30 | return false;
31 | }
32 |
33 | export { getModuleInformation };
34 |
--------------------------------------------------------------------------------
/public/scripts/retrieveData/TEpitech.js:
--------------------------------------------------------------------------------
1 | function getFilteredTEPitech(dataApi, regex, notes) {
2 | return notes.filter(element => {
3 | return element.codemodule === 'B-ANG-058' && regex.test(element.title) && (element.scolaryear).toString() === dataApi.getScolarYear()
4 | });
5 | }
6 |
7 | function getHighestTEpitech(dataApi) {
8 | const generalNotesData = dataApi.getGeneralNotesData();
9 | var highestTEpitech = 0;
10 |
11 | const regex = /TEPitech(?!.*Self-assessment)/;
12 | const filteredTEPitech = getFilteredTEPitech(dataApi, regex, generalNotesData['notes']);
13 |
14 | const bestTEPitechElement = filteredTEPitech.find(element => element.title === "Best TEPitech score of the year");
15 | if (bestTEPitechElement) return bestTEPitechElement.final_note;
16 |
17 | filteredTEPitech.forEach(element => {
18 | highestTEpitech = (element.final_note > highestTEpitech ? element.final_note : highestTEpitech)
19 | });
20 | return highestTEpitech;
21 | }
22 |
23 | function extractSectionsAndScores(comment) {
24 | const regex = /([\w&]+(?: \w+)*):(\d+)\/(\d+)/g;
25 | const sectionsAndScores = {};
26 | let match;
27 |
28 | while ((match = regex.exec(comment)) !== null) {
29 | const section = match[1].trim().toLowerCase();
30 | const score = match[2] ? parseInt(match[2], 10) : null;
31 | const total = match[3] ? parseInt(match[3], 10) : null;
32 | sectionsAndScores[section] = { score, total };
33 | }
34 | return sectionsAndScores;
35 | }
36 |
37 | function getTEpitechStatistics(dataApi) {
38 | const generalNotesData = dataApi.getGeneralNotesData();
39 |
40 | const regex = /TEPitech/;
41 | const filteredTEPitech = getFilteredTEPitech(dataApi, regex, generalNotesData['notes']);
42 |
43 | return filteredTEPitech.map(node => {
44 | return {
45 | date: node.date ?? null,
46 | final_note: node.final_note ?? 0,
47 | sections: (extractSectionsAndScores(node.comment) ?? null),
48 | };
49 | });
50 | }
51 |
52 | export { getHighestTEpitech, getTEpitechStatistics };
53 |
--------------------------------------------------------------------------------
/public/scripts/retrieveData/XPHub.js:
--------------------------------------------------------------------------------
1 | import { updateFrontend } from "../utils/updateFrontend.js";
2 |
3 | export const getXPHubData = async (epitechData, apiData, XPHubData) => {
4 | const generalNotesData = await apiData.getGeneralNotesData();
5 | const hubUnit = epitechData.getHubUnit();
6 |
7 | const location = apiData.getUserLocation();
8 | const pays = location[0];
9 | const region = location[1];
10 |
11 | let activitiesPublic = await apiData.getNodeOnCourseCompleteData({ code: hubUnit, codeinstance: `${pays}-0-1` });
12 | let activitiesCampus = await apiData.getNodeOnCourseCompleteData({ code: hubUnit, codeinstance: `${region}-0-1` });
13 | if (activitiesPublic === undefined) {
14 | activitiesPublic = null;
15 | activitiesPublic = (await apiData.getCompleteDataFromApi(hubUnit, `${pays}-0-1`));
16 | apiData.setNodeCourseCompleteData({ code: hubUnit, codeinstance: `${pays}-0-1` }, activitiesPublic);
17 | }
18 | if (activitiesCampus === undefined) {
19 | activitiesCampus = null;
20 | activitiesCampus = (await apiData.getCompleteDataFromApi(hubUnit, `${region}-0-1`));
21 | apiData.setNodeCourseCompleteData({ code: hubUnit, codeinstance: `${region}-0-1` }, activitiesCampus);
22 | }
23 | const everyActivities = [
24 | ...(activitiesPublic?.activites || []),
25 | ...(activitiesCampus?.activites || [])
26 | ];
27 | everyActivities.map((activite) => {
28 | if (activite.type_title && activite.type_title === "Project") {
29 | // for optimisation, we can store each project and do addProject with every project to avoid multiple loops of generalNotesData
30 | XPHubData.addProject(generalNotesData['notes'], activite.codeacti, activite.begin, activite.end);
31 | } else {
32 | activite.events.map((event) => {
33 | if (event.user_status)
34 | XPHubData.addActivite(activite.title, activite.type_title, event.user_status, event.begin);
35 | else if (event.assistants.find((assistant) => assistant.login === apiData.getUserEmail()))
36 | XPHubData.addActivite(activite.title, activite.type_title, 'organisateur', event.begin);
37 | else if (event.already_register) XPHubData.addActivite(activite.title, activite.type_title, 'soon', event.begin);
38 | });
39 | }
40 | });
41 | XPHubData.countXpSoon();
42 | const XPHub_me = await XPHubData.getMeVariable();
43 | let XPHub_xpAct = await XPHubData.getxpAct();
44 | XPHub_xpAct.map((node) => {
45 | const types = node.alias;
46 | types.push(node.name);
47 |
48 | const activList = XPHub_me.activList;
49 | const activities = activList.filter((act) => types.includes(act.type));
50 | node['activities'] = activities;
51 | });
52 | const XPHub = {
53 | xp_completed: XPHub_me.nbXps,
54 | xp_in_progress: XPHub_me.nbXpsSoon,
55 | xp_lost: XPHub_me.nbXpsLost,
56 | activities_per_type: XPHub_xpAct,
57 | };
58 | updateFrontend('hub', XPHub);
59 | }
60 |
--------------------------------------------------------------------------------
/public/scripts/retrieveData/retrieveData.js:
--------------------------------------------------------------------------------
1 | import { getHighestTEpitech, getTEpitechStatistics } from "./TEpitech.js";
2 | import { updateFrontend } from "../utils/updateFrontend.js";
3 | import { updateRoadBlockInformation } from "./roadBlock.js";
4 | import { getXPHubData } from "./XPHub.js";
5 | import { updateTimelineChart } from "./timeline.js";
6 |
7 | function updateSideBarInformation(apiData) {
8 | const generalUserData = apiData.getGeneralUserData();
9 | if (!generalUserData) return;
10 |
11 | const data = {
12 | name: (generalUserData['title'] ?? ''),
13 | email: apiData.getUserEmail(),
14 | cursus: (generalUserData['course_code'] ?? ''),
15 | semester: (generalUserData['semester_code'] ?? ''),
16 | promo: 'Promotion ' + (generalUserData['promo'] ?? ''),
17 | profilPicture: ("https://intra.epitech.eu" + (generalUserData['picture'] ?? '')),
18 | city: (generalUserData['groups'][0]?.['title'] ?? ''),
19 | };
20 |
21 | updateFrontend('sidebar', data);
22 | }
23 |
24 | async function updateDashboardInformation(epitechData, apiData) {
25 | const generalUserData = apiData.getGeneralUserData();
26 | if (!generalUserData) return;
27 |
28 | const data = {
29 | credits: (generalUserData['credits'] ?? 0),
30 | GPA: (generalUserData['gpa'][0]?.['gpa'] ?? 0.00),
31 | highestTEpitech: (getHighestTEpitech(apiData)),
32 | goalTEpitech: epitechData.getRoadblocksRequirementsByType(apiData.getStudentYear(), 'tepitech'),
33 | };
34 | updateFrontend('dashboard', data);
35 | }
36 |
37 | async function updateTepitechInformation(apiData) {
38 | const generalUserData = apiData.getGeneralUserData();
39 | if (!generalUserData) return;
40 |
41 | updateFrontend('tepitechs', getTEpitechStatistics(apiData));
42 | }
43 |
44 | async function updateCreditsInformation(epitechData, apiData) {
45 | const studentYear = apiData.getStudentYear();
46 |
47 | const credits = apiData.getGeneralUserData()?.['credits'] ?? 0;
48 | const creditsRequirement = epitechData.getCreditsRequirements(studentYear) ?? 0;
49 | const current = await apiData.getCurrent();
50 |
51 | if (!current || current.length < 1) return;
52 | const availableCredits = current.reduce((sum, module) => {
53 | if (module.grade === '-')
54 | return sum + (parseInt(module.credits) || 0);
55 | return sum;
56 | }, 0) + epitechData.getHubMaxCredits(studentYear);
57 |
58 | const data = {
59 | credits: credits,
60 | neededCredits: creditsRequirement,
61 | availableCredits: availableCredits,
62 | status: ((credits >= creditsRequirement) ? 'requirement_met' : ((credits + availableCredits >= creditsRequirement) ? 'requirement_attainable' : 'credits_below_requirement')),
63 | };
64 | updateFrontend('credits_requirement', data);
65 | }
66 |
67 | async function fetchManifestJson() {
68 | const response = await fetch('/manifest.json');
69 | if (!response.ok) return 'null';
70 | return await response.json();
71 | }
72 |
73 | async function updateDevelopperInformation(epitechData) {
74 | const manifestJson = await fetchManifestJson();
75 | const data = {
76 | currentVersion: manifestJson.version,
77 | dataLastUpdate: epitechData.getUpdateDate(),
78 | };
79 | updateFrontend('developper', data);
80 | }
81 |
82 | export async function retrieveData(epitechData, XPHubData, apiData) {
83 | updateDevelopperInformation(epitechData);
84 | updateSideBarInformation(apiData);
85 | updateDashboardInformation(epitechData, apiData);
86 | updateTepitechInformation(apiData);
87 | await getXPHubData(epitechData, apiData, XPHubData);
88 | updateTimelineChart(apiData);
89 | updateRoadBlockInformation(epitechData, apiData, XPHubData);
90 | updateCreditsInformation(epitechData, apiData);
91 | }
92 |
--------------------------------------------------------------------------------
/public/scripts/retrieveData/roadBlock.js:
--------------------------------------------------------------------------------
1 | import { getModuleInformation } from "./ModuleHandler.js";
2 | import { updateFrontend } from "../utils/updateFrontend.js";
3 |
4 | function hubUnitInformation(epitechData, XPHubData, studentYear, moduleInfo) {
5 | let newModuleInfo = moduleInfo;
6 |
7 | newModuleInfo.credits = epitechData.getHubMaxCredits(studentYear);
8 | newModuleInfo.student_credits = Math.floor((XPHubData.getnbXps() / 10));
9 |
10 | if (newModuleInfo.user_credits > newModuleInfo.credits)
11 | newModuleInfo.user_credits = newModuleInfo.credits;
12 | if (newModuleInfo.user_credits >= newModuleInfo.credits)
13 | newModuleInfo.color = 'green';
14 |
15 | return newModuleInfo;
16 | }
17 |
18 | async function getUnitInformation(epitechData, apiData, XPHubData, studentYear, unitCode) {
19 | const semesterCodeMatch = unitCode.match(/-(\d)/);
20 | if (!semesterCodeMatch || !semesterCodeMatch[1]) return null;
21 |
22 | const moduleInfo = await getModuleInformation(apiData, unitCode, semesterCodeMatch[1]);
23 | if (!moduleInfo) return null;
24 |
25 | if (/B-INN-[0-9]00/.test(unitCode)) { // HUB UNIT
26 | return hubUnitInformation(epitechData, XPHubData, studentYear, moduleInfo);
27 | } else {
28 | return moduleInfo;
29 | }
30 | }
31 |
32 | function roadblockInformation(unitsData, rdKey, rdName, creditNeeded) {
33 | const roadblockData = {
34 | key: rdKey,
35 | name: rdName,
36 | modules: unitsData,
37 | total_roadblock_credits: unitsData.reduce((sum, module) => {
38 | return sum + (module.credits || 0);
39 | }, 0),
40 | available_credits: unitsData.reduce((sum, module) => {
41 | return sum + (parseInt(module.user_credits) || 0);
42 | }, 0),
43 | credit_needed: creditNeeded,
44 | actual_student_credits: unitsData.reduce((sum, module) => {
45 | return sum + (module.student_credits || 0);
46 | }, 0)
47 | };
48 | return roadblockData;
49 | }
50 |
51 | export async function updateRoadBlockInformation(epitechData, apiData, XPHubData) {
52 | const roadblocksNames = epitechData.getRoadblocksNames();
53 | const roadblocksKey = Object.keys(roadblocksNames);
54 | const studentYear = apiData.getStudentYear();
55 |
56 | let roadBlocksData = [];
57 |
58 | for (const rdKey of roadblocksKey) {
59 | const unitsCode = epitechData.getUnitsByType(studentYear, rdKey);
60 | let unitsData = [];
61 | for (const unitCode of unitsCode) {
62 | const unitData = await getUnitInformation(epitechData, apiData, XPHubData, studentYear, unitCode);
63 | if (!unitData) continue;
64 | unitsData.push(unitData);
65 | }
66 | const creditNeeded = epitechData.getRoadblocksRequirementsByType(studentYear, rdKey);
67 | const roadblockData = roadblockInformation(unitsData, rdKey, roadblocksNames[rdKey], creditNeeded);
68 | if (roadblockData.key !== 'professional_writings') // do not show professional_writings
69 | roadBlocksData.push(roadblockData);
70 | }
71 | updateFrontend("roadblocks", roadBlocksData);
72 | }
73 |
--------------------------------------------------------------------------------
/public/scripts/retrieveData/timeline.js:
--------------------------------------------------------------------------------
1 | import { updateFrontend } from "../utils/updateFrontend.js";
2 |
3 | export async function updateTimelineChart(apiData) {
4 | let timeLineData = {};
5 | const generalCourse = await apiData.getGeneralCourseData();
6 | const regexSkip = /^(B0|[A-Z]0)|.*Hub.*|.*Roadblock.*|.*Administrative.*|.*Internship.*|.*Hackathon.*/;
7 | for (let node of generalCourse) {
8 | if (node.status === "notregistered") {
9 | continue;
10 | }
11 | if (regexSkip.test(node.title)) {
12 | continue;
13 | }
14 | let nodeCompleteData;
15 | if (node.complete_data === undefined) {
16 | nodeCompleteData = await apiData.getCompleteDataFromApi(node.code, node.codeinstance);
17 | node.complete_data = nodeCompleteData;
18 | } else {
19 | nodeCompleteData = node.complete_data;
20 | }
21 | for (let activite of nodeCompleteData.activites) {
22 | if (activite.is_projet === true && activite.type_code === "proj" && (activite.type_title === "Mini-project" || activite.type_title === "Project")) {
23 | let nodeTitle = node.title.replace(/^[A-Za-z]\d+\s*-\s*/, '');
24 | if (!timeLineData[nodeTitle]) {
25 | timeLineData[nodeTitle] = [];
26 | }
27 | timeLineData[nodeTitle].push({
28 | title: activite.title,
29 | begin: activite.begin,
30 | end: activite.end,
31 | end_register: activite.end_register
32 | });
33 | }
34 | }
35 | }
36 | updateFrontend('timeline', timeLineData);
37 | };
--------------------------------------------------------------------------------
/public/scripts/utils/crypto.js:
--------------------------------------------------------------------------------
1 | async function sha256(str) {
2 | const encoder = new TextEncoder();
3 | const data = encoder.encode(str);
4 | return await crypto.subtle.digest('SHA-256', data).then(hashBuffer => {
5 | const hashArray = Array.from(new Uint8Array(hashBuffer));
6 | const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, '0')).join('');
7 | return hashHex;
8 | });
9 | }
10 |
11 | async function generateIdFromStr(email) {
12 | return await sha256(email).then(hashedEmail => {
13 | const generatedId = hashedEmail.substring(0, 32);
14 | return generatedId;
15 | });
16 | }
17 |
18 | function parseJwtToken(token) {
19 | if (token === undefined || token === null) return undefined;
20 | const base64Url = token.split('.')[1];
21 | const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
22 | const jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function (c) {
23 | return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
24 | }).join(''));
25 | return JSON.parse(jsonPayload);
26 | }
27 |
28 | export { generateIdFromStr, parseJwtToken };
29 |
--------------------------------------------------------------------------------
/public/scripts/utils/updateFrontend.js:
--------------------------------------------------------------------------------
1 | export const updateFrontend = async (eventName, data) => {
2 | // console.warn(`[XPHub] Updating frontend with event ${eventName} and data`, data);
3 | const event = new CustomEvent(eventName, { detail: data });
4 | window.dispatchEvent(event);
5 | }
6 |
--------------------------------------------------------------------------------
/public/scripts/utils/webStorage.js:
--------------------------------------------------------------------------------
1 | function storeData(key, value) {
2 | try {
3 | localStorage.setItem(key, value);
4 | return true;
5 | } catch (error) {
6 | console.error('Error while storing data:', error);
7 | return false;
8 | }
9 | }
10 |
11 | function getData(key) {
12 | try {
13 | const storedData = localStorage.getItem(key);
14 | return storedData;
15 | } catch (error) {
16 | console.error('Error while retrieving data:', error);
17 | return null;
18 | }
19 | }
20 |
21 | export { getData, storeData };
22 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | margin: 0;
3 | padding: 0;
4 | background-color: 'none';
5 | }
6 |
7 | body {
8 | margin: 0;
9 | }
10 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Box } from '@mui/material';
3 | import Layout from './Layout';
4 | import { COLORS, BOX_SHADOW, BORDER_RADIUS } from './styles.js';
5 | import Announcement from './components/AnnouncementHandler.js';
6 |
7 | const App = () => {
8 | function handleOverlayClick(event) {
9 | if (event.target === event.currentTarget) {
10 | window.parent.postMessage({ type: 'outsideClick' }, '*');
11 | }
12 | }
13 |
14 | const overlayStyle = {
15 | position: 'fixed',
16 | top: '0',
17 | left: '0',
18 | height: '100vh',
19 | width: '100vw',
20 | zIndex: '0',
21 | backgroundColor: 'rgba(0, 0, 0, 0.5)'
22 | };
23 |
24 | const appStyle = {
25 | height: '66vh',
26 | width: '66vw',
27 | position: 'fixed',
28 | top: '50%',
29 | left: '50%',
30 | transform: 'translate(-50%, -50%)',
31 | zIndex: '1'
32 | };
33 |
34 | return (
35 |
36 |
37 |
54 |
55 |
56 |
57 |
66 | Il y a un an, nous avons lancé la première version de notre extension pour l'intranet d'Epitech.
67 | Aujourd'hui, nous sommes ravis de vous présenter une mise à jour majeure : notre extension s'ouvre désormais directement dans une popup, offrant un accès plus rapide à vos statistiques.
68 | L'interface a été repensée pour une meilleure expérience utilisateur, avec l'ajout de nouvelles données pour enrichir votre vue d'ensemble de l'année scolaire, et plus encore à venir.
69 | ,
70 |
71 | Cette version est encore en cours d'amélioration, donc des bugs pourraient subsister.
72 | Vos retours sont essentiels pour nous aider à les résoudre et à améliorer l'extension.
73 | Utilisez les issues GitHub pour partager vos suggestions et signaler les problèmes rencontrés.
74 |
,
75 |
76 | Vous pouvez aussi contribuer à maintenir les données de l'extension à jour.
77 | Certaines informations ne sont pas récupérables depuis l'intranet d'Epitech et doivent être actualisées dans le fichier
78 | epitech_data.js .
79 | Signalez toute erreur via les issues GitHub pour garantir la précision des données fournies.
80 |
81 | ]}
82 | />
83 |
84 |
85 |
86 | );
87 | };
88 |
89 | export default App;
90 | // issues GitHub
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 |
4 | test('renders learn react link', () => {
5 | render( );
6 | const linkElement = screen.getByText(/learn react/i);
7 | expect(linkElement).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/src/Layout.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Box } from "@mui/material";
3 | import Sidebar from "./components/Sidebar";
4 | import Dashboard from "./Pages/Dashboard";
5 | import Hub from "./Pages/Hub";
6 | import Roadblock from "./Pages/Roadblock";
7 | import IconButton from "./components/IconButton";
8 | import { COLORS, BOX_SHADOW, BORDER_RADIUS } from './styles.js';
9 |
10 | // icons
11 | import HiveRoundedIcon from '@mui/icons-material/HiveRounded';
12 | import WidgetsRoundedIcon from '@mui/icons-material/WidgetsRounded';
13 | import BatchPredictionRoundedIcon from '@mui/icons-material/BatchPredictionRounded';
14 | import PublicRoundedIcon from '@mui/icons-material/PublicRounded';
15 | import TEPitech from "./Pages/TEPitech.js";
16 | import AboutModal from "./components/AboutModal.js";
17 |
18 | const Layout = () => {
19 | const [selectedPage, setSelectedPage] = useState("dashboard");
20 | const [isModalOpen, setIsModalOpen] = useState(false);
21 |
22 | const handlePageChange = (page) => {
23 | setSelectedPage(page);
24 | };
25 |
26 | const openModal = () => {
27 | setIsModalOpen(true);
28 | };
29 |
30 | const closeModal = () => {
31 | setIsModalOpen(false);
32 | };
33 |
34 | const renderSelectedPage = () => {
35 | switch (selectedPage) {
36 | case "dashboard":
37 | return ;
38 | case "hub":
39 | return ;
40 | case "roadblock":
41 | return ;
42 | case "tepitech":
43 | default:
44 | return ;
45 | }
46 | };
47 |
48 | return (
49 | <>
50 |
59 |
60 |
71 | {renderSelectedPage()}
72 |
73 |
74 |
75 |
85 | handlePageChange("dashboard")}
86 | selected={selectedPage === "dashboard"}
87 | icon={ }
88 | tooltipText="Dashboard"
89 | />
90 | handlePageChange("roadblock")}
92 | selected={selectedPage === "roadblock"}
93 | icon={ }
94 | tooltipText="Roadblock"
95 | />
96 | handlePageChange("hub")}
98 | selected={selectedPage === "hub"}
99 | icon={ }
100 | tooltipText="Hub"
101 | />
102 | handlePageChange("tepitech")}
104 | selected={selectedPage === "tepitech"}
105 | icon={ }
106 | tooltipText="TEPitech"
107 | />
108 |
109 | e.target.style.opacity = '0.8'}
124 | onMouseLeave={(e) => e.target.style.opacity = '1'}
125 | />
126 |
127 | >
128 | );
129 | };
130 |
131 | export default Layout;
132 |
--------------------------------------------------------------------------------
/src/Pages/Dashboard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Box } from '@mui/material';
3 | import { useData } from '../contexts/DataContext';
4 | import SummaryCard from '../components/SummaryCard';
5 | import TimelineBox from './TimelineBox';
6 |
7 | const Dashboard = () => {
8 | const { dashboardData, timelineData } = useData();
9 |
10 | return (
11 |
21 |
26 |
27 |
28 | );
29 | };
30 |
31 | export default Dashboard;
32 |
--------------------------------------------------------------------------------
/src/Pages/Hub.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Box, Skeleton } from '@mui/material';
3 | import { useData } from '../contexts/DataContext';
4 | import SummaryCard from '../components/SummaryCard';
5 | import RowGenerator from '../components/RowGenerator';
6 | import HubCard from '../components/HubCard';
7 |
8 | const Hub = () => {
9 | const { hubData } = useData();
10 |
11 | const itemsPerRow = 2;
12 |
13 | const renderCell = (key, data) => (
14 |
15 | );
16 |
17 | const renderSkeletons = () => {
18 | return (
19 |
20 | {[...Array(4)].map((_, index) => (
21 |
22 |
23 |
24 |
25 |
26 |
27 | ))}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | );
36 | };
37 |
38 | return (
39 |
49 |
54 |
55 | {!hubData ? renderSkeletons() : (
56 |
57 | )}
58 |
59 | );
60 | };
61 |
62 | export default Hub;
63 |
--------------------------------------------------------------------------------
/src/Pages/Roadblock.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Box, Skeleton } from '@mui/material';
3 | import RowGenerator from '../components/RowGenerator';
4 | import { useData } from '../contexts/DataContext';
5 | import RoadBlockCard from '../components/RoadBlockCard';
6 |
7 | const Roadblock = () => {
8 | const { roadblockData } = useData();
9 | const itemsPerRow = 2;
10 |
11 | const renderCell = (key, data) => (
12 |
13 | );
14 |
15 | const renderSkeletons = () => {
16 | return (
17 |
18 | {[...Array(4)].map((_, index) => (
19 |
20 |
21 |
22 |
23 |
24 |
25 | ))}
26 |
27 | );
28 | };
29 |
30 | return (
31 |
42 | {!roadblockData ? renderSkeletons() : (
43 |
44 | )}
45 |
46 | );
47 | };
48 |
49 | export default Roadblock;
--------------------------------------------------------------------------------
/src/Pages/TEPitech.css:
--------------------------------------------------------------------------------
1 | .apexcharts-tooltip-container {
2 | background: #f9f9f9;
3 | }
4 |
5 | .apexcharts-tooltip-content {
6 | padding: 10px;
7 | padding-top: 4px;
8 | }
9 |
10 | .apexcharts-tooltip {
11 | transform: translateX(0px) translateY(-20px);
12 | overflow: visible !important;
13 | white-space: normal !important;
14 | }
--------------------------------------------------------------------------------
/src/Pages/TEPitech.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Chart from 'react-apexcharts';
3 | import { Box, Skeleton, Typography } from '@mui/material';
4 | import { useData } from '../contexts/DataContext';
5 | import './TEPitech.css';
6 | import { COLORS } from '../styles';
7 |
8 | const renderSkeletons = () => {
9 | return (
10 |
11 |
12 |
19 |
20 |
21 | );
22 | };
23 |
24 | const ShowChart = ({ tepitechs }) => {
25 |
26 | if (!tepitechs) return renderSkeletons();
27 | const chartData = tepitechs.map((tepitech) => ({
28 | x: new Date(tepitech.date),
29 | y: tepitech.final_note,
30 | }));
31 |
32 | const chartOptions = {
33 | chart: {
34 | type: 'line',
35 | zoom: {
36 | enabled: false,
37 | },
38 | toolbar: {
39 | show: false,
40 | },
41 | animations: {
42 | enabled: true,
43 | easing: 'easeinout',
44 | speed: 800,
45 | },
46 | },
47 | legend: {
48 | show: false
49 | },
50 | stroke: {
51 | curve: 'smooth',
52 | },
53 | xaxis: {
54 | type: 'datetime',
55 | categories: chartData.map((data) => data.x),
56 | labels: {
57 | format: 'dd/MM/yyyy',
58 | }
59 | },
60 | yaxis: {
61 | title: {
62 | text: 'Notes',
63 | },
64 | min: 0,
65 | max: 980,
66 | },
67 | tooltip: {
68 | custom: ({ series, seriesIndex, dataPointIndex, w }) => {
69 | const data = tepitechs[dataPointIndex];
70 | const note = data?.final_note || '-';
71 | const sections = data?.sections ? Object.entries(data.sections)
72 | .map(([sectionName, { score, total }]) => `${sectionName}: ${score}/${total}
`)
73 | .join('') : '';
74 |
75 | return `
76 |
82 | `;
83 | },
84 | },
85 | };
86 |
87 | return (
88 | {/* 60px est la hauteur du titre */}
89 | data.y) }]} type="line" height='100%' />
90 |
91 | );
92 | };
93 |
94 | const TEPitech = () => {
95 | const { tepitechs } = useData();
96 |
97 | return (
98 |
108 |
122 |
123 |
129 | TEPitech - Notes
130 |
131 | {tepitechs && tepitechs.length === 0 ? renderSkeletons() : ShowChart({ tepitechs })}
132 |
133 |
134 |
135 | );
136 | };
137 |
138 | export default TEPitech;
139 |
--------------------------------------------------------------------------------
/src/Pages/TimelineBox.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Box, IconButton, Modal, Skeleton, Slide } from '@mui/material';
3 | import ChartComponent from "../components/ChartComponent";
4 | import FullscreenIcon from '@mui/icons-material/Fullscreen';
5 | import FullscreenExitIcon from '@mui/icons-material/FullscreenExit';
6 | import { COLORS, BOX_SHADOW } from '../styles.js';
7 |
8 | const TimelineBox = ({ timelineData }) => {
9 | const [isModalOpen, setIsModalOpen] = useState(false);
10 |
11 | const openModal = () => {
12 | setIsModalOpen(true);
13 | };
14 |
15 | const closeModal = () => {
16 | setIsModalOpen(false);
17 | };
18 |
19 | const renderSkeletons = () => {
20 | return (
21 |
22 |
23 |
30 |
31 |
32 |
33 |
40 |
41 |
42 |
43 |
44 |
45 | );
46 | };
47 |
48 | return (
49 |
65 |
66 | {/* {renderSkeletons()} */}
67 | {!timelineData ? renderSkeletons() : (
68 |
78 |
79 |
80 | )}
81 |
82 | {timelineData && }
83 |
93 |
94 |
103 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | );
120 | };
121 |
122 | export default TimelineBox;
--------------------------------------------------------------------------------
/src/components/AboutModal.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Modal, Box, Typography, Link, IconButton } from '@mui/material';
3 | import CloseIcon from '@mui/icons-material/Close';
4 | import { useData } from '../contexts/DataContext';
5 |
6 | const SettingsModal = ({ isOpen, handleClose }) => {
7 | const { developperData } = useData();
8 |
9 | return (
10 |
15 |
20 |
27 |
28 |
33 | Settings
34 |
35 |
36 |
37 |
38 | Actual version: {developperData?.currentVersion ?? '-'}
39 | Last data file update: {developperData?.dataLastUpdate ?? '-'}
40 |
41 |
42 | To report a bug, suggest a feature or report an incorrect data, please consult {' '}
43 | issues on GitHub.
44 |
45 |
46 |
47 |
48 | Developed by RedBoardDev
49 |
50 |
51 |
52 |
53 |
54 | );
55 | };
56 |
57 | export default SettingsModal;
58 |
--------------------------------------------------------------------------------
/src/components/AnnouncementHandler.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { Box, Button, Checkbox, Typography } from '@mui/material';
3 |
4 | const Announcement = ({ id, titles, messages }) => {
5 | const [currentPage, setCurrentPage] = useState(0);
6 | const [showAnnouncement, setShowAnnouncement] = useState(false);
7 | const [showAgain, setShowAgain] = useState(false);
8 |
9 | useEffect(() => {
10 | const seenAnnouncement = localStorage.getItem(id);
11 | if (!seenAnnouncement) {
12 | setShowAnnouncement(true);
13 | }
14 | }, [currentPage, titles, id]);
15 |
16 | const handleClose = () => {
17 | if (currentPage < titles.length - 1) {
18 | setCurrentPage(currentPage + 1);
19 | } else {
20 | setShowAnnouncement(false);
21 | if (showAgain) {
22 | localStorage.setItem(id, 'seen');
23 | }
24 | }
25 | };
26 |
27 | if (!showAnnouncement) return null;
28 |
29 | return (
30 | <>
31 |
42 |
56 |
57 |
67 |
68 |
69 |
70 | {titles[currentPage]}
71 | {messages[currentPage]}
72 |
73 |
74 |
75 |
76 | {currentPage >= titles.length - 1 && (
77 | <>
78 | setShowAgain(e.target.checked)}
81 | />
82 | Ne plus afficher
83 | >
84 | )}
85 |
86 | {currentPage < titles.length - 1 ? 'Suivant' : 'Fermer'}
87 |
88 |
89 | >
90 | );
91 | };
92 |
93 | export default Announcement;
94 |
--------------------------------------------------------------------------------
/src/components/ChartComponent.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Chart from "react-apexcharts";
3 |
4 | const defaultOptions = {
5 | colors: ['#FFA500', '#FF6347', '#228B22', '#9370DB', '#4B0082'],
6 | chart: {
7 | height: '100%',
8 | type: 'rangeBar',
9 | zoom: {
10 | enabled: false,
11 | },
12 | toolbar: {
13 | show: false,
14 | },
15 | animations: {
16 | enabled: true,
17 | easing: 'easeinout',
18 | speed: 600,
19 | },
20 | },
21 | plotOptions: {
22 | bar: {
23 | horizontal: true,
24 | borderRadius: 1,
25 | barHeight: '16%',
26 | rangeBarGroupRows: true,
27 | }
28 | },
29 | yaxis: {
30 | type: 'category'
31 | },
32 | xaxis: {
33 | type: 'datetime',
34 | labels: {
35 | format: 'dd/MM',
36 | }
37 | },
38 | fill: {
39 | type: 'solid',
40 | opacity: 0.9
41 | },
42 | legend: {
43 | show: false
44 | },
45 | grid: {
46 | borderColor: '#C2C2C2',
47 | strokeDashArray: 2,
48 | xaxis: {
49 | lines: {
50 | show: true
51 | }
52 | },
53 | yaxis: {
54 | lines: {
55 | show: true
56 | }
57 | }
58 | },
59 | annotations: {
60 | xaxis: [
61 | {
62 | x: new Date().getTime(),
63 | strokeDashArray: 0,
64 | borderColor: "#1c325c",
65 | opacity: 0.6,
66 | label: {
67 | borderColor: "#1c325c",
68 | style: {
69 | color: "#fff",
70 | background: "#1c325c"
71 | },
72 | text: "Today"
73 | }
74 | },
75 | ],
76 | },
77 | };
78 |
79 | const fullscreenOptions = {
80 | legend: {
81 | horizontalAlign: 'right',
82 | onItemClick: {
83 | toggleDataSeries: true
84 | },
85 | onItemHover: {
86 | highlightDataSeries: true
87 | },
88 | },
89 | chart: {
90 | toolbar: {
91 | show: true,
92 | offsetX: 0,
93 | offsetY: 0,
94 | tools: {
95 | download: true,
96 | selection: false,
97 | zoom: false,
98 | zoomin: false,
99 | zoomout: false,
100 | pan: false,
101 | reset: false,
102 | },
103 | autoSelected: 'download'
104 | },
105 | }
106 | };
107 |
108 | function ChartComponent({ data = {}, fullscreen = false }) {
109 | const options = fullscreen ? { ...defaultOptions, ...fullscreenOptions } : defaultOptions;
110 |
111 | return (
112 |
116 |
123 |
124 | );
125 | }
126 |
127 | export default ChartComponent;
128 |
--------------------------------------------------------------------------------
/src/components/CustomToolTip.js:
--------------------------------------------------------------------------------
1 | import { Tooltip as ToolTipMUI } from '@mui/material';
2 | import React from 'react';
3 |
4 | const CustomToolTip = ({ title, placement, children }) => {
5 | return (
6 |
7 |
23 | {children}
24 |
25 |
26 | );
27 | };
28 |
29 | export default CustomToolTip;
30 |
--------------------------------------------------------------------------------
/src/components/HubCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Box, Typography } from '@mui/material';
3 | import { useState, useEffect } from 'react';
4 | import { COLORS, BOX_SHADOW } from '../styles.js';
5 | import CustomToolTip from './CustomToolTip.js';
6 |
7 | const CardInfo = ({ module }) => {
8 | const [moduleColor, setModuleColor] = useState('#bf6c1b');
9 |
10 | // #bf6c1b orange
11 | // #2d962d green
12 | // #f25050 red
13 | // #a18716 grey yellow
14 |
15 | useEffect(() => {
16 | switch (module.status) {
17 | case 'soon':
18 | setModuleColor('#bf6c1b');
19 | break;
20 | case 'present':
21 | setModuleColor('#2d962d');
22 | break;
23 | case 'absent':
24 | setModuleColor('#f25050');
25 | break;
26 | case 'organisateur':
27 | setModuleColor('#a18716');
28 | break;
29 | default:
30 | setModuleColor('#bf6c1b');
31 | break;
32 | }
33 | }, [module]);
34 |
35 | return (
36 |
37 |
38 | {module.title.replace(/\[(.*?)\]/g, '')}
39 |
40 |
41 | );
42 | };
43 |
44 | const HubCard = ({ data }) => {
45 | const {
46 | name,
47 | activities,
48 | alias,
49 | limitOrg,
50 | limitPart,
51 | nbOrg,
52 | nbPart,
53 | nbXPTotal,
54 | xpLostPart,
55 | xpWinOrg,
56 | xpWinPart
57 | } = data;
58 | if (data === undefined) return null;
59 | return (
60 |
75 |
84 |
89 | {name} - {nbXPTotal} XP
90 |
91 |
97 | {nbPart}/{(limitPart === -1 ? '∞' : limitPart) ?? '∞'}
98 |
99 |
100 |
106 | {activities && activities.length > 0 && activities.map((activity) => (
107 |
108 | ))}
109 |
110 |
111 | );
112 | };
113 |
114 | export default HubCard;
--------------------------------------------------------------------------------
/src/components/IconButton.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { IconButton as IconButtonMui, Tooltip } from "@mui/material";
3 |
4 | const IconButton = ({ onClick, selected, icon, tooltipText }) => {
5 | return (
6 |
7 |
11 | {React.cloneElement(icon, { style: { fontSize: 40 } })}
12 |
13 |
14 | );
15 | };
16 |
17 | export default IconButton;
18 |
--------------------------------------------------------------------------------
/src/components/RoadBlockCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Box, Typography } from '@mui/material';
3 | import { useState, useEffect } from 'react';
4 | import { COLORS, BOX_SHADOW } from '../styles.js';
5 | import CustomToolTip from './CustomToolTip.js';
6 |
7 | const extractModuleInfo = (moduleName) => {
8 | const regex = /(\[[A-Z]-[A-Z]+-\d+\])\s*(\w+)\s*-\s*(.+)/;
9 | const matches = moduleName.match(regex);
10 |
11 | if (matches) {
12 | const code = matches[2];
13 | const restOfString = matches[3];
14 | const moduleCompleteName = `[${code}] - ${restOfString}`;
15 |
16 | return `${moduleCompleteName}`;
17 | }
18 | return moduleName;
19 | };
20 |
21 | const extractStudentNotes = (user_credits = 0, credits = 0, grade = undefined) => {
22 | const creditsString = `${user_credits ?? 0}/${credits} credits`;
23 |
24 | if (!grade || grade === 'N/A')
25 | return `${creditsString}`;
26 | return `${grade.toString()} - ${creditsString}`;
27 | }
28 |
29 | const determineTooltip = (data) => {
30 | if (data === 'N/A')
31 | return 'Not rated';
32 | if (!data)
33 | return "Not registered";
34 | if (data === 'ECHEC')
35 | return 'Failed';
36 | return 'Passed';
37 | }
38 |
39 | const ModuleInfo = ({ module }) => {
40 | const [moduleColor, setModuleColor] = useState('#8d9396');
41 |
42 | // #bf6c1b orange
43 | // #2d962d green
44 | // #f25050 red
45 | // #8d9396 grey
46 |
47 | useEffect(() => {
48 | switch (module.color) {
49 | case 'orange':
50 | setModuleColor('#bf6c1b');
51 | break;
52 | case 'green':
53 | setModuleColor('#2d962d');
54 | break;
55 | case 'red':
56 | setModuleColor('#f25050');
57 | break;
58 | default:
59 | setModuleColor('#8d9396');
60 | break;
61 | }
62 | }, [module]);
63 |
64 | return (
65 |
66 |
67 | {extractModuleInfo(module.name)}
68 |
69 | {extractStudentNotes(module.student_credits, module.credits, module.student_grade)}
70 |
71 | );
72 | };
73 |
74 | const RoadBlockCard = ({ roadblockData }) => {
75 | const {
76 | name: type,
77 | actual_student_credits,
78 | credit_needed,
79 | available_credits,
80 | modules,
81 | } = roadblockData;
82 |
83 | return (
84 |
96 |
105 |
110 | {type}
111 |
112 |
115 | Not enough credits
116 | You can have {available_credits} credits
117 | >
118 | : 'Enough credits'}
119 | placement="left"
120 | >
121 |
128 | {actual_student_credits} / {credit_needed}
129 |
130 |
131 |
132 |
138 | {modules.map((module) => (
139 |
140 | ))}
141 |
142 |
143 | );
144 | };
145 |
146 | export default RoadBlockCard;
--------------------------------------------------------------------------------
/src/components/RowGenerator.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Box } from '@mui/material';
3 |
4 | const RowGenerator = ({ data = [], itemsPerRow, renderCell }) => {
5 | let rows = [];
6 | const numRows = Math.ceil(data.length / itemsPerRow);
7 |
8 | for (let i = 0; i < numRows; i++) {
9 | const startIndex = i * itemsPerRow;
10 | const endIndex = startIndex + itemsPerRow;
11 | const rowData = data.slice(startIndex, endIndex);
12 |
13 | rows.push(
14 |
24 | {rowData.map((item, index) =>
25 | renderCell(index + startIndex, item))}
26 |
27 | );
28 | }
29 |
30 | return rows;
31 | };
32 |
33 | export default RowGenerator;
34 |
--------------------------------------------------------------------------------
/src/components/Sidebar.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { Box, IconButton, Typography } from '@mui/material';
3 | import { useData } from '../contexts/DataContext';
4 | import { COLORS, BOX_SHADOW, BORDER_RADIUS } from '../styles.js';
5 |
6 | const SidebarRender = ({ city, cursus, email, name, profilPicture, promo, semester, creditPhrase }) => {
7 | return (
8 |
24 |
35 |
36 | {
47 | e.target.src = 'https://cdn.pixabay.com/photo/2017/02/23/13/05/avatar-2092113_960_720.png';
48 | }}
49 | />
50 |
51 |
52 | {name}
53 | {email}
54 | {city}
55 | {cursus}
56 | semester {semester}
57 | {promo}
58 |
59 |
60 |
61 | {creditPhrase}
62 |
63 |
64 |
65 |
66 | );
67 | }
68 |
69 |
70 |
71 | const Sidebar = () => {
72 | const { sidebarData, creditsRequirement } = useData();
73 |
74 | const [city, setCity] = useState('-');
75 | const [cursus, setCursus] = useState('-');
76 | const [email, setEmail] = useState('-');
77 | const [name, setName] = useState('-');
78 | const [profilPicture, setProfilPicture] = useState('-');
79 | const [promo, setPromo] = useState('-');
80 | const [semester, setSemester] = useState('-');
81 | const [creditPhrase, setCreditPhrase] = useState('Credit information is currently unavailable.');
82 | const [showSidebar, setShowSidebar] = useState(window.innerWidth >= 1540);
83 | const [sidebarOpen, setSidebarOpen] = useState(false)
84 |
85 | useEffect(() => {
86 | const handleResize = () => {
87 | setShowSidebar(window.innerWidth >= 1540);
88 | setSidebarOpen(window.innerWidth >= 1540);
89 | };
90 |
91 | window.addEventListener("resize", handleResize);
92 | handleResize();
93 |
94 | return () => {
95 | window.removeEventListener("resize", handleResize);
96 | };
97 | }, []);
98 |
99 | useEffect(() => {
100 | setCity(sidebarData.city ?? '-');
101 | setCursus(sidebarData.cursus ?? '-');
102 | setEmail(sidebarData.email ?? '-');
103 | setName(sidebarData.name ?? '-');
104 | setProfilPicture(sidebarData.profilPicture ?? '-');
105 | setPromo(sidebarData.promo ?? '-');
106 | setSemester(sidebarData.semester ?? '-');
107 | }, [sidebarData]);
108 |
109 | const toggleSidebar = () => {
110 | setSidebarOpen(!sidebarOpen);
111 | };
112 |
113 | const generateCreditPhrase = (credits, neededCredits, availableCredits, status) => {
114 |
115 | switch (status) {
116 | case 'requirement_met':
117 | return `You have reached the required ${neededCredits} credits for this year. You currently have ${credits} credits and you can obtain ${availableCredits} credits.`;
118 | case 'requirement_attainable':
119 | return `You currently have ${credits} credits, ${neededCredits - credits} credits below the required ${neededCredits}. However, you can obtain ${availableCredits} more credits to meet the requirement.`;
120 | case 'credits_below_requirement':
121 | return `You currently have ${credits} credits, ${neededCredits - credits} credits below the required ${neededCredits}. Unfortunately, you can't meet the requirement with ${availableCredits} more available credits.`;
122 | default:
123 | return 'Credit information is currently unavailable.';
124 | }
125 | };
126 |
127 | useEffect(() => {
128 | if (creditsRequirement) {
129 | const { credits, neededCredits, availableCredits, status } = creditsRequirement;
130 | const phrase = generateCreditPhrase(credits, neededCredits, availableCredits, status);
131 | setCreditPhrase(phrase);
132 | }
133 | }, [creditsRequirement]);
134 |
135 | if (!showSidebar) {
136 | return (
137 | <>
138 |
155 | {sidebarOpen ? "<" : ">"}
156 |
157 | {sidebarOpen && (
158 |
168 | )}
169 | >
170 | );
171 | }
172 |
173 | return (
174 |
184 | )
185 | };
186 |
187 | export default Sidebar;
188 |
--------------------------------------------------------------------------------
/src/components/SummaryCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Box, Tooltip, Typography } from '@mui/material';
3 | import { COLORS, BOX_SHADOW } from '../styles.js';
4 |
5 | const CustomCard = ({ title = '-', text = '-', tooltipTitle = null }) => {
6 | return (
7 |
19 |
33 |
34 | {text}
35 |
36 |
37 | {title}
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | const SummaryCard = ({ cardsData, tooltipTitle = null }) => {
45 | if (!cardsData) return null;
46 |
47 | return (
48 |
58 | {cardsData.map((card, index) => (
59 |
60 | ))}
61 |
62 | );
63 | };
64 |
65 | export default SummaryCard;
66 |
--------------------------------------------------------------------------------
/src/contexts/DataContext.js:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useState, useEffect } from 'react';
2 | import { addListener, removeListener } from '../utils/listeners';
3 |
4 | const DataContext = createContext();
5 |
6 | function transformDataToSeries(data) {
7 | const courseNames = Object.keys(data);
8 |
9 | let series = [];
10 |
11 | courseNames.forEach(courseName => {
12 | let courseData = data[courseName];
13 |
14 | courseData.forEach(project => {
15 | let projectIndex = series.findIndex(serie => serie.name === project.title);
16 |
17 | if (projectIndex === -1) {
18 | series.push({
19 | name: project.title,
20 | data: [{ x: courseName, y: [new Date(project.begin).getTime(), new Date(project.end).getTime()] }]
21 | });
22 | } else {
23 | series[projectIndex].data.push({ x: courseName, y: [new Date(project.begin).getTime(), new Date(project.end).getTime()] });
24 | }
25 | });
26 | });
27 | return series;
28 | }
29 |
30 | export const DataProvider = ({ children }) => {
31 | const [dashboardData, setDashboardData] = useState({});
32 | const [sidebarData, setSidebarData] = useState({});
33 | const [roadblockData, setRoadblockData] = useState(undefined);
34 | const [hubData, setHubData] = useState(null);
35 | const [timelineData, setTimelineData] = useState(undefined);
36 | const [creditsRequirement, setCreditsRequirement] = useState(undefined);
37 | const [developperData, setDevelopperData] = useState({});
38 | const [tepitechs, setTepitechs] = useState(undefined);
39 |
40 | useEffect(() => {
41 | const handleDashboardUpdate = (event) => {
42 | const { detail } = event;
43 | setDashboardData(detail);
44 | };
45 |
46 | const handleSidebarUpdate = (event) => {
47 | const { detail } = event;
48 | setSidebarData(detail);
49 | };
50 |
51 | const handleRoadblockUpdate = (event) => {
52 | const { detail } = event;
53 | setRoadblockData(detail);
54 | }
55 |
56 | const handleHubUpdate = (event) => {
57 | const { detail } = event;
58 | setHubData(detail);
59 | }
60 |
61 | const handleTimelineUpdate = (event) => {
62 | const { detail } = event;
63 | setTimelineData(transformDataToSeries(detail));
64 | }
65 |
66 | const handleCreditsRequirementUpdate = (event) => {
67 | const { detail } = event;
68 | setCreditsRequirement(detail);
69 | }
70 |
71 | const handleDevelopperUpdate = (event) => {
72 | const { detail } = event;
73 | setDevelopperData(detail);
74 | }
75 |
76 | const handleTepitechsUpdate = (event) => {
77 | const { detail } = event;
78 | setTepitechs(detail);
79 | }
80 |
81 | addListener('sidebar', handleSidebarUpdate);
82 | addListener('dashboard', handleDashboardUpdate);
83 | addListener('roadblocks', handleRoadblockUpdate);
84 | addListener('hub', handleHubUpdate);
85 | addListener('timeline', handleTimelineUpdate);
86 | addListener('credits_requirement', handleCreditsRequirementUpdate);
87 | addListener('developper', handleDevelopperUpdate);
88 | addListener('tepitechs', handleTepitechsUpdate);
89 |
90 | return () => {
91 | removeListener('sidebar', handleSidebarUpdate);
92 | removeListener('dashboard', handleDashboardUpdate);
93 | removeListener('roadblocks', handleRoadblockUpdate);
94 | removeListener('hub', handleHubUpdate);
95 | removeListener('timeline', handleTimelineUpdate);
96 | removeListener('credits_requirement', handleCreditsRequirementUpdate);
97 | removeListener('developper', handleDevelopperUpdate);
98 | removeListener('tepitechs', handleTepitechsUpdate);
99 | };
100 | }, []);
101 |
102 | return (
103 |
113 | {children}
114 |
115 | );
116 | };
117 |
118 | export const useData = () => {
119 | const context = useContext(DataContext);
120 | if (!context) {
121 | throw new Error('useData must be used within a DataProvider');
122 | }
123 | return context;
124 | };
125 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import { DataProvider } from './contexts/DataContext';
6 |
7 | const root = ReactDOM.createRoot(document.getElementById('root'));
8 | root.render(
9 |
10 | {/* */}
22 |
23 |
24 |
25 | {/*
*/}
26 |
27 | );
28 |
29 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/src/styles.js:
--------------------------------------------------------------------------------
1 | export const COLORS = {
2 | box2: 'rgba(228, 230, 229, 0.8)',
3 | box22: 'rgba(228, 230, 229, 1.0)',
4 | backgroundBox: 'rgba(157, 163, 166, 0.2)',
5 | sidebar: 'rgba(157, 163, 166, 0.2)',
6 | mainBackground: '#D7D7D7',
7 | };
8 |
9 | export const BOX_SHADOW = {
10 | box2: '0px 4px 9px rgba(51, 51, 51, 0.1)',
11 | box22: '0px 4px 9px rgba(51, 51, 51, 0.1)',
12 | backgroundBox: '2px 3px 8px rgba(51, 51, 51, 0.0), 0px 0px 12px rgba(51, 51, 51, 0.3)',
13 | sidebar: '2px 3px 8px rgba(51, 51, 51, 0.1), 0px 0px 12px rgba(51, 51, 51, 0.3)',
14 | mainBackground: '0px 0px 8px 0px #1F364D',
15 | };
16 |
17 | export const BORDER_RADIUS = {
18 | box2: 3,
19 | main: 6
20 | }
21 |
--------------------------------------------------------------------------------
/src/utils/listeners.js:
--------------------------------------------------------------------------------
1 | export const addListener = (eventName, callback) => {
2 | window.addEventListener(eventName, callback);
3 | };
4 |
5 | export const removeListener = (eventName, callback) => {
6 | window.removeEventListener(eventName, callback);
7 | };
8 |
--------------------------------------------------------------------------------