given files

This commit is contained in:
Gu://em_ 2026-05-15 11:08:23 +02:00
parent 9bf88844e9
commit a2c31f873d
48 changed files with 10458 additions and 0 deletions

8
.env Normal file
View file

@ -0,0 +1,8 @@
VITE_HOST="localhost"
VITE_PORT="8080"
VITE_URL="http://${VITE_HOST}:${VITE_PORT}"
VITE_API_URL="http://localhost:3333"
# VITE_API_URL="https://eplace.assistants.epita.fr"
VITE_AUTH_URL="https://cri.epita.fr"
VITE_CLIENT_ID="assistants-atelier-js"

24
.gitignore vendored Normal file
View file

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

6
.prettierrc.js Normal file
View file

@ -0,0 +1,6 @@
module.exports = {
tabWidth: 4,
useTabs: false, // Use spaces instead of defaults tabs
semi: true, // Force semilicons
printWidth: 80, // Max width
};

71
eslint.config.mjs Normal file
View file

@ -0,0 +1,71 @@
import jestPlugin from "eslint-plugin-jest";
import globals from "globals";
import prettierPlugin from "eslint-plugin-prettier"; // Import the Prettier plugin
import eslintComments from "eslint-plugin-eslint-comments";
import js from "@eslint/js";
const cleanGlobals = (obj) =>
Object.fromEntries(
Object.entries(obj).map(([key, val]) => [key.trim(), val]),
);
export default [
js.configs.recommended, // Nice defaults rules
{
files: ["**/*.js", "**/*.mjs"], // Apply to .js and .mjs files
languageOptions: {
sourceType: "module",
globals: {
AudioWorkletGlobalScope: "readonly",
...cleanGlobals(globals.node),
...cleanGlobals(jestPlugin.environments.globals.globals),
...cleanGlobals(globals.browser),
},
},
plugins: {
prettier: prettierPlugin, // Add Prettier plugin correctly
jest: jestPlugin, // Jest plugin
"eslint-comments": eslintComments, // Add plugin to detect cheats
},
rules: {
"eslint-comments/no-use": ["error", { allow: [] }], // Disallow disable rules
curly: ["error", "all"], // Enforce curlies in conditionnal blocks
"brace-style": ["error", "1tbs"],
"max-statements-per-line": ["error", { max: 1 }],
semi: ["error", "always"], // Enforce semicolons
"prefer-const": "error", // Prefer const over let
"no-undef": "error", // Detect definition
"no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
// Detect error of line width (but can't fix them without the prettier)
"max-len": [
"error",
{
code: 80,
tabWidth: 4,
ignoreComments: true,
ignoreStrings: true,
ignoreTemplateLiterals: true,
ignoreRegExpLiterals: true,
},
], // More rules to complete with the existants in .prettierrc.js file
"prettier/prettier": ["error"], // Enforce Prettier formatting
"padding-line-between-statements": [
// Create nice looking paddings between statements
"error",
{
blankLine: "always",
prev: ["const", "let", "var", "if", "for", "while", "do"],
next: "*",
},
{
blankLine: "any",
prev: ["const", "let", "var"],
next: ["const", "let", "var"],
},
], // Requires blank lines between the given 2 kinds of statements
},
},
];

31
package.json Normal file
View file

@ -0,0 +1,31 @@
{
"name": "eplace-client",
"private": true,
"version": "1.0.0",
"scripts": {
"dev": "vite",
"debug": "vite --mode debug"
},
"devDependencies": {
"eslint": "^9.25.1",
"eslint-config-prettier": "^8.8.0",
"eslint-config-standard": "^17.0.0",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-n": "^15.0.0",
"eslint-plugin-promise": "^6.0.0",
"less": "^4.1.3",
"vite": "^4.2.0"
},
"dependencies": {
"axios": "^1.4.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-jest": "^28.11.0",
"eslint-plugin-prettier": "^5.2.6",
"jquery": "^3.6.4",
"jwt-decode": "^3.1.2",
"node-fetch": "^3.3.1",
"prettier": "^3.5.3",
"socket.io-client": "^4.6.1",
"uuid": "^9.0.0"
}
}

BIN
public/default-avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
public/default-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 590 B

14
public/selector.svg Normal file
View file

@ -0,0 +1,14 @@
<svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" height="120%" width="120%">
<g stroke-opacity=".6" stroke-width="2">
<g stroke="#000">
<path d="m3 9v-6h6"></path>
<path d="m14.9999 3h6v6"></path>
<path d="m20.9999 15.0001v6h-6"></path>
<path d="m9 21.0001h-6v-6"></path>
</g>
<path d="m1 9v-8h8" stroke="#fff"></path>
<path d="m15 1h8v8" stroke="#fff"></path>
<path d="m23 15v8h-8" stroke="#fff"></path>
<path d="m9 23h-8v-8" stroke="#fff"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 588 B

37
server/.env Normal file
View file

@ -0,0 +1,37 @@
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
NODE_ENV="production"
RECORDER="active"
SERVER_PORT=3333
WSS_PORT=3334
POSTGRES_USER="postgres"
POSTGRES_PASSWORD="postgres"
POSTGRES_HOST="postgres"
POSTGRES_PORT=5432
POSTGRES_DB="eplace"
POSTGRES_SCHEMA="public"
DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}?schema=${POSTGRES_SCHEMA}"
REDIS_HOST="redis"
REDIS_PORT=6379
PUBLIC_API_URL="http://localhost:3333/api"
JWKS_URI="https://cri.epita.fr/jwks"
# DO NOT ERASE THIS UID, IT IS USE AS DEFAULT USER TO SETUP ROOMs, PIXELS
# CHECK README FOR MORE DETAILS
# You can add more admin uids by separating them with a comma
# Example: 9361,9362,9363
ADMIN_UID_LIST="9361"
RATE_LIMITS_CONFIG_PATH="./config/rate-limits.config.json"
ROOMS_CONFIG_PATH="./config/rooms.config.json"
DISABLE_ADMIN_PREVILEGES="true"
DISABLE_AUTH="false"
DISABLE_RATE_LIMITING="true"

File diff suppressed because one or more lines are too long

Binary file not shown.

View file

@ -0,0 +1,54 @@
{
"testsLimiter": {
"limit": 10,
"interval": 1
},
"getCanvasLimiter": {
"limit": 10,
"interval": 1
},
"getPixelLimiter": {
"limit": 10,
"interval": 1
},
"placePixelLimiter": {
"limit": 1,
"interval": 30
},
"getRoomsLimiter": {
"limit": 10,
"interval": 1
},
"getRoomConfigLimiter": {
"limit": 10,
"interval": 1
},
"createRoomLimiter": {
"limit": 1,
"interval": 300
},
"updateRoomLimiter": {
"limit": 1,
"interval": 1
},
"deleteRoomLimiter": {
"limit": 2,
"interval": 1
},
"getStudentLimiter": {
"limit": 10,
"interval": 1
},
"updateStudentLimiter": {
"limit": 1,
"interval": 1
},
"sendMessageLimiter": {
"limit": 1,
"interval": 1
},
"reportRoomLimiter": {
"limit": 1,
"interval": 5
}
}

View file

@ -0,0 +1,46 @@
{
"maxRoomsCreatedPerUser": 3,
"rooms": {
"default": {
"metadata": {
"name": "default room",
"slug": "default",
"description": "default room",
"canvasDimensions": 50,
"iconURL": "https://media.tenor.com/XUHq8pN_maQAAAAi/puffer-fish-fish.gif",
"isPublic": false
},
"settings": {
"roomColors": "#ffffff,#d4d7d9,#898d90,#515252,#000000,#fe4500,#fea800,#fed634,#01a268,#7eed56,#2350a4,#3690ea,#51e9f4,#811f9f,#b44bc0,#ff99aa,#9c6925",
"defaultCanvas": "config/default-canvas-50.txt"
}
},
"epi-place": {
"metadata": {
"name": "epi/place",
"slug": "epi-place",
"description": "Le Roi de la malice est passé par là",
"canvasDimensions": 250,
"iconURL": "https://media.tenor.com/XUHq8pN_maQAAAAi/puffer-fish-fish.gif",
"isPublic": true
},
"settings": {
"roomColors": "#ffffff,#d4d7d9,#898d90,#515252,#000000,#6c001a,#be0039,#fe4500,#fea800,#fed634,#fff8b8,#01a268,#00cc78,#7eed56,#02756f,#019eaa,#00ccbf,#2350a4,#3690ea,#51e9f4,#493ac1,#6a5cff,#94b3ff,#811f9f,#b44bc0,#e4aaff,#de107f,#ff3981,#ff99aa,#6d482f,#9c6925,#ffb470,#811f9f,#000000",
"defaultCanvas": "config/default-canvas-250.txt"
}
},
"test": {
"metadata": {
"name": "Test Room",
"description": "A room small enough to test things out",
"canvasDimensions": 10,
"iconURL": "https://media.tenor.com/XUHq8pN_maQAAAAi/puffer-fish-fish.gif",
"slug": "test",
"isPublic": true
},
"settings": {
"roomColors": "#ffffff,#d4d7d9,#898d90,#515252,#000000,#fe4500,#fea800,#fed634,#01a268,#7eed56,#2350a4,#3690ea,#51e9f4,#811f9f,#b44bc0,#ff99aa,#9c6925"
}
}
}
}

56
server/docker-compose.yml Normal file
View file

@ -0,0 +1,56 @@
version: '3.9'
services:
postgres:
image: registry.cri.epita.fr/ing/assistants/public/registry/postgres:15.2-alpine
container_name: postgres
restart: always
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: eplace
expose:
- 5432
volumes:
- postgres-data:/var/lib/postgresql/data
# Proper docker-compose would use named networks
# networks:
# - postgres-network
redis:
image: registry.cri.epita.fr/ing/assistants/public/registry/redis:7.0.9-alpine
container_name: redis
restart: always
expose:
- 6379
volumes:
- redis-data:/data
# Proper docker-compose would use named networks
# networks:
# - redis-network
eplace:
image: registry.cri.epita.fr/ing/assistants/public/registry/eplace:latest
container_name: eplace
restart: always
environment:
NODE_ENV: production
volumes:
- ./config:/usr/src/app/config
- type: 'bind'
source: './.env'
target: '/usr/src/app/.env'
ports:
- 3000:3000
- 3333:3333
# Proper docker-compose would use named networks
# networks:
# - postgres-network
# - redis-network
depends_on:
- postgres
- redis
volumes:
postgres-data:
redis-data:
# Proper docker-compose would use named networks
# networks:
# postgres-network:
# redis-network:

5503
server/openapi/openapi.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,12 @@
<div class="Alert">
<div class="Icon">
<i class="fa {{icon_classes}}"></i>
</div>
<div class="AlertBody">
<span class="AlertTitle">{{title}}</span>
<span class="AlertContent">{{content}}</span>
</div>
<span class="AlertClose">
<i class="fa fa-times-circle"></i>
</span>
</div>

View file

@ -0,0 +1,10 @@
<button class="Room">
<img src="{{icon_url}}" class="Avatar" />
<div class="TextContainer">
<div class="Name">
<i class="RoomPrivacy {{privacy_icon}}"></i>
<span>{{name}}</span>
</div>
<span class="RoomOwner">owner: {{owner_login}}</span>
</div>
</button>

View file

@ -0,0 +1,8 @@
<div class="ChatMessage">
<div class="MessageHeader">
<img src="{{avatar_url}}" class="Avatar" />
<span class="Login">{{message_author}}</span>
<span class="Time">{{sent_at}}</span>
</div>
<span class="MessageContent">{{message_content}}</span>
</div>

View file

@ -0,0 +1,70 @@
<div class="FormOverlay">
<form class="StylisedForm" id="room-upsert-form">
<div class="FormHeader">
<h2 class="FormTitle">{{form_title}}</h2>
</div>
<div class="FormItem">
<label for="name" class="FormLabel">Name</label>
<input
type="text"
class="FormInput"
id="name"
name="name"
placeholder="Enter name"
/>
</div>
<div class="FormItem">
<label for="description" class="FormLabel">Description</label>
<input
type="text"
class="FormInput"
id="description"
name="description"
placeholder="Enter description"
/>
</div>
<div class="FormItem">
<label for="icon-url" class="FormLabel">Icon URL</label>
<input
type=""
class="FormInput"
id="icon-url"
name="icon-url"
placeholder="Enter icon URL"
/>
</div>
<div class="FormItem">
<label for="whitelist" class="FormLabel">Students Whitelist</label>
<input
type="text"
class="FormInput"
id="whitelist"
name="whitelist"
placeholder="Enter student's logins or UIDs, separated by commas (private rooms only)"
/>
</div>
<div class="FormItem">
<label for="blacklist" class="FormLabel">Students Blacklist</label>
<input
type="text"
class="FormInput"
id="blacklist"
name="blacklist"
placeholder="Enter student's logins or UIDs, separated by commas (public rooms only)"
/>
</div>
<div class="FormItem">
<label for="is-public" class="FormLabel">Is Public</label>
<input
type="checkbox"
class="FormInput"
id="is-public"
name="is-public"
/>
</div>
<div class="FormButtons">
<button type="button" id="close-modal">Cancel</button>
<button type="submit">Submit</button>
</div>
</form>
</div>

View file

@ -0,0 +1,3 @@
<div class="ChatUserEvent">
<span class="MessageContent">{{message_content}}</span>
</div>

View file

@ -0,0 +1,42 @@
<div class="FormOverlay">
<form class="StylisedForm" id="student-update-form">
<div class="FormHeader">
<h2 class="FormTitle">Update Profile</h2>
</div>
<div class="FormItem">
<label for="avatar-url" class="FormLabel">Avatar URL</label>
<input
type=""
class="FormInput"
id="avatar-url"
name="avatar-url"
placeholder="Enter avatar URL"
/>
</div>
<div class="FormItem">
<label for="guild-tag" class="FormLabel">Guild Tag</label>
<input
type="text"
class="FormInput"
id="guild-tag"
name="guild-tag"
placeholder="Enter guild tag"
/>
</div>
<div class="FormItem">
<label for="quote" class="FormLabel">Quote</label>
<input
type="text"
class="FormInput"
id="quote"
name="quote"
placeholder="Enter a quote"
/>
</div>
<div class="FormButtons">
<button type="button" id="close-modal">Cancel</button>
<button type="submit">Submit</button>
</div>
</form>
</div>

View file

@ -0,0 +1,12 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>E/PLACE</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
<script type="module" src="index.js"></script>
</head>
<body></body>
</html>

View file

@ -0,0 +1,2 @@
// FIXME: This file should handle the auth redirection
// Get the code from the URL parameters and redirect to the relevant page

View file

@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Debug Page</title>
</head>
<body>
<div class="AlertContainer" id="alert-container"></div>
<div class="container">
<h1>Debug Dashboard</h1>
<section class="card">
<h2>Local Storage Tokens</h2>
<div class="token-section">
<h3>Access Token</h3>
<p id="token" class="token-text">{{ token }}</p>
<h3>Refresh Token</h3>
<p id="refresh_token" class="token-text">{{ refresh_token }}</p>
</div>
<div class="token-actions button-group">
<button id="deleteTokenBtn">Delete Access Token</button>
<button id="deleteRefreshTokenBtn">Delete Refresh Token</button>
<button id="clearTokens">Clear All Tokens</button>
</div>
</section>
<section class="card">
<h2>User Profile</h2>
<div class="StudentProfile">
<img src="" class="Avatar" id="profile-info-avatar" alt="User Avatar" />
<div class="TextContainer">
<span class="Login" id="profile-info-login">no login</span>
<span class="Quote" id="profile-info-quote"></span>
</div>
</div>
<div class="profile-actions button-group">
<button id="launchOIDC">Launch OIDC</button>
<button id="displayProfile">Display Profile Info</button>
<button id="hideProfile">Clear Profile Info</button>
</div>
</section>
<section class="card">
<h2>Error Simulation</h2>
<div class="error-actions button-group">
<button id="errorBtn">Generate Error Response</button>
<button id="invalidToken">Generate Invalid Token</button>
<button id="expiredTokenBtn">Generate Expired Token</button>
</div>
</section>
</div>
</body>
</html>

122
src/pages/debug/debug.js Normal file
View file

@ -0,0 +1,122 @@
import $ from "jquery";
import debugHtml from "./debug.html";
import { displayStudentProfile, expired } from "./utils";
import { createAlert } from "../../utils/notify";
const { authedAPIRequest, authenticate } = await import("../../utils/auth.js");
const authed = authedAPIRequest ?? (() => {});
function clearLocalStorage() {
localStorage.removeItem("token");
localStorage.removeItem("refresh_token");
}
function clearUserProfile(log = true) {
$("#profile-info-login").text("No login");
$("#profile-info-quote").text("No quote");
log && createAlert("Debug", "Clear user profile info", "success");
}
function refreshLocalStorage() {
$("#token").text(localStorage.getItem("token") ?? "N/A");
$("#refresh_token").text(localStorage.getItem("refresh_token") ?? "N/A");
}
async function refreshProfile() {
await displayStudentProfile()
.then(() => {
createAlert("Debug", "Display profile succeed", "success");
})
.catch((_error) => {
createAlert("Debug", "Cannot display profile", "error");
clearUserProfile(false);
});
}
async function refresh() {
refreshLocalStorage();
await refreshProfile();
}
(() => {
if (import.meta.env.MODE !== "debug") {
return;
}
$.get(debugHtml, function (response) {
$("body").html(response);
refreshLocalStorage();
}).fail(function (_xhr, _status, error) {
console.error("Error fetching debug HTML:", error);
});
$(document).on("click", "#launchOIDC", async function () {
clearLocalStorage();
if (await authenticate()) {
createAlert("Debug", "OIDC succeed", "error");
} else {
createAlert("Debug", "OIDC failed", "error");
}
await refresh();
});
$(document).on("click", "#displayProfile", refreshProfile);
$(document).on("click", "#hideProfile", clearUserProfile);
$(document).on("click", "#errorBtn", async function () {
await authed("/tests/error", { method: "GET" });
createAlert("Debug", "Error response generated", "success");
await refresh();
});
$(document).on("click", "#invalidToken", async function () {
await authedAPIRequest("/tests/invalid-token", { method: "POST" });
createAlert("Debug", "Invalid token response generated", "success");
await refresh();
});
$(document).on("click", "#expiredTokenBtn", async function () {
expired();
let res = await authedAPIRequest("/tests/valid", { method: "GET" });
res = await res.json();
if (res.response === "A valid response") {
createAlert(
"Debug",
"Token has been refreshed and the request has been re-send",
"success",
);
} else {
createAlert("Debug", "An error occured", "error");
}
await refresh();
});
$(document).on("click", "#deleteTokenBtn", async function () {
localStorage.removeItem("token");
createAlert("Debug", "Token removed from local storage", "success");
await refresh();
});
$(document).on("click", "#deleteRefreshTokenBtn", async function () {
localStorage.removeItem("refresh_token");
createAlert(
"Debug",
"Refresh token removed from local storage",
"success",
);
await refresh();
});
$(document).on("click", "#clearTokens", async function () {
createAlert(
"Debug",
"Token and refresh token removed from local storage",
"success",
);
clearLocalStorage();
await refresh();
});
refresh();
})();

