given files
This commit is contained in:
parent
9bf88844e9
commit
a2c31f873d
48 changed files with 10458 additions and 0 deletions
8
.env
Normal file
8
.env
Normal 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
24
.gitignore
vendored
Normal 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
6
.prettierrc.js
Normal 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
71
eslint.config.mjs
Normal 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
31
package.json
Normal 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
BIN
public/default-avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
BIN
public/default-icon.png
Normal file
BIN
public/default-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 590 B |
14
public/selector.svg
Normal file
14
public/selector.svg
Normal 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
37
server/.env
Normal 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"
|
||||||
1
server/config/default-canvas-250.txt
Normal file
1
server/config/default-canvas-250.txt
Normal file
File diff suppressed because one or more lines are too long
BIN
server/config/default-canvas-50.txt
Normal file
BIN
server/config/default-canvas-50.txt
Normal file
Binary file not shown.
54
server/config/rate-limits.config.json
Normal file
54
server/config/rate-limits.config.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
46
server/config/rooms.config.json
Normal file
46
server/config/rooms.config.json
Normal 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
56
server/docker-compose.yml
Normal 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
5503
server/openapi/openapi.json
Normal file
File diff suppressed because it is too large
Load diff
12
src/components/notifications/index.html
Normal file
12
src/components/notifications/index.html
Normal 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>
|
||||||
10
src/components/rooms/index.html
Normal file
10
src/components/rooms/index.html
Normal 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>
|
||||||
8
src/components/rooms/message.html
Normal file
8
src/components/rooms/message.html
Normal 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>
|
||||||
70
src/components/rooms/upsert.html
Normal file
70
src/components/rooms/upsert.html
Normal 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>
|
||||||
3
src/components/rooms/user-event.html
Normal file
3
src/components/rooms/user-event.html
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<div class="ChatUserEvent">
|
||||||
|
<span class="MessageContent">{{message_content}}</span>
|
||||||
|
</div>
|
||||||
42
src/components/students/update.html
Normal file
42
src/components/students/update.html
Normal 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>
|
||||||
12
src/pages/complete/epita/index.html
Normal file
12
src/pages/complete/epita/index.html
Normal 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>
|
||||||
2
src/pages/complete/epita/index.js
Normal file
2
src/pages/complete/epita/index.js
Normal 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
|
||||||
58
src/pages/debug/debug.html
Normal file
58
src/pages/debug/debug.html
Normal 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
122
src/pages/debug/debug.js
Normal 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
128
src/pages/debug/index.css
Normal 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
57
src/pages/debug/utils.js
Normal 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
78
src/pages/index.css
Normal 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
239
src/pages/index.html
Normal 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
6
src/pages/index.js
Normal 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
1080
src/pages/styles.less
Normal file
File diff suppressed because it is too large
Load diff
83
src/pages/utils.js
Normal file
83
src/pages/utils.js
Normal 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);
|
||||||
|
});
|
||||||
7
src/rooms/canvas/index.js
Normal file
7
src/rooms/canvas/index.js
Normal 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
467
src/rooms/canvas/utils.js
Normal 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
4
src/rooms/chat/index.js
Normal 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
6
src/rooms/chat/utils.js
Normal 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
10
src/rooms/index.js
Normal 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
6
src/rooms/utils.js
Normal 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
5
src/students/index.js
Normal 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
5
src/students/utils.js
Normal 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
6
src/utils/auth.js
Normal 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
64
src/utils/notify.js
Normal 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
3
src/utils/rateLimits.js
Normal 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
4
src/utils/redirect.js
Normal 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
9
src/utils/streams.js
Normal 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
44
vite.config.js
Normal 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",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue