2 Commits

Author SHA1 Message Date
H3XploR 3124239374 Merge pull request #23 from OlaketalAmigo/Rendu
MERGED
2026-04-02 11:51:55 +02:00
gprunet c066fdc31c Round Timer + room creation synced with browse tab 2026-04-01 21:31:14 +02:00
11 changed files with 136 additions and 484 deletions
-3
View File
@@ -1,3 +0,0 @@
INTRA_CLIENT_ID=u-s4t2ud-c226cd35cd1ac08a4c6668deee1c64d7d67a13a766aee672acafd4a1522d483c
INTRA_CLIENT_SECRET=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
-2
View File
@@ -12,7 +12,6 @@ import playerStatsRouter from './routes/player_stats.js';
import {waitForDb, createTables, runMigrations, ensureOauthClient} from './db.js';
import setupSocketIO from './services/socket.js';
import avatarService from './services/avatar.js';
import intraRouter from './routes/intra.js';
const app = express();
const httpsOptions = {
@@ -54,7 +53,6 @@ async function startServer()
app.use('/api/avatar', avatarRouter);
app.use('/api/friends', friendsRouter);
app.use('/api/stats', playerStatsRouter);
app.use('/api/intra', intraRouter);
app.get('/api', (req, res) => res.send('Backend running'));
server.listen(3001, () =>
@@ -1,53 +0,0 @@
// routes/intra.js
import express from 'express';
const router = express.Router();
let token;
async function set_token() {
try {
const response = await fetch("https://api.intra.42.fr/oauth/token", {
method: "POST",
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: process.env.INTRA_CLIENT_ID,
client_secret: process.env.INTRA_CLIENT_SECRET
}),
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
});
token = await response.json();
setTimeout(set_token, (token.expires_in - 60) * 1000);
} catch (e) {
console.error("Token error:", e);
}
}
set_token();
router.get('/profile/:login', async (req, res) => {
try {
const response = await fetch(
`https://api.intra.42.fr/v2/users/${req.params.login}`,
{
headers: {
Authorization: `Bearer ${token.access_token}`
}
}
);
if (!response.ok) {
return res.status(response.status).json({ error: 'User not found' });
}
res.json(await response.json());
} catch (e) {
res.status(500).json({ error: 'Failed to fetch profile' });
}
});
export default router;
+37 -45
View File
@@ -140,47 +140,6 @@ async function saveRoundPoints(currentScores, roundStartScores) {
}
}
function handlePlayerDeparture(io, roomId, username) {
const gameState = gameRooms.get(roomId);
if (!gameState || !gameState.isPlaying) return;
if (!Array.isArray(gameState.players)) return;
const leavingIndex = gameState.players.indexOf(username);
if (leavingIndex === -1) return;
const wasDrawer = gameState.drawer === username;
gameState.players = gameState.players.filter(p => p !== username);
if (gameState.scores) {
delete gameState.scores[username];
}
if (gameState.currentPlayerIndex >= leavingIndex) {
gameState.currentPlayerIndex = Math.max(0, gameState.currentPlayerIndex - 1);
}
if (gameState.currentPlayerIndex >= gameState.players.length) {
gameState.currentPlayerIndex = 0;
}
if (wasDrawer && gameState.players.length > 0) {
stopRoomTimer(roomId);
const newDrawer = gameState.players[gameState.currentPlayerIndex];
gameState.drawer = newDrawer;
gameState.currentWord = '';
gameState.revealedLetters = [];
gameState.revealedWord = [];
gameState.guessedLetters = [];
gameState.wrongGuesses = 0;
io.to(roomId).emit('game-drawer-changed', {
newDrawer: newDrawer,
reason: 'drawer_left',
message: `${username} (dessinateur) a quitte, ${newDrawer} devient le nouveau dessinateur`
});
startRoomTimer(io, roomId, 60);
}
}
function setupSocketIO(io)
{
ioInstance = io;
@@ -317,7 +276,6 @@ function setupSocketIO(io)
username: socket.user.username,
userId: socket.user.userId
});
handlePlayerDeparture(io, roomId, socket.user.username);
socket.leave(roomId);
console.log(`${socket.user.username} left ${roomId}`);
@@ -616,7 +574,7 @@ function setupSocketIO(io)
// Points: 10 per letter found, -5 for wrong guess
if (success) {
points = lettersFound * 5;
points = lettersFound * 10;
gameState.scores[username] += points;
} else {
points = -5;
@@ -715,7 +673,42 @@ function setupSocketIO(io)
message: `${username} a quitté la partie`
});
handlePlayerDeparture(io, roomId, username);
const gameState = gameRooms.get(roomId);
if (gameState)
{
const wasDrawer = gameState.drawer === username;
gameState.players = gameState.players.filter(p => p !== username);
delete gameState.scores[username];
io.to(roomId).emit('scores-updated', gameState.scores);
// If the drawer left and there are still enough players, choose a new drawer
if (wasDrawer && gameState.players.length >= 1)
{
stopRoomTimer(roomId);
// Pick the next player as the new drawer
gameState.currentPlayerIndex = gameState.currentPlayerIndex % gameState.players.length;
const newDrawer = gameState.players[gameState.currentPlayerIndex];
gameState.drawer = newDrawer;
// Reset the word state for the new round
gameState.currentWord = '';
gameState.revealedLetters = [];
gameState.revealedWord = [];
gameState.guessedLetters = [];
gameState.wrongGuesses = 0;
console.log(`Drawer ${username} left, new drawer is ${newDrawer}`);
io.to(roomId).emit('game-drawer-changed', {
newDrawer: newDrawer,
reason: 'drawer_left',
message: `${username} (dessinateur) a quitté, ${newDrawer} devient le nouveau dessinateur`
});
startRoomTimer(io, roomId, 60);
}
}
await checkAndStopSinglePlayerGame(io, roomId, dbRoomId);
@@ -963,7 +956,6 @@ function setupSocketIO(io)
username: socket.user.username,
userId: socket.user.userId
});
handlePlayerDeparture(io, roomId, socket.user.username);
// Get updated player list and broadcast
if (dbRoomId) {
-48
View File
@@ -3,8 +3,6 @@
* Initializes windows and handles menu interactions
*/
import { windowRegistry } from './core/windows.js';
import { API, STORAGE_KEYS } from './core/config.js';
import { eventBus, Events } from './core/events.js';
import { LoginWindow } from './windows/login.js';
import { LogoutWindow } from './windows/logout.js';
import { GlobalChat } from './windows/global_chat.js';
@@ -19,7 +17,6 @@ import { StatsWindow } from './windows/stats.js';
*/
class App {
constructor() {
this.invalidateStaleToken();
this.initWindows();
this.initMenu();
this.initPage();
@@ -27,51 +24,6 @@ class App {
this.colorizeUI();
}
async invalidateStaleToken() {
const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
if (!token) return;
if (this.isJwtExpired(token)) {
localStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN);
eventBus.emit(Events.USER_LOGGED_OUT);
return;
}
try {
const response = await fetch(API.STATS.ME, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (response.status === 401) {
localStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN);
eventBus.emit(Events.USER_LOGGED_OUT);
setTimeout(() => window.location.reload(), 500);
}
} catch (error) {
console.warn('Token validation skipped:', error);
}
}
isJwtExpired(token) {
try {
const payload = this.decodeJwtPayload(token);
if (!payload || !payload.exp) return false;
const now = Math.floor(Date.now() / 1000);
return payload.exp <= now;
} catch (error) {
return false;
}
}
decodeJwtPayload(token) {
const parts = token.split('.');
if (parts.length < 2) return null;
const base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=');
return JSON.parse(atob(padded));
}
/**
* Initializes all windows
*/
@@ -82,7 +82,6 @@ html {
body {
margin: 0;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
color: var(--color-text);
line-height: 1.5;
@@ -1,53 +0,0 @@
import { colorizeText, updateElement } from "./tools.js";
/* /////////////////////////////////////////// */
export class Popup {
constructor(msg, parent = document.body) {
this.msg = msg;
this.parent = parent;
this.obj = updateElement({
parent: parent,
classList: ['popup'],
additionalStyles: {
opacity: '0'
}
});
this.run();
}
async create() {
this.parent.appendChild(this.obj);
this.obj.style.transition = "opacity 0.5s ease";
requestAnimationFrame(() => {
this.obj.style.opacity = "1";
});
await new Promise(r => setTimeout(r, 500));
}
async write(speed = 30) {
for (let i = 0; i < this.msg.length; i++) {
this.obj.textContent += this.msg[i];
// colorizeText();
await new Promise(r => setTimeout(r, speed));
}
}
async remove() {
await new Promise(r => setTimeout(r, 2000));
this.obj.style.transition = "opacity 0.3s ease";
this.obj.style.opacity = "0";
await new Promise(r => setTimeout(r, 300));
if (this.obj.parentNode) {
this.obj.parentNode.removeChild(this.obj);
}
}
async run() {
await this.create();
await this.write();
await this.remove();
}
}
@@ -1,61 +0,0 @@
/* /////////////////////////////////////////// */
// render in color the text of all .multicolor
export function colorizeText() {
const elements = document.querySelectorAll(".multicolor");
const colorizeText = (el) => {
const text = el.textContent;
el.innerHTML = "";
const baseHue = Math.random() * 360;
// 🎲 random step = makes rainbow "scrambled"
const step = (Math.random() * 60) + 10; // 10 → 70
// 🎲 random direction (left or right rainbow)
const direction = Math.random() < 0.5 ? 1 : -1;
[...text].forEach((char, i) => {
const span = document.createElement("span");
span.textContent = char;
const hue = baseHue + (i * step * direction);
span.style.color = `hsl(${hue}, 90%, 60%)`;
span.style.textShadow = `1px 1px 0 rgba(0,0,0,0.3)`;
el.appendChild(span);
});
};
elements.forEach(colorizeText);
}
export function updateElement({
el, // existing element or null to create new
parent = document.body,
id = null,
classList = [], // object like { css - classes to add }
textContent = "",
additionalStyles = {} // object like { color: 'red', display: 'flex' }
} = {}) {
// If no element passed, create a div by default
if (!el) {
el = document.createElement('div');
parent.appendChild(el);
}
// Set ID if provided
if (id) el.id = id;
// Manage classes
classList.forEach(cls => el.classList.add(cls));
// Set text content
if (textContent !== undefined) el.textContent = textContent;
Object.assign(el.style, additionalStyles);
return el;
}
@@ -1,106 +1,79 @@
body {
margin: 0;
height: 100vh;
:root {
--color-primary: #ffc75e;
--color-primary-hover: #ffc75e;
--color-success: #00c71b;
--color-success-dark: #ffc75e;
--color-error: #ff4d4d;
--color-warning: #ffc75e;
--color-github: #ffc75e;
--color-bg: #ffe5b5;
--color-surface: #ffcc00;
--color-surface-light: #feffa6;
--color-text: #000000;
--color-text-muted: #353535;
--font-size-base: 10px;
--font-size-sm: 1.2rem;
--font-size-md: 1.4rem;
--font-size-lg: 1.6rem;
--font-size-xl: 3rem;
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 12px;
--spacing-lg: 16px;
--spacing-xl: 24px;
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 12px;
--radius-full: 50%;
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.5);
--shadow-lg: 0 8px 16px rgba(0, 0, 0, 0.5);
--transition-fast: 150ms ease;
--transition-normal: 250ms ease;
--z-menu: 2;
--z-window: 100;
--z-modal: 200;
}
.game {
position: fixed;
top: var(--spacing-lg);
right: 50px;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(135deg, #1f001f, #1f1f00);
overflow: hidden;
flex-direction: column;
gap: var(--spacing-lg);
z-index: var(--z-menu);
}
h1 {
font-size: 4rem;
color: white;
animation: float 2s ease-in-out infinite alternate;
border: 5px double #654050;
.game__item {
background: var(--color-surface);
color: var(--color-text);
border: 1px solid var(--color-surface-light);
border-radius: var(--radius-lg);
border-color: #fda725;
padding: var(--spacing-sm) var(--spacing-md);
font-size: var(--font-size-md);
cursor: pointer;
transition: all var(--transition-fast);
text-align: center;
}
@keyframes float {
from { transform: translateY(0); }
to { transform: translateY(-20px); }
}
/* /////////////////////////////////////////// */
.box:hover {
.game__item:hover {
background: var(--color-surface-light);
font-size: var(--font-size-lg);
animation: bobble 0.4s ease-in-out infinite alternate;
}
@keyframes bobble {
from {
transform: translateX(-5px);
}
to {
transform: translateX(5px);
}
.game__item--active {
background: var(--color-primary);
border-color: var(--color-primary);
}
.box {
background: #142d4a;
height: 100px;
margin: 15px;
aspect-ratio: 1/1;
border-radius: 25px;
position: fixed;
top: 0;
right: 0;
display: flex;
justify-content: center;
align-items: center;
font-family: "Roboto";
font-size: 15px;
z-index: 10;
}
@property --deg {
syntax: '<angle>';
inherits: true;
initial-value: 0deg;
}
.box::before,
.box::after {
content: "";
position: absolute;
height: 100%;
width: 100%;
background: conic-gradient(
from var(--deg) at center,
#00c3ff,
#4d0199,
#6300c6,
#009dcd
);
border-radius: inherit;
z-index: -2;
padding: 2px;
animation: autoRotate 2s linear infinite;
}
.box::after {
filter: blur(10px);
}
@keyframes autoRotate {
to{ --deg: 360deg; }
}
/* /////////////////////////////////////////// */
.popup {
max-width: 300px;
background-color: #41030c;
margin: 30px;
margin-right: -50px;
padding: 1em;
font-weight: 700;
color: #ffffff;
font-size: 20px;
text-align: center;
border: 10px solid var(--clr-accent);
border-radius: 50px;
position: fixed;
right: 50%;
top: 50%;
z-index: 999;
}
@@ -5,24 +5,45 @@
<title>Wiskas</title>
<link rel="stylesheet" href="wiskas.css" />
<style>
body {
margin: 0;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(135deg, #6a11cb, #2575fc);
font-family: Arial, sans-serif;
overflow: hidden;
}
h1 {
font-size: 4rem;
color: white;
animation: float 2s ease-in-out infinite alternate;
}
@keyframes float {
from { transform: translateY(0); }
to { transform: translateY(-20px); }
}
</style>
</head>
<body>
<h1 id="helloText"></h1>
<h1 id="helloText">METS TON CHAT ICI</h1>
<nav class="game" aria-label="Game">
<button class="box multicolor" data-action="Home page" aria-label="Home Page"
<button class="game__item" data-action="Home page" aria-label="Home Page"
onclick="window.location.href='../index.html'">Home Page</button>
</nav>
</nav>
<nav class="login" aria-label="Game">
<button class="box multicolor" aria-label="Home Page" id="login-button">Login</button>
</nav>
<script>
const colors = ["#ff4b5c", "#56cfe1", "#80ed99", "#ffd166"];
const text = document.getElementById("helloText");
<div id="login"></div>
<script type="module" src="./wiskas.js"></script>
setInterval(() => {
const randomColor = colors[Math.floor(Math.random() * colors.length)];
text.style.color = randomColor;
}, 500);
</script>
</body>
</html>
@@ -1,113 +0,0 @@
import { Popup } from "./popup.js";
import { colorizeText, updateElement } from "./tools.js";
/* /////////////////////////////////////////// */
/* /////////////////////////////////////////// */
// 2️⃣ Add click handler
async function tryLogin() {
const login = prompt("Enter your 42 login:"); // Ask for a login
if (!login) return;
try {
// Call your backend route
const res = await fetch(`/api/intra/profile/${login}`);
if (!res.ok) {
new Popup('Please, who do you think we are?\nWe already know all about you.\nNow enter your correct login and nobody gets hurt');
const errorData = await res.json();
return null;
}
const data = await res.json();
console.log("Profile data:", data);
return data;
} catch (err) {
console.error("Fetch failed:", err);
return null;
}
}
/* /////////////////////////////////////////// */
export class Wiskas {
constructor(parent = document.body) {
this.parent = parent;
this.obj = updateElement({
parent: parent,
classList: ['wiskas']
})
this.json_login = '';
this.index_chaberu = 0;
this.iniChat();
}
chaberu() {
let num = Math.min(this.index_chaberu, this.discussions.length - 1);
let text = this.discussions[num];
new Popup(text, this.obj);
this.index_chaberu++;
}
iniChat() {
this.discussions = ['Well hi there...',
'Please refrain from touching\n the yellow button without\n beeing logged in',
'We are going to take actions\n if you continue..',
'Actions already taken\n you are only making it worse'];
}
async login() {
let answer = await tryLogin();
if (!answer) return;
this.json_login = answer;
let dataUser = {
firstName: this.json_login.usual_first_name ?? this.json_login.first_name,
lastName: this.json_login.last_name,
photo: this.json_login.image.link,
month: this.json_login.pool_month,
year: this.json_login.pool_year,
projects: this.json_login.projects_users.filter(project => project.status === "in_progress").map(project => project.project.name),
perfect: this.json_login.projects_users.filter(project => project.final_mark === 125).map(project => project.project.name),
};
this.discussions = [
`Welcome ${dataUser.firstName} ${dataUser.lastName}.`,
`We heard quite a lot about the piscine of ${dataUser.month} ${dataUser.year}...\nIt's suprising to see you here`,
`How is your ${dataUser.projects[Math.floor(Math.random() * dataUser.projects.length)]} coming along?`,
`Perfect score for ${dataUser.perfect[Math.floor(Math.random() * dataUser.perfect.length)]}, impressive.. Should you really spend so much time in front of a screen?`,
`Shouldn't you be working on your ${dataUser.projects[Math.floor(Math.random() * dataUser.projects.length)]}?`,
`Quite an ugly human...\n but then again, you arent a cat`
];
}
}
/* /////////////////////////////////////////// */
let el = document.getElementById('helloText');
let img = document.createElement('img');
img.src = '../assets/wiskas-the-third.jpg';
el.append(img);
/* /////////////////////////////////////////// */
colorizeText();
/* /////////////////////////////////////////// */
// 1️⃣ Create a button dynamically
const app = document.getElementById('login');
let cat = new Wiskas;
let buttonLogin = document.getElementById('login-button');
Object.assign(buttonLogin.style, {
position: 'fixed', // make sure it's fixed
top: '0',
left: '0',
right: 'auto' // remove the right: 0 if it comes from CSS
});
buttonLogin.addEventListener('click', () => cat.login());
img.addEventListener('click', () => { cat.chaberu() })