128
src/pages/debug/index.css Normal file
View file

@ -0,0 +1,128 @@
:root {
--bg-page: #f0f2f5;
--bg-card: #5A6991;
--bg-token: #e8eaed;
--bg-profile: #e8eaed;
--border-card: #d1d5db;
--text-heading: #111827;
--text-label: #6b7280;
--text-body: #1f2937;
--text-quote: #6b7280;
--shadow-card: 0 2px 8px rgba(0, 0, 0, 0.08);
--btn-bg: #455069;
--btn-bg-hover: #111827;
--btn-text: #ffffff;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-page: #0f1117;
--bg-card: #1a1d27;
--bg-token: #2a2d3a;
--bg-profile: #2a2d3a;
--border-card: #2e3348;
--text-heading: #f0f2f5;
--text-label: #8b92a5;
--text-body: #e2e5ed;
--text-quote: #8b92a5;
--shadow-card: 0 2px 8px rgba(0, 0, 0, 0.4);
--btn-bg: #3B4257;
--btn-bg-hover: #51596E;
--btn-text: #e2e5ed;
}
}
body {
background-color: var(--bg-page);
color: var(--text-body);
transition: background-color 0.2s, color 0.2s;
}
.container {
width: 800px;
}
.card {
background-color: var(--bg-card);
border: 1px solid var(--border-card);
border-radius: 10px;
padding: 20px;
margin: 20px;
box-shadow: var(--shadow-card);
}
.card h2 {
margin-top: 0;
margin-bottom: 15px;
font-size: 1.5em;
color: var(--text-heading);
}
.token-section h3 {
margin-bottom: 5px;
font-size: 1.1em;
color: var(--text-body);
}
.token-text {
background-color: var(--bg-token);
padding: 10px;
border-radius: 5px;
word-break: break-all;
color: var(--text-body);
}
.StudentProfile {
display: flex;
align-items: center;
gap: 15px;
width: 30%;
margin: 10px 0 30px;
border-radius: 5px;
padding: 7px 10px;
background-color: var(--bg-profile);
}
.Avatar {
width: 60px;
height: 60px;
border-radius: 50%;
object-fit: cover;
border: 1px solid var(--border-card);
}
.TextContainer {
display: flex;
flex-direction: column;
}
.Login {
font-weight: bold;
font-size: 1.1em;
color: var(--text-body);
}
.Quote {
font-style: italic;
color: var(--text-quote);
}
.button-group {
display: flex;
justify-content: space-around;
}
button {
background-color: var(--btn-bg);
color: var(--btn-text);
border: none;
border-radius: 6px;
padding: 8px 16px;
font-size: 0.9em;
cursor: pointer;
}
button:hover {
background-color: var(--btn-bg-hover);
}

57
src/pages/debug/utils.js Normal file
View file

@ -0,0 +1,57 @@
// FIXME: File that provide utils function for the debug page
import $ from "jquery";
import jwt_decode from "jwt-decode";
import { createAlert } from "../../utils/notify";
export async function displayStudentProfile() {
const token = localStorage.getItem("token");
const decoded = jwt_decode(token);
const _uid = decoded.uid;
// You have to write a request to fetch your informations
const request_result = null;
if (request_result === null) {
createAlert(
"debug",
"Fetch not implemented in the pages/debug/utils.js file",
"error",
);
}
const student_resources = await request_result?.json();
$("#profile-info-avatar").attr(
"src",
student_resources.avatarURL ?? "/default-avatar.png",
);
$("#profile-info-login").text(student_resources.login);
$("#profile-info-quote").text(student_resources.quote);
}
export function expired() {
const token = localStorage.getItem("token");
const splited_token = token.split(".");
let base64 = splited_token[1].replace(/-/g, "+").replace(/_/g, "/");
while (base64.length % 4 !== 0) {
base64 += "=";
}
const parts = JSON.parse(atob(base64));
parts["exp"] = 0;
const recodedValue = btoa(JSON.stringify(parts))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
splited_token[1] = recodedValue;
const expiredToken = splited_token.join(".");
localStorage.setItem("token", expiredToken);
}

78
src/pages/index.css Normal file
View file

