summaryrefslogtreecommitdiff
path: root/rushs/eplace/src
diff options
context:
space:
mode:
authorMartial Simon <msimon_fr@hotmail.com>2025-09-15 01:08:27 +0200
committerMartial Simon <msimon_fr@hotmail.com>2025-09-15 01:08:27 +0200
commitc9b6b9a5ca082fe7c1b6f58d7713f785a9eb6a5c (patch)
tree3e4f42f93c7ae89a364e4d51fff6e5cec4e55fa9 /rushs/eplace/src
add: graphs et rushs
Diffstat (limited to 'rushs/eplace/src')
-rw-r--r--rushs/eplace/src/components/debug.html30
-rw-r--r--rushs/eplace/src/components/notifications/index.html12
-rw-r--r--rushs/eplace/src/components/rooms/index.html10
-rw-r--r--rushs/eplace/src/components/rooms/message.html8
-rw-r--r--rushs/eplace/src/components/rooms/upsert.html70
-rw-r--r--rushs/eplace/src/components/rooms/user-event.html3
-rw-r--r--rushs/eplace/src/components/students/update.html31
-rw-r--r--rushs/eplace/src/pages/complete/epita/index.html12
-rw-r--r--rushs/eplace/src/pages/complete/epita/index.js53
-rw-r--r--rushs/eplace/src/pages/debug.js34
-rw-r--r--rushs/eplace/src/pages/index.css78
-rw-r--r--rushs/eplace/src/pages/index.html173
-rw-r--r--rushs/eplace/src/pages/index.js10
-rw-r--r--rushs/eplace/src/pages/styles.less905
-rw-r--r--rushs/eplace/src/pages/utils.js95
-rw-r--r--rushs/eplace/src/rooms/canvas/index.js96
-rw-r--r--rushs/eplace/src/rooms/canvas/utils.js432
-rw-r--r--rushs/eplace/src/rooms/chat/index.js4
-rw-r--r--rushs/eplace/src/rooms/chat/utils.js6
-rw-r--r--rushs/eplace/src/rooms/index.js50
-rw-r--r--rushs/eplace/src/rooms/utils.js6
-rw-r--r--rushs/eplace/src/students/index.js19
-rw-r--r--rushs/eplace/src/students/utils.js5
-rw-r--r--rushs/eplace/src/utils/auth.js124
-rw-r--r--rushs/eplace/src/utils/notify.js57
-rw-r--r--rushs/eplace/src/utils/rateLimits.js3
-rw-r--r--rushs/eplace/src/utils/streams.js108
27 files changed, 2434 insertions, 0 deletions
diff --git a/rushs/eplace/src/components/debug.html b/rushs/eplace/src/components/debug.html
new file mode 100644
index 0000000..750906d
--- /dev/null
+++ b/rushs/eplace/src/components/debug.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <link rel="stylesheet" type="text/css" media="screen" href="index.css" />
+</head>
+
+<body>
+ <div class="container">
+ <h1>Local storage</h1>
+
+ <h3>Token</h3>
+ <p id="token" class="token-text">
+ {{ token }}
+ </p>
+ <h3>Refresh Token</h3>
+ <p id="refresh_token" class="token-text">
+ {{ refresh_token }}
+ </p>
+
+ <button id="errorBtn">Generate an error response</button>
+ <button id="expiredTokenBtn">Generate an expired token response</button>
+ <button id="deleteTokenBtn">Delete token</button>
+ <button id="deleteRefreshTokenBtn">Delete refresh token</button>
+ </div>
+</body>
+
+</html>
diff --git a/rushs/eplace/src/components/notifications/index.html b/rushs/eplace/src/components/notifications/index.html
new file mode 100644
index 0000000..c211a39
--- /dev/null
+++ b/rushs/eplace/src/components/notifications/index.html
@@ -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>
diff --git a/rushs/eplace/src/components/rooms/index.html b/rushs/eplace/src/components/rooms/index.html
new file mode 100644
index 0000000..3d9f20e
--- /dev/null
+++ b/rushs/eplace/src/components/rooms/index.html
@@ -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>
diff --git a/rushs/eplace/src/components/rooms/message.html b/rushs/eplace/src/components/rooms/message.html
new file mode 100644
index 0000000..1c35e20
--- /dev/null
+++ b/rushs/eplace/src/components/rooms/message.html
@@ -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>
diff --git a/rushs/eplace/src/components/rooms/upsert.html b/rushs/eplace/src/components/rooms/upsert.html
new file mode 100644
index 0000000..8b02b69
--- /dev/null
+++ b/rushs/eplace/src/components/rooms/upsert.html
@@ -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>
diff --git a/rushs/eplace/src/components/rooms/user-event.html b/rushs/eplace/src/components/rooms/user-event.html
new file mode 100644
index 0000000..3aec718
--- /dev/null
+++ b/rushs/eplace/src/components/rooms/user-event.html
@@ -0,0 +1,3 @@
+<div class="ChatUserEvent">
+ <span class="MessageContent">{{message_content}}</span>
+</div>
diff --git a/rushs/eplace/src/components/students/update.html b/rushs/eplace/src/components/students/update.html
new file mode 100644
index 0000000..912526e
--- /dev/null
+++ b/rushs/eplace/src/components/students/update.html
@@ -0,0 +1,31 @@
+<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="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>
diff --git a/rushs/eplace/src/pages/complete/epita/index.html b/rushs/eplace/src/pages/complete/epita/index.html
new file mode 100644
index 0000000..a6ebe75
--- /dev/null
+++ b/rushs/eplace/src/pages/complete/epita/index.html
@@ -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>
diff --git a/rushs/eplace/src/pages/complete/epita/index.js b/rushs/eplace/src/pages/complete/epita/index.js
new file mode 100644
index 0000000..ced46cf
--- /dev/null
+++ b/rushs/eplace/src/pages/complete/epita/index.js
@@ -0,0 +1,53 @@
+// FIXME: This file should handle the auth redirection
+
+// Get the code from the URL parameters and redirect to the relevant page
+const params = new URLSearchParams(window.location.search);
+
+const code = params.get("code");
+const form = new FormData();
+
+form.append("client_id", import.meta.env.VITE_CLIENT_ID);
+form.append("redirect_uri", `${import.meta.env.VITE_URL}/complete/epita/`);
+form.append("grant_type", "authorization_code");
+form.append("code", code);
+const config = {
+ method: "POST",
+ body: form,
+};
+
+export async function getToken() {
+ try {
+ const res = await fetch(
+ `${import.meta.env.VITE_URL}/auth-api/token`,
+ config,
+ );
+ const response = await res.json();
+
+ const token = response.id_token;
+
+ localStorage.setItem("token", token);
+ localStorage.setItem("refresh_token", response.refresh_token);
+ window.location.replace(import.meta.env.VITE_URL);
+ return true;
+ } catch (error) {
+ console.log(error);
+ return false;
+ }
+}
+
+if (!code) {
+ const authQueryParams = {
+ client_id: import.meta.env.VITE_CLIENT_ID,
+ scope: "epita profile picture",
+ redirect_uri: `${import.meta.env.VITE_URL}/complete/epita/`,
+ response_type: "code",
+ };
+ const url = new URL(
+ `?client_id=${authQueryParams.client_id}&scope=${authQueryParams.scope}&redirect_uri=${authQueryParams.redirect_uri}&response_type=${authQueryParams.response_type}`,
+ `${import.meta.env.VITE_AUTH_URL}/authorize`,
+ );
+
+ window.location.replace(url);
+} else {
+ getToken();
+}
diff --git a/rushs/eplace/src/pages/debug.js b/rushs/eplace/src/pages/debug.js
new file mode 100644
index 0000000..e2da5f4
--- /dev/null
+++ b/rushs/eplace/src/pages/debug.js
@@ -0,0 +1,34 @@
+import $ from "jquery";
+import debugHtml from "../components/debug.html";
+
+function refreshLocalStorage() {
+ $("#token").text(localStorage.getItem("token") ?? "N/A");
+ $("#refresh_token").text(localStorage.getItem("refresh_token") ?? "N/A");
+}
+
+if (import.meta.env.MODE === "debug") {
+ $.get(debugHtml, function (response) {
+ $("body").html(response);
+ refreshLocalStorage();
+ }).fail(function (xhr, status, error) {
+ console.error("Error fetching debug HTML:", error);
+ });
+
+ $(document).on("click", "#errorBtn", function () {
+ // Make a call to VITE_URL/tests/error using your own api call function
+ });
+
+ $(document).on("click", "#expiredTokenBtn", function () {
+ // Make a call to VITE_URL/tests/expired using your own api call function
+ });
+
+ $(document).on("click", "#deleteTokenBtn", function () {
+ localStorage.removeItem("token");
+ refreshLocalStorage();
+ });
+
+ $(document).on("click", "#deleteRefreshTokenBtn", function () {
+ localStorage.removeItem("refresh_token");
+ refreshLocalStorage();
+ });
+}
diff --git a/rushs/eplace/src/pages/index.css b/rushs/eplace/src/pages/index.css
new file mode 100644
index 0000000..6548524
--- /dev/null
+++ b/rushs/eplace/src/pages/index.css
@@ -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;
+}
diff --git a/rushs/eplace/src/pages/index.html b/rushs/eplace/src/pages/index.html
new file mode 100644
index 0000000..d52232b
--- /dev/null
+++ b/rushs/eplace/src/pages/index.html
@@ -0,0 +1,173 @@
+<!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" src="index.js"></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="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>
+ <button class="ReportButton">Report Abuse</button>
+ </div>
+ </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="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>
diff --git a/rushs/eplace/src/pages/index.js b/rushs/eplace/src/pages/index.js
new file mode 100644
index 0000000..59cdb91
--- /dev/null
+++ b/rushs/eplace/src/pages/index.js
@@ -0,0 +1,10 @@
+// FIXME: This is the entry point of the application, write your code here
+
+import { calculateLayout } from "./utils";
+import "./debug";
+import { initSocket } from "../utils/streams";
+
+// Initialize the layout
+calculateLayout();
+
+initSocket();
diff --git a/rushs/eplace/src/pages/styles.less b/rushs/eplace/src/pages/styles.less
new file mode 100644
index 0000000..dd8ae95
--- /dev/null
+++ b/rushs/eplace/src/pages/styles.less
@@ -0,0 +1,905 @@
+@base: rgba(16, 18, 60, 0.95);
+@dark: #05061a;
+@light: #f0f0f0;
+@accent: #ff4603;
+
+.App {
+ background-color: @dark;
+ min-height: 100vh;
+ margin: 0 auto;
+ overflow: hidden;
+ padding: 0rem;
+ position: relative;
+ text-align: center;
+ width: 100vw;
+}
+
+.AlertContainer {
+ position: absolute;
+ top: 0;
+ right: 0;
+ display: flex;
+ flex-direction: column-reverse;
+ align-items: flex-end;
+ justify-content: flex-end;
+ gap: 1rem;
+ padding: 2rem;
+ z-index: 3;
+ pointer-events: none;
+
+ .Alert {
+ position: relative;
+ background-color: #fff;
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ padding: 0.5rem;
+ width: 100%;
+ pointer-events: all;
+
+ box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px;
+
+ .Icon {
+ font-size: 28px;
+ }
+
+ .AlertBody {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: center;
+ margin: 0.5rem 2rem 0.5rem 0;
+ text-align: left;
+ font-family: Barlow;
+ color: @dark;
+
+ .AlertTitle {
+ font-size: 1rem;
+ font-weight: 800;
+ text-transform: uppercase;
+ }
+ .AlertContent {
+ font-size: 0.75rem;
+ opacity: 50%;
+ }
+ }
+
+ .AlertClose {
+ position: absolute;
+ top: 0.5rem;
+ right: 0.5rem;
+ font-size: 1em;
+ color: @dark;
+ }
+ }
+
+ .AlertSuccess {
+ border-left: 5px solid #49d761;
+
+ .Icon {
+ color: #49d761;
+ }
+ }
+ .AlertWarning {
+ border-left: 5px solid #ff9f1c;
+
+ .Icon {
+ color: #ff9f1c;
+ }
+ }
+ .AlertError {
+ border-left: 5px solid #fd2020;
+
+ .Icon {
+ color: #fd2020;
+ }
+ }
+}
+
+.Container {
+ column-gap: 2em;
+ display: grid;
+ flex-direction: row;
+ grid-auto-flow: row dense;
+ // Fully openned sidebars layout
+ // -> grid-template-columns: 1fr 2.5fr 1fr;
+ // Closed sidebars layout
+ grid-template-columns: 0fr 2.5fr 0fr;
+ height: calc(100vh - 4rem);
+ overflow: hidden;
+ padding: 2rem;
+
+ transition: all 0.5s ease-in-out;
+}
+
+.RoomList {
+ background-color: @base;
+ border-radius: 0.5rem;
+ display: grid;
+ flex-grow: 0;
+ gap: 0.5em 0em;
+ grid-template-rows: 1fr 8.25fr 0.75fr;
+ overflow: hidden;
+ padding: 1.5rem;
+ z-index: 1;
+ // Fully openned sidebar
+ // -> opacity: 1;
+ // Closed sidebar
+ opacity: 0;
+
+ transition: opacity 0.5s ease-in-out;
+
+ .Header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ overflow: hidden;
+
+ .Title {
+ color: @light;
+ font-family: Barlow;
+ font-size: 3rem;
+ font-weight: 800;
+ grid-row-start: 1;
+ overflow: hidden;
+ text-align: start;
+ text-overflow: ellipsis;
+ text-transform: uppercase;
+ white-space: nowrap;
+ }
+ }
+
+ .ListContainer {
+ display: flex;
+ flex-direction: column;
+ gap: 1em;
+ margin: 1rem 0rem;
+ overflow: auto;
+ padding: 0rem;
+ }
+
+ .ListSearchBar {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5em 0em;
+
+ .FilterContainer {
+ background-color: rgba(black, 0.5);
+ align-items: center;
+ display: flex;
+ font-family: Barlow;
+ font-weight: 800;
+ font-size: 0.75rem;
+ gap: 0em 1em;
+ overflow: hidden;
+ text-align: start;
+ white-space: nowrap;
+ padding: 0.5rem;
+ border-radius: 0.5rem;
+
+ .FilterText {
+ margin-left: 0.5rem;
+ opacity: 0.5;
+ font-size: 0.8rem;
+ }
+
+ .FilterHeader {
+ display: flex;
+ font-family: Barlow;
+ font-weight: 800;
+ overflow: scroll;
+ scrollbar-width: none;
+ text-align: start;
+ gap: 0.5em;
+
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ &::-webkit-scrollbar {
+ display: none;
+ }
+
+ .Filter {
+ background-color: rgba(@light, 0.25);
+ border-radius: 0.5rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5em;
+ padding: 0.25rem 0.5rem;
+ font-size: 0.65rem;
+ }
+ }
+ }
+ }
+
+ .RoomsContainer {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ grid-row-start: 2;
+ overflow: scroll;
+
+ .Room {
+ background-color: rgba(black, 0.25);
+ border-radius: 0.5rem;
+ display: flex;
+ gap: 1em;
+ align-items: center;
+ padding: 0.5rem;
+
+ &:disabled {
+ border: @accent 0.15rem solid;
+ cursor: not-allowed;
+ }
+
+ .Avatar {
+ height: 4rem;
+ width: 4rem;
+ object-fit: cover;
+ aspect-ratio: 1/1;
+ }
+
+ .TextContainer {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 0.5em;
+ color: @light;
+ overflow: hidden;
+
+ .Name {
+ font-family: Barlow;
+ font-size: 1rem;
+ font-weight: 800;
+ text-align: start;
+ text-transform: uppercase;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+
+ .RoomPrivacy {
+ margin-right: 0.25rem;
+ }
+ }
+
+ .RoomOwner {
+ font-family: Montserrat;
+ font-size: 0.65rem;
+ font-weight: 700;
+ text-align: start;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+ }
+
+ .ListRoomDivider {
+ background-color: rgba(@light, 0.25);
+ height: 0.1rem;
+ margin: 0.5rem 0rem;
+ }
+ }
+
+ .StudentProfile {
+ align-items: center;
+ background-color: @light;
+ border-radius: 5rem;
+ column-gap: 0.5em;
+ display: flex;
+ flex-direction: row;
+ grid-row-start: 3;
+ overflow: hidden;
+ padding: 0.5rem;
+
+ .Avatar {
+ border-radius: 100%;
+ max-height: 3rem;
+ max-width: 3rem;
+ object-fit: cover;
+ aspect-ratio: 1/1;
+ }
+
+ .TextContainer {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ margin-right: 0.5rem;
+ overflow: hidden;
+ color: black;
+ text-align: start;
+ overflow: hidden;
+
+ .Login {
+ font-family: Barlow;
+ font-size: 1rem;
+ font-weight: 800;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .Quote {
+ font-family: Montserrat;
+ font-size: 0.7rem;
+ font-weight: 700;
+ opacity: 65%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+
+ .ModifyProfileButton {
+ background-color: transparent;
+ color: @dark;
+ padding: 0.25rem;
+ margin-left: auto;
+ margin-right: 0.5rem;
+ border-radius: 100%;
+ }
+ }
+}
+
+.RoomCanvas {
+ display: grid;
+ flex-direction: column;
+ flex-grow: 0;
+ grid-template-rows: 0.25fr 9.75fr;
+ min-width: 50vw;
+ row-gap: 0em;
+ overflow: hidden;
+
+ .Header {
+ background-color: @base;
+ border-radius: 0.5rem;
+ align-items: center;
+ justify-content: center;
+ display: flex;
+ flex-direction: row;
+ gap: 1.5rem;
+ grid-row-start: 1;
+ padding: 0.5rem;
+ z-index: 1;
+
+ .TextContainer {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ text-align: end;
+
+ .Title {
+ color: @light;
+ font-family: Barlow;
+ font-size: 2rem;
+ font-weight: 800;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ text-transform: uppercase;
+ white-space: nowrap;
+ }
+
+ .Description {
+ // Start with the description hidden.
+ // It needs to be visible if the room has a description.
+ display: none;
+ position: relative;
+ top: -0.25rem;
+
+ color: @light;
+ font-family: Montserrat;
+ font-size: 0.5rem;
+ font-weight: 700;
+ opacity: 0.5;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+
+ .HeaderDivider {
+ background-color: rgba(@light, 0.25);
+ height: 100%;
+ margin: 0.5rem 0rem;
+ width: 0.1rem;
+ }
+
+ .ButtonContainer {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+
+ .ReportButton {
+ background-color: @accent;
+ border-radius: 0.5rem;
+ border: 0px solid transparent;
+ color: @light;
+ display: block;
+ font-family: Montserrat;
+ font-size: 0.75rem;
+ font-weight: 700;
+ height: auto;
+ padding: 0.35rem 0.75rem;
+ text-transform: uppercase;
+ }
+ }
+ }
+
+ .CanvasContainer {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ grid-row-start: 2;
+ overflow: hidden;
+ z-index: 0;
+
+ .Canvas {
+ transform: translate(0, 0) scale(2.5);
+ }
+
+ .Selector {
+ position: absolute;
+ inset: 0;
+ width: 1px;
+ height: 1px;
+ margin: auto;
+ pointer-events: none;
+ }
+
+ .Tooltip {
+ // The tooltip is a flexbox
+ // -> display: flex;
+ // Start with the tooltip hidden
+ display: none;
+
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ transform: translate(-50%, -150%);
+ background-color: @base;
+ border-radius: 0.5rem;
+ border: @accent 0.15rem solid;
+ padding: 0.5rem;
+
+ .Header {
+ display: flex;
+ gap: 0.75rem;
+
+ .PlacedByInfo {
+ position: relative;
+ display: inline-block;
+ height: 3rem;
+ width: 3rem;
+
+ .Avatar {
+ border-radius: 100%;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ aspect-ratio: 1/1;
+ }
+
+ .Profile {
+ visibility: hidden;
+ background-color: @dark;
+ color: @light;
+ text-align: center;
+ padding: 0.5rem;
+ border-radius: 0.5rem;
+
+ bottom: 100%;
+ left: 50%;
+ transform: translate(-50%, 0);
+ position: absolute;
+ z-index: 1;
+
+ &::after {
+ content: " ";
+ position: absolute;
+ top: 100%;
+ left: 50%;
+ margin-left: -5px;
+ border-width: 5px;
+ border-style: solid;
+ border-color: @dark transparent transparent transparent;
+ }
+
+ .Login {
+ font-family: Barlow;
+ font-size: 0.8rem;
+ font-weight: 800;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .Quote {
+ font-family: Montserrat;
+ font-size: 0.7rem;
+ font-weight: 700;
+ opacity: 0.5;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+
+ &:hover .Profile {
+ visibility: visible;
+ }
+ }
+
+ .TextContainer {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ font-family: Montserrat;
+ font-size: 0.75rem;
+ font-weight: 700;
+ text-align: left;
+ }
+ }
+
+ .ButtonContainer {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 0.5rem;
+ width: 100%;
+ font-family: Montserrat;
+ font-size: 0.75rem;
+ font-weight: 700;
+
+ .ColorPicker {
+ padding: 0.2rem;
+ outline: none;
+ color: white;
+ border: white 0.1rem solid;
+ }
+
+ .PlaceButton {
+ padding: 0.2rem;
+ border: @accent 0.1rem solid;
+ border-radius: 0.5rem;
+ outline: none;
+ font-family: Barlow;
+ font-size: 0.75rem;
+ font-weight: 800;
+
+ &:active {
+ background-color: @accent;
+ color: @light;
+ }
+
+ &:disabled {
+ background-color: @dark;
+ color: @accent;
+ }
+ }
+ }
+ }
+ }
+
+ .PositionTooltip {
+ background-color: rgba(@dark, 0.25);
+ padding: 0.5rem;
+ margin: 0.5rem auto auto 0;
+ border-radius: 0.5rem;
+ font-family: Montserrat;
+ font-weight: 700;
+ z-index: 2;
+ }
+
+ .ColorWheelContainer {
+ // The color wheel is a flexbox
+ // -> display: block;
+ // Start with the color wheel hidden
+ display: none;
+
+ background-color: @base;
+ margin: auto 0 0 auto;
+ padding: 0.5rem;
+ max-height: calc(100% - 12rem);
+ overflow: scroll;
+ border-radius: 1rem;
+ border: @accent 0.15rem solid;
+ z-index: 1;
+
+ min-block-size: -webkit-fill-available;
+
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ &::-webkit-scrollbar {
+ display: none;
+ }
+
+ transition: all 0.5s ease-in-out;
+
+ .ColorWheel {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ width: 100%;
+ margin: 0.25rem 0;
+ }
+ }
+}
+
+.RoomChat {
+ display: grid;
+ flex-grow: 0;
+ grid-auto-flow: row;
+ grid-template-rows: 1fr 8fr;
+ overflow: hidden;
+ padding: 0rem;
+ z-index: 1;
+ // Fully openned sidebar
+ // -> opacity: 1;
+ // Closed sidebar
+ opacity: 0;
+
+ transition: opacity 0.5s ease-in-out;
+
+ .ChatContainer {
+ background-color: @base;
+ border-radius: 0.5rem;
+ display: grid;
+ gap: 1em;
+ grid-row-start: 2;
+ grid-template-rows: 0.5fr 6.75fr 0.75fr;
+ grid-template-columns: 1fr;
+ overflow: hidden;
+ padding: 0.8rem;
+
+ .Header {
+ display: flex;
+ flex-direction: row-reverse;
+ align-items: center;
+ }
+
+ .ChatMessageContainer {
+ display: flex;
+ flex-direction: column-reverse;
+ overflow: auto;
+ padding: 0rem;
+ row-gap: 1em;
+
+ .ChatMessage {
+ color: @light;
+ background-color: rgba(black, 0.25);
+ border-radius: 0.5rem;
+ display: flex;
+ flex-direction: column;
+ padding: 0.5rem;
+
+ .MessageHeader {
+ column-gap: 0.5em;
+ display: flex;
+ padding: 0.5rem;
+ overflow: hidden;
+
+ .Avatar {
+ border-radius: 100%;
+ max-height: 2rem;
+ max-width: 2rem;
+ object-fit: cover;
+ aspect-ratio: 1/1;
+ }
+
+ .Login {
+ font-family: Barlow;
+ font-weight: 800;
+ text-align: left;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .Time {
+ font-family: Barlow;
+ margin-left: auto;
+ text-align: end;
+ }
+ }
+ }
+
+ .ChatUserEvent {
+ background-color: rgba(black, 0.25);
+ border-radius: 0.5rem;
+ display: flex;
+ flex-direction: column;
+ padding: 0.25rem;
+ opacity: 0.5;
+ }
+
+ .ChatMessageMentionned {
+ background-color: rgba(@accent, 0.25);
+ }
+
+ .MessageContent {
+ font-family: Montserrat;
+ font-size: 0.75rem;
+ font-weight: 400;
+ overflow-wrap: break-word;
+ padding: 0.5rem;
+ text-align: start;
+ }
+ }
+ }
+}
+
+.Hidden {
+ width: 0;
+ height: 0;
+ margin: 0;
+ padding: 0;
+}
+
+.CloseButton {
+ background-color: @accent;
+ border-radius: 0.5rem;
+ border: 0px solid transparent;
+ color: @light;
+ display: block;
+ font-family: Montserrat;
+ font-size: 0.75rem;
+ font-weight: 700;
+ max-height: 2rem;
+ padding: 0.5rem 0.75rem;
+ text-transform: uppercase;
+}
+
+.InputContainer {
+ background-color: rgba(black, 0.5);
+ border-radius: 0.5rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem;
+ overflow: hidden;
+
+ .Divider {
+ background-color: @light;
+ height: 2rem;
+ width: 0.1rem;
+ opacity: 0.75;
+ }
+}
+
+.RoomButton {
+ background-color: rgba(@light, 0.25);
+ padding: 0.35rem 0.5rem;
+ font-size: 0.75rem;
+}
+
+.ChatTimeout {
+ --fill-percent: 0;
+
+ border-radius: 50%;
+ padding: 0.25rem 0.5rem;
+
+ background: radial-gradient(
+ closest-side,
+ @dark 80%,
+ transparent 0 99.9%,
+ @dark 0
+ ),
+ conic-gradient(@accent calc(var(--fill-percent) * 1%), @dark 0);
+}
+
+.Input {
+ background-color: rgba(black, 0);
+ color: @light;
+ flex-grow: 1;
+ font-family: Montserrat;
+ font-size: 0.75rem;
+ font-weight: 700;
+ opacity: 1;
+ padding: 0.5rem;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.FormOverlay {
+ position: absolute;
+ inset: 0;
+ background-color: rgba(@dark, 0.95);
+ z-index: 2;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ .StylisedForm {
+ background-color: @base;
+ padding: 2rem 1rem;
+ border: 0.25rem solid @accent;
+ border-radius: 0.5rem;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ align-items: center;
+ justify-content: center;
+ font-family: Barlow;
+ min-width: 65vw;
+ max-width: 90vw;
+ color: @light;
+
+ .FormHeader {
+ display: flex;
+ margin-bottom: 2rem;
+
+ .FormTitle {
+ font-size: 1.5rem;
+ font-weight: 800;
+ text-align: center;
+ }
+ }
+
+ .FormItem {
+ display: grid;
+ grid-template-columns: 1fr 6fr;
+ width: 100%;
+ gap: 1rem;
+ align-items: center;
+ justify-content: space-between;
+
+ .FormLabel {
+ font-size: 1rem;
+ font-weight: 800;
+ text-align: end;
+ }
+
+ .FormInput {
+ color: @light;
+ grid-column: 2;
+ padding: 0.5rem;
+ border-radius: 0.25rem;
+ font-family: Montserrat;
+ background-color: @dark;
+ }
+
+ .FormInput[type="checkbox"] {
+ width: auto;
+ margin-right: auto;
+ }
+ }
+
+ .FormButtons {
+ display: flex;
+ margin-top: 1rem;
+ gap: 0.5rem;
+ justify-content: space-around;
+
+ box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+
+ [type="submit"] {
+ background-color: #ff4603;
+ }
+
+ button {
+ background-color: @dark;
+ color: @light;
+ border: 0.1rem solid #ff4603;
+
+ &:hover {
+ border: 0.1rem solid @light;
+ }
+ }
+ }
+ }
+}
diff --git a/rushs/eplace/src/pages/utils.js b/rushs/eplace/src/pages/utils.js
new file mode 100644
index 0000000..f8b1008
--- /dev/null
+++ b/rushs/eplace/src/pages/utils.js
@@ -0,0 +1,95 @@
+/**
+ * Global variables
+ */
+let [leftTimer, rightTimer] = [null, null];
+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");
+
+leftContainer.classList.toggle("Hidden", !leftSize);
+
+/**
+ * Calculate the layout of the home page
+ */
+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,
+ );
+});
+
+// If the mouse holds on the left side of the screen, open the left container
+document.addEventListener("mousemove", (e) => {
+ if (e.clientX < 10) {
+ if (!leftTimer) {
+ leftTimer = setTimeout(() => {
+ leftSize = 1;
+ localStorage.setItem("leftSize", leftSize);
+
+ calculateLayout();
+ setTimeout(() => {
+ leftContainer.classList.toggle("Hidden", false);
+ }, 300);
+ }, 200);
+ }
+ } else {
+ clearTimeout(leftTimer);
+ leftTimer = null;
+ }
+});
+
+// If the mouse holds on the right side of the screen, open the right container
+document.addEventListener("mousemove", (e) => {
+ if (e.clientX > window.innerWidth - 10) {
+ if (!rightTimer) {
+ rightTimer = setTimeout(() => {
+ rightSize = 1;
+ localStorage.setItem("rightSize", rightSize);
+
+ calculateLayout();
+ setTimeout(() => {
+ rightContainer.classList.toggle("Hidden", false);
+ }, 300);
+ }, 200);
+ }
+ } else {
+ clearTimeout(rightTimer);
+ rightTimer = null;
+ }
+});
diff --git a/rushs/eplace/src/rooms/canvas/index.js b/rushs/eplace/src/rooms/canvas/index.js
new file mode 100644
index 0000000..440e0d3
--- /dev/null
+++ b/rushs/eplace/src/rooms/canvas/index.js
@@ -0,0 +1,96 @@
+// FIXME: This file should handle the room canvas API
+// Link buttons to their respective functions
+// Functions may include:
+
+import { getStudent } from "../../students";
+import { authedAPIRequest } from "../../utils/auth";
+import { getPlacementData } from "./utils";
+
+// - getCanvas (get the canvas of a room and deserialize it)
+export async function getCanvas(slug) {
+ const config = {
+ method: "get",
+ };
+
+ return authedAPIRequest(`/rooms/${slug}/canvas`, config)
+ .then(async (res) => {
+ if (!res) {
+ return null;
+ }
+
+ const response = await res.json();
+
+ const pixels = response.pixels
+ .split("")
+ .map((c) => c.charCodeAt(0).toString(2).padStart(8, "0"))
+ .join("")
+ .match(/.{5}/g)
+ .map((b) => parseInt(b, 2));
+
+ return pixels;
+ })
+ .catch((error) => {
+ console.log(error);
+ return null;
+ });
+}
+// - subscribeToRoom (subscribe to the stream of a room)
+// - getPixelInfo (get the pixel info of a room)
+export async function getPixelInfo() {
+ const info = getPlacementData();
+
+ const response = await authedAPIRequest(
+ `/rooms/epi-place/canvas/pixels?posX=${info.posX}&posY=${info.posY}`,
+ { method: "get" },
+ );
+
+ if (!response) {
+ return null;
+ } else {
+ return response.json();
+ }
+}
+
+// - placePixel (place a pixel in a room)
+export async function placePixel() {
+ const info = getPlacementData();
+
+ const res = await authedAPIRequest(`/rooms/epi-place/canvas/pixels`, {
+ method: "POST",
+ headers: {
+ Accept: "application/json",
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ posX: info.posX,
+ posY: info.posY,
+ color: info.color,
+ }),
+ });
+
+ if (!res) {
+ return;
+ }
+
+ const response = await res.json();
+
+ const placeDate = new Date(response.timestamp);
+ const studentInfo = await getStudent(response.placedByUid);
+
+ if (!studentInfo) {
+ return;
+ }
+
+ document.getElementById("tooltip-time").innerHTML =
+ placeDate.toLocaleTimeString();
+ document.getElementById("tooltip-date").innerHTML =
+ placeDate.toLocaleDateString();
+ document
+ .getElementById("tooltip-info-avatar")
+ .setAttribute("src", studentInfo.avatarURL ?? "/default-avatar.png");
+ document.getElementById("tooltip-info-quote").innerHTML = studentInfo.quote;
+ document.getElementById("tooltip-info-login").innerHTML = studentInfo.login;
+}
+document
+ .getElementById("color-place-button")
+ .addEventListener("click", placePixel);
diff --git a/rushs/eplace/src/rooms/canvas/utils.js b/rushs/eplace/src/rooms/canvas/utils.js
new file mode 100644
index 0000000..5737dbb
--- /dev/null
+++ b/rushs/eplace/src/rooms/canvas/utils.js
@@ -0,0 +1,432 @@
+// 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";
+import { getPixelInfo } from ".";
+import { getStudent } from "../../students";
+
+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
+ * @returns {{color: number, posX: number, posX: number}} the data
+ */
+export const getPlacementData = () => ({
+ color: selectedColorIdx,
+ posX: target.x,
+ posY: target.y,
+});
+
+/**
+ * Toggle the tooltip and display the pixel's information
+ * @param {boolean} state
+ */
+export const toggleTooltip = async (state = false) => {
+ tooltip.style.display = state ? "flex" : "none";
+
+ if (state) {
+ const pixelInfo = await getPixelInfo();
+ const uid = pixelInfo.placedByUid;
+ const placeDate = new Date(pixelInfo.timestamp);
+
+ if (!pixelInfo) {
+ throw new Error(
+ 'An error occured while hitting the "/rooms/epi-place/canvas/pixels" endpoint',
+ );
+ }
+
+ const studentInfo = await getStudent(uid);
+
+ if (!studentInfo) {
+ throw new Error(
+ 'An error occured while hitting the "/students/:id" endpoint',
+ );
+ }
+
+ document.getElementById("tooltip-time").innerHTML =
+ placeDate.toLocaleTimeString();
+ document.getElementById("tooltip-date").innerHTML =
+ placeDate.toLocaleDateString();
+ document
+ .getElementById("tooltip-info-avatar")
+ .setAttribute(
+ "src",
+ studentInfo.avatarURL ?? "/default-avatar.png",
+ );
+ document.getElementById("tooltip-info-quote").innerHTML =
+ studentInfo.quote;
+ document.getElementById("tooltip-info-login").innerHTML =
+ studentInfo.login;
+ // 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
+ * @param {*} event
+ */
+const positionUpdate = (event) => positionDisplay(calculateTarget(event));
+
+/**
+ * Update the position tooltip
+ * @param {{x: number, y: number}} target
+ */
+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
+ * @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
+ */
+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
+ * @param {*} roomConfig
+ * @param {number[]} pixels
+ */
+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);
+};
+
+/**
+ * Update the canvas
+ * @param {string} color
+ * @param {number} x
+ * @param {number} y
+ */
+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 canvas values
+ */
+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})`;
+};
diff --git a/rushs/eplace/src/rooms/chat/index.js b/rushs/eplace/src/rooms/chat/index.js
new file mode 100644
index 0000000..493f142
--- /dev/null
+++ b/rushs/eplace/src/rooms/chat/index.js
@@ -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)
diff --git a/rushs/eplace/src/rooms/chat/utils.js b/rushs/eplace/src/rooms/chat/utils.js
new file mode 100644
index 0000000..2d21ab2
--- /dev/null
+++ b/rushs/eplace/src/rooms/chat/utils.js
@@ -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)
diff --git a/rushs/eplace/src/rooms/index.js b/rushs/eplace/src/rooms/index.js
new file mode 100644
index 0000000..dde88e6
--- /dev/null
+++ b/rushs/eplace/src/rooms/index.js
@@ -0,0 +1,50 @@
+// FIXME: This file should handle the rooms API
+// Functions may include:
+
+import { subscribe } from "../utils/streams";
+import { authedAPIRequest } from "../utils/auth";
+
+// - fetchRoomConfig (get the configuration of a room)
+export async function fetchRoomConfig(slug) {
+ const config = {
+ method: "get",
+ };
+
+ return authedAPIRequest(`/rooms/${slug}/config`, config)
+ .then(async (res) => {
+ if (!res) {
+ return null;
+ }
+
+ const response = await res.json();
+
+ document.getElementById("room-name").innerHTML =
+ response.metadata.name;
+ const description = document.getElementById("room-description");
+
+ if (response.metadata.description) {
+ description.innerHTML = response.metadata.description;
+ description.style.display = "inherit";
+ } else {
+ description.style.display = "none";
+ }
+
+ return response;
+ })
+ .catch((error) => {
+ console.log(error);
+ return null;
+ });
+}
+// - joinRoom (join a room by its slug)
+export function joinRoom(slug) {
+ if (!slug) {
+ slug = "epi-place";
+ }
+
+ subscribe(slug);
+}
+// - listRooms (list all the rooms available)
+// - createRoom (create a room)
+// - updateRoom (update a room's configuration)
+// - deleteRoom (delete a room)
diff --git a/rushs/eplace/src/rooms/utils.js b/rushs/eplace/src/rooms/utils.js
new file mode 100644
index 0000000..5e94739
--- /dev/null
+++ b/rushs/eplace/src/rooms/utils.js
@@ -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)
diff --git a/rushs/eplace/src/students/index.js b/rushs/eplace/src/students/index.js
new file mode 100644
index 0000000..2aa18a2
--- /dev/null
+++ b/rushs/eplace/src/students/index.js
@@ -0,0 +1,19 @@
+// FIXME: This file should handle the students API
+// Functions may include:
+
+import { authedAPIRequest } from "../utils/auth";
+
+// - getStudent (get a student from the API by its uid or login)
+export async function getStudent(id) {
+ const response = await authedAPIRequest(`/students/${id}`, {
+ method: "get",
+ });
+
+ if (!response) {
+ return null;
+ } else {
+ return response.json();
+ }
+}
+// - getUserUidFromToken (get the user's uid from the token in local storage)
+// - updateStudent (update the student's profile through the API)
diff --git a/rushs/eplace/src/students/utils.js b/rushs/eplace/src/students/utils.js
new file mode 100644
index 0000000..09bb32e
--- /dev/null
+++ b/rushs/eplace/src/students/utils.js
@@ -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)
diff --git a/rushs/eplace/src/utils/auth.js b/rushs/eplace/src/utils/auth.js
new file mode 100644
index 0000000..2576282
--- /dev/null
+++ b/rushs/eplace/src/utils/auth.js
@@ -0,0 +1,124 @@
+// FIXME: This file should handle the authentication
+// Exports must include:
+// - authedAPIRequest (make an authenticated request to the API)
+
+/**
+ * This function makes an authenticated request to the API
+ * This function must always hit the /api/* endpoint.
+ * @param {string} endpoint
+ * @param {object} options This object should at least contain the method.
+ * For the other options, you can refer to the fetch documentation as it should be the same.
+ * @returns {Promise<Response>} the response
+ * We want a {Promise<Response>} so we can read the headers as well as the body, rather than
+ * just the body.
+ **/
+export async function authedAPIRequest(endpoint, options) {
+ if (localStorage.getItem("token")) {
+ // essaye de contacter l'endpoint
+ const headers = {
+ Authorization: `Bearer ${localStorage.getItem("token")}`,
+ };
+
+ if (options["headers"]) {
+ options["headers"]["Authorization"] =
+ `Bearer ${localStorage.getItem("token")}`;
+ } else {
+ options["headers"] = headers;
+ }
+
+ const apiURL = `${import.meta.env.VITE_URL}/api${endpoint}`;
+
+ try {
+ const response = await fetch(apiURL, options);
+
+ if (response.status === 200) {
+ return response;
+ } else {
+ console.log("request error");
+ console.log(response);
+ if (
+ response.status === 401 &&
+ (await response.json()).message.match(/Token expired/)
+ ) {
+ localStorage.removeItem("token");
+ if (!(await sendTokenRequest())) {
+ return null;
+ }
+
+ const response = await authedAPIRequest(endpoint, options);
+
+ if (!response) {
+ return null;
+ }
+
+ return response;
+ } else {
+ localStorage.clear();
+ // window.location.replace(import.meta.env.VITE_URL);
+ await sendTokenRequest();
+ return null;
+ }
+ }
+ } catch {
+ console.log("an error occured while fetching");
+ return null;
+ }
+ }
+
+ await sendTokenRequest();
+ return null;
+}
+
+// Functions may include:
+// - sendTokenRequest (get a token or refresh it)
+export async function sendTokenRequest() {
+ if (
+ !localStorage.getItem("token") &&
+ !localStorage.getItem("refresh_token")
+ ) {
+ const authQueryParams = {
+ client_id: import.meta.env.VITE_CLIENT_ID,
+ scope: "epita profile picture",
+ redirect_uri: `${import.meta.env.VITE_URL}/complete/epita/`,
+ response_type: "code",
+ };
+ const url = new URL(`${import.meta.env.VITE_AUTH_URL}/authorize`);
+
+ //`?client_id=${authQueryParams.client_id}&scope=${authQueryParams.scope}&redirect_uri=${authQueryParams.redirect_uri}&response_type=${authQueryParams.response_type}`,
+ url.searchParams.append("client_id", authQueryParams.client_id);
+ url.searchParams.append("scope", authQueryParams.scope);
+ url.searchParams.append("redirect_uri", authQueryParams.redirect_uri);
+ url.searchParams.append("response_type", authQueryParams.response_type);
+
+ window.location.replace(url);
+ return false;
+ } else if (localStorage.getItem("refresh_token")) {
+ const form = new FormData();
+
+ form.append("client_id", import.meta.env.VITE_CLIENT_ID);
+ form.append(
+ "redirect_uri",
+ `${import.meta.env.VITE_URL}/complete/epita/`,
+ );
+ form.append("grant_type", "refresh_token");
+ form.append("refresh_token", localStorage.getItem("refresh_token"));
+ const res = await fetch(`${import.meta.env.VITE_URL}/auth-api/token`, {
+ method: "POST",
+ body: form,
+ });
+
+ if (res.status === 200) {
+ const response = await res.json();
+
+ localStorage.setItem("token", response.id_token);
+ localStorage.setItem("refresh_token", response.refresh_token);
+ return true;
+ } else {
+ localStorage.clear();
+ // window.location.replace(import.meta.env.VITE_URL);
+ // HERE ptetre return direct senTokenRequest
+ await sendTokenRequest();
+ return false;
+ }
+ }
+}
diff --git a/rushs/eplace/src/utils/notify.js b/rushs/eplace/src/utils/notify.js
new file mode 100644
index 0000000..b6ed7dc
--- /dev/null
+++ b/rushs/eplace/src/utils/notify.js
@@ -0,0 +1,57 @@
+import $ from "jquery";
+import alertHtml from "../components/notifications/index.html";
+
+const alertContainer = $("#alert-container")?.[0];
+
+const iconMap = {
+ info: "fa-info-circle",
+ success: "fa-thumbs-up",
+ warning: "fa-exclamation-triangle",
+ error: "ffa fa-exclamation-circle",
+};
+
+/**
+ * Create an alert
+ * @param {string} title
+ * @param {string} message
+ * @param {string} type - success, warning, error
+ */
+export const createAlert = (title, message, type) => {
+ $.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);
+ },
+ });
+};
diff --git a/rushs/eplace/src/utils/rateLimits.js b/rushs/eplace/src/utils/rateLimits.js
new file mode 100644
index 0000000..d7b11a6
--- /dev/null
+++ b/rushs/eplace/src/utils/rateLimits.js
@@ -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)
diff --git a/rushs/eplace/src/utils/streams.js b/rushs/eplace/src/utils/streams.js
new file mode 100644
index 0000000..f714527
--- /dev/null
+++ b/rushs/eplace/src/utils/streams.js
@@ -0,0 +1,108 @@
+// FIXME: This file should handle the sockets and the subscriptions
+import { io } from "socket.io-client";
+import { v4 as uuidv4 } from "uuid";
+
+import { createAlert } from "./notify";
+import { fetchRoomConfig, joinRoom } from "../rooms";
+import { getCanvas } from "../rooms/canvas";
+import { initCanvas, renderCanvasUpdate } from "../rooms/canvas/utils";
+import { sendTokenRequest } from "../utils/auth";
+
+let subscribed = false;
+
+export let socket = undefined;
+
+const updateBuffer = [];
+
+// Exports must include
+// - initSocket (initialize the connection to the socket server)
+export async function initSocket() {
+ if (!localStorage.getItem("token")) {
+ if (!(await sendTokenRequest())) {
+ createAlert("ForgeID", "Authentication failed", "error");
+ return;
+ }
+
+ createAlert("ForgeID", "Authentication successful", "success");
+ }
+
+ if (socket) {
+ return socket;
+ }
+
+ socket = io(import.meta.env.VITE_URL, {
+ extraHeaders: {
+ Authorization: `Bearer ${localStorage.getItem("token")}`,
+ },
+ });
+ socket.on("connect", () => {
+ joinRoom();
+ });
+
+ socket.on("message", async (msg) => {
+ if (msg.result && msg.result.type === "started") {
+ const roomConfig = await fetchRoomConfig("epi-place");
+
+ if (!roomConfig) {
+ return;
+ }
+
+ const pixels = await getCanvas("epi-place");
+
+ if (!pixels) {
+ return;
+ }
+
+ initCanvas(roomConfig, pixels);
+ // debuffer pixel updates
+ catchUpOnUpdates();
+ subscribed = true;
+ }
+ });
+
+ socket.on("pixel-update", (update) => {
+ if (!subscribed) {
+ updateBuffer.push({
+ color: update.result.data.json.color,
+ posX: update.result.data.json.posX,
+ posY: update.result.data.json.posY,
+ });
+ } else {
+ renderCanvasUpdate(
+ update.result.data.json.color,
+ update.result.data.json.posX,
+ update.result.data.json.posY,
+ );
+ }
+ });
+ return socket;
+}
+// - socket (variable resulting of initSocket function)
+
+// Functions may include:
+// - subscribe (subscribe to a room's stream or chat)
+export function subscribe(slug) {
+ const subscription = {
+ id: uuidv4(),
+ method: "subscription",
+ params: {
+ path: "rooms.canvas.getStream",
+ input: {
+ json: {
+ roomSlug: slug,
+ },
+ },
+ },
+ };
+
+ socket.send(subscription);
+}
+function catchUpOnUpdates() {
+ while (updateBuffer.length > 0) {
+ const update = updateBuffer.pop();
+
+ renderCanvasUpdate(update.color, update.posX, update.posY);
+ }
+}
+// - unsubscribe (unsubscribe from a room's stream or chat)
+// - sendMessage (send a message to a room's chat)