8 Commits

Author SHA1 Message Date
Kali Gallon b40dad8f57 ^^._, work in progress, small changes 2026-04-02 15:00:36 +02:00
Kali Gallon 23cca7a249 Merge branch 'Rendu' of github.com:OlaketalAmigo/Transcendence into Rendu 2026-04-02 14:56:50 +02:00
Kali Gallon e1e529b3ca ^^._, work in progress, small changes 2026-04-02 14:55:07 +02:00
Thomas Fauve-Piot e96c16819d Merge branch 'Rendu' of github.com:OlaketalAmigo/Transcendence into Rendu 2026-04-02 12:59:19 +02:00
Thomas Fauve-Piot aefb858247 Overflow game.css 2026-04-02 12:55:56 +02:00
Yannis Antoine a9f81b4d65 5 point par lettre bonne 2026-04-02 12:53:46 +02:00
Georges-Leonard Prunet 4b3909c1a3 auto log out + clean leave 2026-04-02 12:52:06 +02:00
Georges-Leonard Prunet 4fa835b62a clean 2026-04-02 11:58:46 +02:00
11 changed files with 484 additions and 136 deletions
+3
View File
@@ -0,0 +1,3 @@
INTRA_CLIENT_ID=u-s4t2ud-c226cd35cd1ac08a4c6668deee1c64d7d67a13a766aee672acafd4a1522d483c
INTRA_CLIENT_SECRET=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
+2
View File
@@ -12,6 +12,7 @@ import playerStatsRouter from './routes/player_stats.js';
import {waitForDb, createTables, runMigrations, ensureOauthClient} from './db.js'; import {waitForDb, createTables, runMigrations, ensureOauthClient} from './db.js';
import setupSocketIO from './services/socket.js'; import setupSocketIO from './services/socket.js';
import avatarService from './services/avatar.js'; import avatarService from './services/avatar.js';
import intraRouter from './routes/intra.js';
const app = express(); const app = express();
const httpsOptions = { const httpsOptions = {
@@ -53,6 +54,7 @@ async function startServer()
app.use('/api/avatar', avatarRouter); app.use('/api/avatar', avatarRouter);
app.use('/api/friends', friendsRouter); app.use('/api/friends', friendsRouter);
app.use('/api/stats', playerStatsRouter); app.use('/api/stats', playerStatsRouter);
app.use('/api/intra', intraRouter);
app.get('/api', (req, res) => res.send('Backend running')); app.get('/api', (req, res) => res.send('Backend running'));
server.listen(3001, () => server.listen(3001, () =>
@@ -0,0 +1,53 @@
// 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;
+45 -37
View File
@@ -140,6 +140,47 @@ 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) function setupSocketIO(io)
{ {
ioInstance = io; ioInstance = io;
@@ -276,6 +317,7 @@ function setupSocketIO(io)
username: socket.user.username, username: socket.user.username,
userId: socket.user.userId userId: socket.user.userId
}); });
handlePlayerDeparture(io, roomId, socket.user.username);
socket.leave(roomId); socket.leave(roomId);
console.log(`${socket.user.username} left ${roomId}`); console.log(`${socket.user.username} left ${roomId}`);
@@ -574,7 +616,7 @@ function setupSocketIO(io)
// Points: 10 per letter found, -5 for wrong guess // Points: 10 per letter found, -5 for wrong guess
if (success) { if (success) {
points = lettersFound * 10; points = lettersFound * 5;
gameState.scores[username] += points; gameState.scores[username] += points;
} else { } else {
points = -5; points = -5;
@@ -673,42 +715,7 @@ function setupSocketIO(io)
message: `${username} a quitté la partie` message: `${username} a quitté la partie`
}); });
const gameState = gameRooms.get(roomId); handlePlayerDeparture(io, roomId, username);
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); await checkAndStopSinglePlayerGame(io, roomId, dbRoomId);
@@ -956,6 +963,7 @@ function setupSocketIO(io)
username: socket.user.username, username: socket.user.username,
userId: socket.user.userId userId: socket.user.userId
}); });
handlePlayerDeparture(io, roomId, socket.user.username);
// Get updated player list and broadcast // Get updated player list and broadcast
if (dbRoomId) { if (dbRoomId) {
+48
View File
@@ -3,6 +3,8 @@
* Initializes windows and handles menu interactions * Initializes windows and handles menu interactions
*/ */
import { windowRegistry } from './core/windows.js'; 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 { LoginWindow } from './windows/login.js';
import { LogoutWindow } from './windows/logout.js'; import { LogoutWindow } from './windows/logout.js';
import { GlobalChat } from './windows/global_chat.js'; import { GlobalChat } from './windows/global_chat.js';
@@ -17,6 +19,7 @@ import { StatsWindow } from './windows/stats.js';
*/ */
class App { class App {
constructor() { constructor() {
this.invalidateStaleToken();
this.initWindows(); this.initWindows();
this.initMenu(); this.initMenu();
this.initPage(); this.initPage();
@@ -24,6 +27,51 @@ class App {
this.colorizeUI(); 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 * Initializes all windows
*/ */
@@ -82,6 +82,7 @@ html {
body { body {
margin: 0; margin: 0;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
color: var(--color-text); color: var(--color-text);
line-height: 1.5; line-height: 1.5;
@@ -0,0 +1,53 @@
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();
}
}
@@ -0,0 +1,61 @@
/* /////////////////////////////////////////// */
// 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,79 +1,106 @@
:root { body {
--color-primary: #ffc75e; margin: 0;
--color-primary-hover: #ffc75e; height: 100vh;
--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; display: flex;
flex-direction: column; justify-content: center;
gap: var(--spacing-lg); align-items: center;
background: linear-gradient(135deg, #1f001f, #1f1f00);
z-index: var(--z-menu); overflow: hidden;
} }
.game__item { h1 {
background: var(--color-surface); font-size: 4rem;
color: var(--color-text); color: white;
border: 1px solid var(--color-surface-light); animation: float 2s ease-in-out infinite alternate;
border-radius: var(--radius-lg); border: 5px double #654050;
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;
} }
.game__item:hover { @keyframes float {
from { transform: translateY(0); }
to { transform: translateY(-20px); }
}
/* /////////////////////////////////////////// */
.box:hover {
background: var(--color-surface-light); background: var(--color-surface-light);
font-size: var(--font-size-lg); font-size: var(--font-size-lg);
animation: bobble 0.4s ease-in-out infinite alternate;
} }
.game__item--active { @keyframes bobble {
background: var(--color-primary); from {
border-color: var(--color-primary); transform: translateX(-5px);
}
to {
transform: translateX(5px);
}
}
.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,45 +5,24 @@
<title>Wiskas</title> <title>Wiskas</title>
<link rel="stylesheet" href="wiskas.css" /> <link rel="stylesheet" href="wiskas.css" />
<style> <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> </style>
</head> </head>
<body> <body>
<h1 id="helloText">METS TON CHAT ICI</h1> <h1 id="helloText"></h1>
<nav class="game" aria-label="Game"> <nav class="game" aria-label="Game">
<button class="game__item" data-action="Home page" aria-label="Home Page" <button class="box multicolor" data-action="Home page" aria-label="Home Page"
onclick="window.location.href='../index.html'">Home Page</button> onclick="window.location.href='../index.html'">Home Page</button>
</nav> </nav>
<script> <nav class="login" aria-label="Game">
const colors = ["#ff4b5c", "#56cfe1", "#80ed99", "#ffd166"]; <button class="box multicolor" aria-label="Home Page" id="login-button">Login</button>
const text = document.getElementById("helloText"); </nav>
setInterval(() => { <div id="login"></div>
const randomColor = colors[Math.floor(Math.random() * colors.length)];
text.style.color = randomColor; <script type="module" src="./wiskas.js"></script>
}, 500);
</script>
</body> </body>
</html> </html>
@@ -0,0 +1,113 @@
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() })