@ -0,0 +1,78 @@
@import url(http://fonts.googleapis.com/css?family=Barlow:800);
@import url(http://fonts.googleapis.com/css?family=Montserrat:400,700);
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif, Barlow;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
justify-content: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
margin: 0;
}
h2 {
margin: 0;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
input {
border: none;
}
.token-text {
max-width: 10ch;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}

239
src/pages/index.html Normal file
View file

@ -0,0 +1,239 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>E/PLACE</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
<link
rel="stylesheet"
type="text/css"
media="screen"
href="index.css"
/>
<link rel="stylesheet/less" type="text/css" href="styles.less" />
<link
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"
rel="stylesheet"
/>
<script src="https://cdn.jsdelivr.net/npm/less"></script>
<script type="module">
if (import.meta.env.MODE === "debug") {
const link = document.createElement("link");
link.rel = "stylesheet";
link.type = "text/css";
link.media = "screen";
link.href = "debug/index.css";
document.head.appendChild(link);
}
const script = document.createElement("script");
if (import.meta.env.MODE !== "debug") {
script.src = "./index.js";
} else {
script.src = "./debug/debug.js";
}
script.type = "module";
document.head.appendChild(script);
</script>
</head>
<body>
<div class="App">
<div class="AlertContainer" id="alert-container"></div>
<div class="Container" id="container">
<div class="RoomList Hidden" id="left-container">
<div class="Header">
<h1 class="Title">E/PLACE</h1>
<button class="CloseButton" id="close-left">
<i class="fa-solid fa-backward"></i>
</button>
</div>
<div class="ListContainer">
<div class="ListSearchBar">
<div class="InputContainer">
<input
aria-placeholder="Search a room..."
placeholder="Search a room..."
class="Input"
id="search-input"
/>
<div class="Divider"></div>
<button class="RoomButton" id="create-room">
<i class="fa-solid fa-square-plus"></i>
</button>
<button class="RoomButton" id="refresh-rooms">
<i class="fa-solid fa-rotate"></i>
</button>
</div>
<div class="FilterContainer">
<span class="FilterText">Filters:</span>
<div class="FilterHeader">
<button class="Filter" id="filter-name">
<span>Name</span>
<i class="fa-solid fa-sort-down"></i>
</button>
<button class="Filter" id="filter-owner">
<span>Owned by you</span>
<i class="fa-solid fa-plus"></i>
</button>
<button class="Filter" id="filter-public">
<span>Public</span>
<i class="fa-solid fa-minus"></i>
</button>
<button class="Filter" id="filter-private">
<span>Private</span>
<i class="fa-solid fa-minus"></i>
</button>
</div>
</div>
</div>
<div class="RoomsContainer" id="rooms-container"></div>
</div>
<div class="StudentProfile">
<img src="" class="Avatar" id="profile-info-avatar" />
<div class="TextContainer">
<span class="Login" id="profile-info-login"></span>
<span class="Quote" id="profile-info-quote"></span>
</div>
<button class="ModifyProfileButton" id="profile-update">
<i class="fa-solid fa-pencil"></i>
</button>
</div>
</div>
<div class="RoomCanvas">
<div class="Header">
<div class="Header-left">
<button class="HeaderButton" id="MenuButton">
Menu
</button>
</div>
<div class="Header-center">
<div class="TextContainer">
<h2 class="Title" id="room-name">
{{room_name}}
</h2>
<span class="Description" id="room-description"
>{{room_description}}</span
>
</div>
<div class="HeaderDivider"></div>
<div class="ButtonContainer">
<button class="RoomButton" id="edit-room">
<i class="fa-solid fa-pencil"></i>
</button>
<button class="RoomButton" id="delete-room">
<i class="fa-solid fa-trash-can"></i>
</button>
</div>
</div>
<div class="Header-right">
<button class="HeaderButton" id="ChatButton">
Chat
</button>
</div>
</div>
<div class="GuildLeaderboard" id="guild-leaderboard">
<div class="LeaderboardHeader">
<i class="fa-solid fa-trophy"></i>
<span class="LeaderboardTitle">GUILDS</span>
</div>
<ol class="LeaderboardList" id="leaderboard-list"></ol>
</div>
<div class="CanvasContainer" id="canvas-container">
<canvas class="Canvas" id="canvas"></canvas>
<img
class="Selector"
id="selector"
src="/selector.svg"
/>
<div class="Tooltip" id="tooltip">
<div class="Header">
<div class="PlacedByInfo">
<img
src="{{student_avatar}}"
class="Avatar"
id="tooltip-info-avatar"
/>
<div class="Profile">
<span
class="Login"
id="tooltip-info-login"
>{{student_login}}</span
>
<span
class="Guild"
id="tooltip-info-guild"
>{{student_guild}}</span
>
<span
class="Quote"
id="tooltip-info-quote"
>{{student_quote}}</span
>
</div>
</div>
<div class="TextContainer">
<span id="tooltip-date"
>{{date_placed}}</span
>
<span id="tooltip-time"
>{{time_placed}}</span
>
</div>
</div>
<div class="ButtonContainer">
<button class="ColorPicker" id="color-picker">
<i class="fas fa-eye-dropper"></i>
</button>
<button
class="PlaceButton"
id="color-place-button"
>
PLACE
</button>
</div>
</div>
</div>
<div class="PositionTooltip" id="position-tooltip">
<span>X=0</span>
<span>Y=0</span>
</div>
<div class="ColorWheelContainer" id="color-wheel-container">
<div class="ColorWheel" id="color-wheel"></div>
</div>
</div>
<div class="RoomChat" id="right-container">
<div class="ChatContainer">
<div class="Header">
<button class="CloseButton" id="close-right">
<i class="fa-solid fa-forward"></i>
</button>
</div>
<div
class="ChatMessageContainer"
id="chat-message-container"
></div>
<form
class="InputContainer"
id="chat-input-form"
autocomplete="off"
>
<input
aria-placeholder="Type your message here..."
placeholder="Type your message here..."
class="Input"
id="message-content"
name="message-content"
autocomplete="off"
/>
<div class="ChatTimeout">
<i class="fa-solid fa-stopwatch"></i>
</div>
</form>
</div>
</div>
</div>
</div>
</body>
</html>

6
src/pages/index.js Normal file
View file

@ -0,0 +1,6 @@
// FIXME: This is the entry point of the application, write your code here
import { calculateLayout } from "./utils";
// Initialize the layout
calculateLayout();

1080
src/pages/styles.less Normal file

File diff suppressed because it is too large Load diff

83
src/pages/utils.js Normal file
View file

@ -0,0 +1,83 @@
/**
* Global variables
*/
let [leftSize, rightSize] = [
localStorage.getItem("leftSize") ?? 0,
localStorage.getItem("rightSize") ?? 0,
];
const parentContainer = document.getElementById("container");
const leftContainer = document.getElementById("left-container");
const closeLeftButton = document.getElementById("close-left");
const rightContainer = document.getElementById("right-container");
const closeRightButton = document.getElementById("close-right");
const roomButton = document.getElementById("MenuButton");
const chatButton = document.getElementById("ChatButton");
leftContainer.classList.toggle("Hidden", !leftSize);
/**
* Calculate the layout of the home page.
*
* Initialize the layout of the home page, the left and right sidebars, the
* rest of the script adds event listeners to the elements in the layout so
* that the layout follows the mouse cursor when clicked and dragged.
*
* @returns {void}
**/
export const calculateLayout = () => {
const parentContainerSize = `${4.5 - leftSize - rightSize}fr`;
// left and right are reversed because of the grid layout
parentContainer.style.gridTemplateColumns = `${leftSize}fr ${parentContainerSize} ${rightSize}fr`;
leftContainer.style.opacity = leftSize;
rightContainer.style.opacity = rightSize;
};
closeLeftButton.addEventListener("click", () => {
leftSize = 1 - leftSize;
localStorage.setItem("leftSize", leftSize);
calculateLayout();
setTimeout(
() => {
leftContainer.classList.toggle("Hidden", true);
},
leftSize ? 0 : 300,
);
});
closeRightButton.addEventListener("click", () => {
rightSize = 1 - rightSize;
localStorage.setItem("rightSize", rightSize);
calculateLayout();
setTimeout(
() => {
rightContainer.classList.toggle("Hidden", true);
},
rightSize ? 0 : 300,
);
});
roomButton.addEventListener("click", () => {
leftSize = 1;
localStorage.setItem("leftSize", leftSize);
calculateLayout();
setTimeout(() => {
leftContainer.classList.toggle("Hidden", false);
}, 300);
});
chatButton.addEventListener("click", () => {
rightSize = 1;
localStorage.setItem("rightSize", rightSize);
calculateLayout();
setTimeout(() => {
rightContainer.classList.toggle("Hidden", false);
}, 300);
});

View file

@ -0,0 +1,7 @@
// FIXME: This file should handle the room canvas API
// Link buttons to their respective functions
// Functions may include:
// - getCanvas (get the canvas of a room and deserialize it)
// - subscribeToRoom (subscribe to the stream of a room)
// - getPixelInfo (get the pixel info of a room)
// - placePixel (place a pixel in a room)

467
src/rooms/canvas/utils.js Normal file
View file

@ -0,0 +1,467 @@
// This file handles the room canvas DOM manipulation
// Functions includes:
// - initCanvas (initialize the canvas)
// - renderCanvasUpdate (render a canvas update)
// - getPlacementData (get the necessary data to place a pixel)
// - toggleTooltip (toggle the tooltip and display the pixel's information)
import $ from "jquery";
const canvasContainer = $("#canvas-container")?.[0];
const canvas = $("#canvas")?.[0];
const canvasCtx = canvas.getContext("2d");
const selector = $("#selector")?.[0];
const positionTooltip = $("#position-tooltip")?.[0];
const tooltip = $("#tooltip")?.[0];
const colorPicker = $("#color-picker")?.[0];
const colorWheelContainer = $("#color-wheel-container")?.[0];
const colorWheel = $("#color-wheel")?.[0];
/**
* Global variables
*/
let board, palette, selectedColorIdx;
let animation;
const zoomSpeed = 1 / 25;
let zoom = 2.5;
let x, y;
let cx = 0;
let cy = 0;
let target = { x: 0, y: 0 };
let isDrag = false;
/**
* Returns the necessary data to place a pixel.
*
* Get the placement data, i.e. the color the user has selected and the
* coordinates of the pixel he is focusing on.
*
* @returns {{color: number, posX: number, posX: number}} the data
*/
export const getPlacementData = () => ({
color: selectedColorIdx,
posX: target.x,
posY: target.y,
});
/**
* Get the currently focused pixel's information and display it in the tooltip.
*
* @param {boolean} [state=state]
*
* @returns {Promise<void>}
*/
export const toggleTooltip = async (state = false) => {
tooltip.style.display = state ? "flex" : "none";
if (state) {
// FIXME: You should implement or call a function to get the pixel's information
// and display it. Make use of target.x and target.y to get the pixel's position.
}
};
/**
* Calculate the target position according to the top left corner of the canvas.
*
* @param {*} event
*
* @returns {x: number, y: number} the target position
*/
const calculateTarget = (event) => {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const canvasLeft = rect.left + window.pageXOffset;
const canvasTop = rect.top + window.pageYOffset;
return {
x: Math.floor(
((event?.pageX ?? window.innerWidth / 2) - canvasLeft) * scaleX,
),
y: Math.floor(
((event?.pageY ?? window.innerHeight / 2) - canvasTop) * scaleY,
),
};
};
/**
* Update the position tooltip according to the event.
*
* @param {*} event
*
* @returns {void}
*/
const positionUpdate = (event) => positionDisplay(calculateTarget(event));
/**
* Update the tooltip's position.
*
* @param {{x: number, y: number}} target
*
* @returns {void}
*/
const positionDisplay = ({ x, y }) => {
positionTooltip.innerText = `X=${x} Y=${y}`;
canvas.style.transform = `translate(${cx}px, ${cy}px) scale(${zoom})`;
// We add the canvas.width * zoom to make cx and cy positive
let selectorX = cx + canvas.width * zoom;
let selectorY = cy + canvas.height * zoom;
// Make odd canvas align
if (canvas.width % 2 !== 0) {
selectorX += zoom / 2;
selectorY += zoom / 2;
}
// Find the translate
selectorX %= zoom;
selectorY %= zoom;
// Center selector on the pixel
selectorX -= zoom / 2;
selectorY -= zoom / 2;
selector.style.transform = `translate(${selectorX}px, ${selectorY}px) scale(${zoom})`;
};
// Toggle the color wheel on click on the color picker
colorPicker.addEventListener("click", () => {
const state = colorWheelContainer.style.display;
colorWheelContainer.style.display =
!state || state === "none" ? "block" : "none";
});
/**
* Transform #RRGGBB to 0xBBGGRRAA. Hexadecimal color to 32 bits integer.
*
* @param {string} hex
*
* @returns {number} the 32 bits color
*/
const transformHexTo32Bits = (hex) => {
const reverse = hex.substring(1).match(/.{2}/g).reverse().join("");
return parseInt(`0xFF${reverse}`, 16);
};
/**
* Render the canvas.
*
* @param {number[]} pixels
* @param {string[]} colors
*
* @returns {void}
*/
const renderCanvas = (pixels, colors) => {
const img = new ImageData(canvas.width, canvas.height);
const data = new Uint32Array(img.data.buffer);
board = pixels;
palette = colors;
for (let i = 0; i < pixels.length; i++) {
data[i] = transformHexTo32Bits(colors[pixels[i]]);
}
canvasCtx.putImageData(img, 0, 0);
canvasCtx.imageSmoothingEnabled = false;
canvas.style.imageRendering = "pixelated";
// Remove all the colors from the color wheel
while (colorWheel.firstChild) {
colorWheel.removeChild(colorWheel.firstChild);
}
// Add the colors to the color wheel
for (let i = 0; i < colors.length; i++) {
const btn = document.createElement("button");
colorWheel.appendChild(btn);
btn.addEventListener("click", () => {
selectedColorIdx = i;
colorPicker.style.color = colors[i];
colorPicker.style.border = `${colors[i]} 0.1rem solid`;
});
btn.style.backgroundColor = colors[i];
}
};
/**
* Initialize the canvas with the given room configuration and pixels.
*
* @param {*} roomConfig
* @param {number[]} pixels
*
* @returns {void}
*/
export const initCanvas = (roomConfig, pixels) => {
const canvasDimensions = roomConfig.metadata.canvasDimensions;
canvas.width = canvasDimensions;
canvas.height = canvasDimensions;
positionDisplay({ x: canvasDimensions / 2, y: canvasDimensions / 2 });
selectedColorIdx = 0;
const roomColors = roomConfig.settings.roomColors.split(",");
colorPicker.style.color = roomColors[0];
colorPicker.style.border = `${roomColors[0]} 0.1rem solid`;
renderCanvas(pixels, roomColors);
};
/**
* Render the canvas update, i.e. update the pixel at the given coordinates.
*
* @param {string} color
* @param {number} x
* @param {number} y
*
* @returns {void}
*/
export const renderCanvasUpdate = (color, x, y) => {
const img = new ImageData(canvas.width, canvas.height);
const data = new Uint32Array(img.data.buffer);
board[y * canvas.width + x] = color;
for (let i = 0; i < board.length; i++) {
data[i] = transformHexTo32Bits(palette[board[i]]);
}
canvasCtx.putImageData(img, 0, 0);
};
/**
* Reset the values of the canvas, i.e. the zoom, the coordinates and the
* display position.
*
* @returns {void}
*/
export const resetValues = () => {
zoom = 2.5;
x = 0;
y = 0;
cx = 0;
cy = 0;
isDrag = false;
positionDisplay({ x, y });
colorWheelContainer.style.display = "none";
toggleTooltip(false);
};
// Handle scroll on canvas
document.addEventListener("wheel", (e) => {
// Make sure we're scrolling on the canvas or the body and not the UI
if (e.target !== canvas && e.target !== canvasContainer) {
return;
}
clearInterval(animation);
toggleTooltip(false);
const delta = Math.sign(e.deltaY) * zoomSpeed;
const zoomFactor = 1 + delta;
const oldZoom = zoom;
const newZoom = Math.max(2.5, Math.min(40, oldZoom * zoomFactor));
// Get the position of the mouse relative to the canvas
const mouseX = e.clientX - window.innerWidth / 2;
const mouseY = e.clientY - window.innerHeight / 2;
// Calculate the new center point based on the mouse position
const newCx = mouseX - (mouseX - cx) * (newZoom / oldZoom);
const newCy = mouseY - (mouseY - cy) * (newZoom / oldZoom);
if (newZoom !== oldZoom) {
zoom = newZoom;
cx = newCx;
cy = newCy;
positionUpdate();
}
});
// Handle click and drag on canvas
document.addEventListener("mousedown", (e) => {
// Make sure we're clicking on the canvas or the body and not the UI
if (e.target !== canvas && e.target !== canvasContainer) {
return;
}
e.preventDefault();
// Ignore if right click
if (e.button === 2) {
return;
}
clearInterval(animation);
isDrag = false;
x = e.clientX;
y = e.clientY;
document.addEventListener("mousemove", mouseMove);
});
// Smooth animation
function easeOutQuart(t, b, c, d) {
t /= d;
t--;
return -c * (t * t * t * t - 1) + b;
}
// Handle when the user releases the mouse
document.addEventListener("mouseup", (e) => {
document.removeEventListener("mousemove", mouseMove);
// Make sure we're clicking on the canvas or the body and not the UI
if (e.target !== canvas && e.target !== canvasContainer) {
return;
}
e.preventDefault();
// Get the tile position
target = calculateTarget(e);
// Make sure we're clicking on the canvas
if (
target.x >= 0 &&
target.x < canvas.width &&
target.y >= 0 &&
target.y < canvas.height
) {
// We want to differentiate between a click and a drag
// If it is a click, we want to move the camera to the clicked tile
// We wait to see if the position changed
// If it did not, we consider it a click
if (!isDrag) {
const duration = 1000;
const startZoom = zoom;
const endZoom = Math.max(15, Math.min(40, zoom));
// Get the position of the click in relation to the center of the screen
const clickX = e.clientX - window.innerWidth / 2;
const clickY = e.clientY - window.innerHeight / 2;
const canvaswidthzoom = canvas.width * startZoom;
const canvasheightzoom = canvas.height * startZoom;
const startx = (cx + canvaswidthzoom / 2) / startZoom;
const starty = (cy + canvasheightzoom / 2) / startZoom;
const endx = startx - clickX / startZoom;
const endy = starty - clickY / startZoom;
const endCx = endx * endZoom - (canvas.width / 2) * endZoom;
const endCy = endy * endZoom - (canvas.height / 2) * endZoom;
const startCx = cx;
const startCy = cy;
const startTime = Date.now();
// If the distance is small enough, we just warp to it
if (
Math.abs(endCx - startCx) < 10 &&
Math.abs(endCy - startCy) < 10
) {
cx = endCx;
cy = endCy;
zoom = endZoom;
canvas.style.transform = `translate(${cx}px, ${cy}px) scale(${zoom})`;
} else {
clearInterval(animation);
animation = setInterval(() => {
const elapsed = Date.now() - startTime;
if (elapsed >= duration) {
clearInterval(animation);
return;
}
const t = elapsed / duration;
zoom = easeOutQuart(t, startZoom, endZoom - startZoom, 1);
cx = easeOutQuart(t, startCx, endCx - startCx, 1);
cy = easeOutQuart(t, startCy, endCy - startCy, 1);
positionUpdate();
}, 10);
}
}
// Toggle the tooltip if it is a click
toggleTooltip(!isDrag);
// Update the position of the tooltip
positionDisplay(target);
}
});
// Handle mouse move
const mouseMove = (e) => {
e.preventDefault();
toggleTooltip(false);
positionUpdate();
const dx = e.clientX - x;
const dy = e.clientY - y;
// For a big enough delta, we consider it a drag
if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5) {
isDrag = true;
}
x = e.clientX;
y = e.clientY;
cx += dx;
cy += dy;
canvas.style.transform = `translate(${cx}px, ${cy}px) scale(${zoom})`;
};
export const displayLeaderboard = (guilds) => {
console.log(guilds);
const list = document.getElementById("leaderboard-list");
for (const { name, points } of guilds) {
console.log(`${name} has ${points}`);
const existingItem = [
...list.querySelectorAll(".LeaderboardItem"),
].find((el) => el.dataset.guild === name);
if (existingItem) {
existingItem.querySelector(".GuildPoints").textContent = points;
existingItem.dataset.points = points;
} else {
const li = document.createElement("li");
li.className = "LeaderboardItem";
li.dataset.guild = name;
li.dataset.points = points;
li.innerHTML = `
<span class="Rank"></span>
<span class="GuildName">${name}</span>
<span class="GuildPoints">${points}</span>
`;
list.appendChild(li);
}
}
const items = [...list.querySelectorAll(".LeaderboardItem")].sort(
(a, b) => b.dataset.points - a.dataset.points,
);
list.innerHTML = "";
items.slice(0, 5).forEach((item, index) => {
item.className = `LeaderboardItem rank-${index + 1}`;
item.querySelector(".Rank").textContent = index + 1;
list.appendChild(item);
});
};

4
src/rooms/chat/index.js Normal file
View file

@ -0,0 +1,4 @@
// FIXME: This file should handle the room's chat subscription
// Functions may include:
// - subscribeToRoomChat (subscribe to the chat of a room)
// - sendChatMessage (send a chat message)

6
src/rooms/chat/utils.js Normal file
View file

@ -0,0 +1,6 @@
// FIXME: This file should handle the room's chat DOM manipulation
// Link buttons to their respective functions
// Handle the chat input form and its submission
// Functions may include:
// - displayChatMessage (display a chat message in the DOM)
// - displayUserEvents (display a user event in the DOM)

10
src/rooms/index.js Normal file
View file

@ -0,0 +1,10 @@
// FIXME: This file should handle the rooms API
// Functions may include:
// - fetchRoomConfig (get the configuration of a room)
// - setCurrentRoomConfig (set the current room configuration and update the DOM accordingly)
// - getCurrentRoomConfig (get the current room configuration)
// - joinRoom (join a room by its slug)
// - listRooms (list all the rooms available)
// - createRoom (create a room)
// - updateRoom (update a room's configuration)
// - deleteRoom (delete a room)

6
src/rooms/utils.js Normal file
View file

@ -0,0 +1,6 @@
// FIXME: This file should handle the rooms DOM manipulation
// Link buttons to their respective functions
// Functions may include:
// - showModal (add a form modal to the DOM)
// - createRoomObject (create a room in the DOM)
// - displayRoomsList (display the rooms list in the DOM)

5
src/students/index.js Normal file
View file

@ -0,0 +1,5 @@
// FIXME: This file should handle the students API
// Functions may include:
// - getStudent (get a student from the API by its uid or login)
// - getUserUidFromToken (get the user's uid from the token in local storage)
// - updateStudent (update the student's profile through the API)

5
src/students/utils.js Normal file
View file

@ -0,0 +1,5 @@
// FIXME: This file should handle the students DOM manipulation
// Link buttons to their respective functions
// Functions may include:
// - displayStudentProfile (display the student's profile in the DOM)
// - showModal (add a form modal to the DOM)

6
src/utils/auth.js Normal file
View file

@ -0,0 +1,6 @@
// FIXME: This file should handle the authentication
// Functions may include:
// - getToken (exchanges the code for a token)
// - refreshToken (refreshes the token using the refresh_token)
// - authenticate (checks if the user is authenticated)
// - authedAPIRequest (makes an authenticated request to the API)

64
src/utils/notify.js Normal file
View file

@ -0,0 +1,64 @@
import $ from "jquery";
import alertHtml from "../components/notifications/index.html";
const iconMap = {
info: "fa-info-circle",
success: "fa-thumbs-up",
warning: "fa-exclamation-triangle",
error: "ffa fa-exclamation-circle",
};
/**
* Create an alert.
*
* Create an alert with the given title, message and type.
* The alert will display in the top right corner of the screen.
* This is a useful function to notify the user of any errors or warnings.
*
* @param {string} title
* @param {string} message
* @param {string} type success, warning, error
*
* @returns {void}
**/
export const createAlert = (title, message, type) => {
const alertContainer = $("#alert-container")?.[0];
$.ajax({
url: alertHtml,
success: (data) => {
const [alert] = $(data);
// Return if the alert cannot be created, usefull when a redirect is made
if (!alertContainer || !alert || !alert.classList) {
return;
}
// Add the type class to the alert
alert.classList.add(
`Alert${type.charAt(0).toUpperCase() + type.slice(1)}`,
);
// Replace values in innerHTML
alert.innerHTML = alert.innerHTML
.replace(/{{title}}/g, title)
.replace(/{{content}}/g, message)
.replace(/{{icon_classes}}/g, iconMap[type]);
// Get the close button
const closeBtn = alert.getElementsByClassName("AlertClose")?.[0];
closeBtn?.addEventListener("click", () => {
alert.remove();
});
// Append the alert to the container
alertContainer.append(alert);
// Remove the alert after 5 seconds
setTimeout(() => {
alert.remove();
}, 5000);
},
});
};

3
src/utils/rateLimits.js Normal file
View file

@ -0,0 +1,3 @@
// FIXME: This file should handle the rate limits
// Functions may include:
// - displayTimer (util function to display the timer for the rate limit)

4
src/utils/redirect.js Normal file
View file

@ -0,0 +1,4 @@
// FIXME: This file should handle the redirection to the AUTH URL
// Functions may include:
// - createLink (construct and return the URL to redirect the user to the login page)
// - redirectToLoginPage (redirect the user to the Forge ID login page)

9
src/utils/streams.js Normal file
View file

@ -0,0 +1,9 @@
// FIXME: This file should handle the sockets and the subscriptions
// Exports must include
// - initSocket (initialize the connection to the socket server)
// - socket (variable resulting of initSocket function)
// Functions may include:
// - subscribe (subscribe to a room's stream or chat)
// - unsubscribe (unsubscribe from a room's stream or chat)
// - sendMessage (send a message to a room's chat)

44
vite.config.js Normal file
View file

@ -0,0 +1,44 @@
import { resolve } from "path";
import { defineConfig, loadEnv } from "vite";
import dns from "dns";
dns.setDefaultResultOrder("verbatim");
const root = resolve(__dirname, "src/pages/");
export default ({ mode }) => {
process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };
return defineConfig({
root,
server: {
host: process.env.VITE_HOST,
port: process.env.VITE_PORT,
proxy: {
// $VITE_URL/api* -> $VITE_API_URL/api*
"/api": {
target: process.env.VITE_API_URL,
changeOrigin: true,
},
// $VITE_URL/socket.io* -> $VITE_API_URL/socket.io*
"/socket.io": {
target: process.env.VITE_API_URL,
changeOrigin: true,
ws: true,
},
// $VITE_URL/auth-api* -> $VITE_AUTH_URL*
"/auth-api": {
target: process.env.VITE_AUTH_URL,
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/auth-api/, ""),
},
},
},
publicDir: resolve(__dirname, "public"),
assetsInclude: [
"src/components/**/*.html",
"src/pages/debug/debug.html",
],
});
};

1957
yarn.lock Normal file

File diff suppressed because it is too large Load diff