Compare commits
8 Commits
b31436a40a
...
b40dad8f57
| Author | SHA1 | Date | |
|---|---|---|---|
| b40dad8f57 | |||
| 23cca7a249 | |||
| e1e529b3ca | |||
| e96c16819d | |||
| aefb858247 | |||
| a9f81b4d65 | |||
| 4b3909c1a3 | |||
| 4fa835b62a |
@@ -0,0 +1,3 @@
|
||||
|
||||
INTRA_CLIENT_ID=u-s4t2ud-c226cd35cd1ac08a4c6668deee1c64d7d67a13a766aee672acafd4a1522d483c
|
||||
INTRA_CLIENT_SECRET=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
|
||||
@@ -12,6 +12,7 @@ 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 = {
|
||||
@@ -53,6 +54,7 @@ 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, () =>
|
||||
|
||||
@@ -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;
|
||||
@@ -30,6 +30,63 @@ async function broadcastRoomsList(io) {
|
||||
}
|
||||
}
|
||||
|
||||
function startRoomTimer(io, roomId, seconds)
|
||||
{
|
||||
const gameState = gameRooms.get(roomId);
|
||||
if (!gameState) return;
|
||||
|
||||
if (gameState.timerInterval)
|
||||
clearInterval(gameState.timerInterval);
|
||||
|
||||
gameState.timerSeconds = seconds;
|
||||
|
||||
gameState.timerInterval = setInterval(() => {
|
||||
gameState.timerSeconds--;
|
||||
|
||||
if (gameState.timerSeconds < 0)
|
||||
gameState.timerSeconds = 0;
|
||||
|
||||
if (gameState.timerSeconds <= 0)
|
||||
{
|
||||
io.to(roomId).emit('game-timer-sync', {
|
||||
remaining: 0
|
||||
});
|
||||
clearInterval(gameState.timerInterval);
|
||||
gameState.timerInterval = null;
|
||||
io.to(roomId).emit('game-timer-ended', { message: 'Temps écoulé !' });
|
||||
|
||||
gameState.currentPlayerIndex = (gameState.currentPlayerIndex + 1) % gameState.players.length;
|
||||
const nextDrawer = gameState.players[gameState.currentPlayerIndex];
|
||||
gameState.drawer = nextDrawer;
|
||||
|
||||
|
||||
gameState.currentWord = '';
|
||||
gameState.revealedLetters = [];
|
||||
gameState.revealedWord = [];
|
||||
gameState.guessedLetters = [];
|
||||
gameState.wrongGuesses = 0;
|
||||
|
||||
io.to(roomId).emit('game-new-round', {
|
||||
drawer: nextDrawer
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
io.to(roomId).emit('game-timer-sync', {
|
||||
remaining: gameState.timerSeconds
|
||||
});
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function stopRoomTimer(roomId)
|
||||
{
|
||||
const gameState = gameRooms.get(roomId);
|
||||
if (!gameState || !gameState.timerInterval) return;
|
||||
clearInterval(gameState.timerInterval);
|
||||
gameState.timerInterval = null;
|
||||
}
|
||||
|
||||
// Check if a playing game has only 1 player left and auto-stop it
|
||||
async function checkAndStopSinglePlayerGame(io, roomId, dbRoomId) {
|
||||
if (!dbRoomId) return;
|
||||
@@ -43,6 +100,7 @@ async function checkAndStopSinglePlayerGame(io, roomId, dbRoomId) {
|
||||
const players = await gameRoomService.getRoomPlayers(dbRoomId);
|
||||
if (players.length <= 1) {
|
||||
console.log(`Room ${dbRoomId} has only ${players.length} player(s) left, ending game`);
|
||||
stopRoomTimer(roomId);
|
||||
|
||||
// Update room status to 'ended'
|
||||
await gameRoomService.updateRoomStatus(dbRoomId, 'waiting');
|
||||
@@ -82,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)
|
||||
{
|
||||
ioInstance = io;
|
||||
@@ -192,7 +291,9 @@ function setupSocketIO(io)
|
||||
revealedLetters: gameState.revealedLetters,
|
||||
revealedWord: gameState.revealedWord || [],
|
||||
guessedLetters: gameState.guessedLetters,
|
||||
players: gameState.players
|
||||
players: gameState.players,
|
||||
scores: gameState.scores || {},
|
||||
timer: gameState.timerSeconds || 0
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -216,6 +317,7 @@ 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}`);
|
||||
|
||||
@@ -277,7 +379,8 @@ function setupSocketIO(io)
|
||||
revealedWord: gameState.revealedWord || [],
|
||||
guessedLetters: gameState.guessedLetters,
|
||||
players: gameState.players,
|
||||
scores: gameState.scores || {}
|
||||
scores: gameState.scores || {},
|
||||
timer: gameState.timerSeconds || 0
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -399,6 +502,7 @@ function setupSocketIO(io)
|
||||
const gameState = gameRooms.get(roomId);
|
||||
if (!gameState) return;
|
||||
|
||||
startRoomTimer(io, roomId, 60);
|
||||
gameState.currentWord = data.word.toLowerCase();
|
||||
gameState.revealedLetters = new Array(data.word.length).fill(false);
|
||||
gameState.revealedWord = new Array(data.word.length).fill('_');
|
||||
@@ -512,7 +616,7 @@ function setupSocketIO(io)
|
||||
|
||||
// Points: 10 per letter found, -5 for wrong guess
|
||||
if (success) {
|
||||
points = lettersFound * 10;
|
||||
points = lettersFound * 5;
|
||||
gameState.scores[username] += points;
|
||||
} else {
|
||||
points = -5;
|
||||
@@ -561,6 +665,8 @@ function setupSocketIO(io)
|
||||
// Update round start scores for next round
|
||||
gameState.roundStartScores = { ...gameState.scores };
|
||||
|
||||
stopRoomTimer(roomId);
|
||||
|
||||
io.to(roomId).emit('game-word-found', {
|
||||
word: gameState.currentWord,
|
||||
winner: username,
|
||||
@@ -609,40 +715,7 @@ function setupSocketIO(io)
|
||||
message: `${username} a quitté la partie`
|
||||
});
|
||||
|
||||
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)
|
||||
{
|
||||
// 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`
|
||||
});
|
||||
}
|
||||
}
|
||||
handlePlayerDeparture(io, roomId, username);
|
||||
|
||||
await checkAndStopSinglePlayerGame(io, roomId, dbRoomId);
|
||||
|
||||
@@ -661,6 +734,7 @@ function setupSocketIO(io)
|
||||
socket.on('game-end', async () => {
|
||||
const roomId = socket.gameRoomId;
|
||||
if (!roomId) return;
|
||||
stopRoomTimer(roomId);
|
||||
|
||||
// Update room status to 'waiting' in database
|
||||
const dbRoomId = socket.gameRoomDbId;
|
||||
@@ -889,6 +963,7 @@ 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) {
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* 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';
|
||||
@@ -17,6 +19,7 @@ import { StatsWindow } from './windows/stats.js';
|
||||
*/
|
||||
class App {
|
||||
constructor() {
|
||||
this.invalidateStaleToken();
|
||||
this.initWindows();
|
||||
this.initMenu();
|
||||
this.initPage();
|
||||
@@ -24,6 +27,51 @@ 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,6 +82,7 @@ html {
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
color: var(--color-text);
|
||||
line-height: 1.5;
|
||||
|
||||
@@ -34,6 +34,7 @@ export class GameRoomWindow extends Window {
|
||||
});
|
||||
|
||||
this.updateTabsAccess();
|
||||
this.loadCurrentTab();
|
||||
|
||||
// Verifier si l'utilisateur est deja dans un salon au chargement
|
||||
const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
|
||||
@@ -119,7 +120,8 @@ export class GameRoomWindow extends Window {
|
||||
this.gameInfo = this.createElement('div', 'gameroom__game-info');
|
||||
this.currentDrawerInfo = this.createElement('div', 'gameroom__drawer-info', { text: '' });
|
||||
this.scoresDisplay = this.createElement('div', 'gameroom__scores-display');
|
||||
this.gameInfo.append(this.currentDrawerInfo, this.scoresDisplay);
|
||||
this.timerDisplay = this.createElement('div', 'gameroom__timer-display');
|
||||
this.gameInfo.append(this.currentDrawerInfo, this.scoresDisplay, this.timerDisplay);
|
||||
|
||||
// Affichage du mot caché
|
||||
this.wordDisplay = this.createElement('div', 'gameroom__word-display');
|
||||
@@ -196,7 +198,8 @@ export class GameRoomWindow extends Window {
|
||||
guessedLetters: [],
|
||||
scores: {},
|
||||
counter: 0,
|
||||
counterRound: 0
|
||||
counterRound: 0,
|
||||
timer: 0
|
||||
};
|
||||
|
||||
this.initDrawing();
|
||||
@@ -521,6 +524,8 @@ export class GameRoomWindow extends Window {
|
||||
this.gameState.revealedWord = data.revealedWord || new Array(data.wordLength).fill('_');
|
||||
this.gameState.players = data.players;
|
||||
this.gameState.scores = data.scores || {};
|
||||
this.gameState.timer = data.timer || 0;
|
||||
this.updateTimerUI();
|
||||
|
||||
this.showGameUI();
|
||||
this.updateWordDisplay();
|
||||
@@ -610,6 +615,15 @@ export class GameRoomWindow extends Window {
|
||||
// Setup UI for new round with new drawer
|
||||
this.setupRound();
|
||||
});
|
||||
|
||||
this.socket.on('game-timer-sync', (data) => {
|
||||
this.gameState.timer = data.remaining;
|
||||
this.updateTimerUI();
|
||||
});
|
||||
|
||||
this.socket.on('game-timer-ended', (data) => {
|
||||
this.showMessage(data.message || 'Temps écoulé !', 'info');
|
||||
})
|
||||
}
|
||||
|
||||
disconnectGameSocket() {
|
||||
@@ -1244,6 +1258,12 @@ export class GameRoomWindow extends Window {
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
updateTimerUI()
|
||||
{
|
||||
if (this.timerDisplay)
|
||||
this.timerDisplay.textContent = `Temps restant : ${this.gameState.timer}s`;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// LOGIQUE DU JEU
|
||||
// ============================================
|
||||
@@ -1298,6 +1318,9 @@ export class GameRoomWindow extends Window {
|
||||
this.isSpectating = false;
|
||||
|
||||
this.gameState.scores = {};
|
||||
this.gameState.counter = 0;
|
||||
this.gameState.counterRound = 0;
|
||||
this.gameState.timer = 0;
|
||||
this.gameState.players = [];
|
||||
this.gameState.currentPlayerIndex = 0;
|
||||
this.gameState.guessedLetters = [];
|
||||
|
||||
@@ -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 {
|
||||
--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;
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-lg);
|
||||
|
||||
z-index: var(--z-menu);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #1f001f, #1f1f00);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.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;
|
||||
h1 {
|
||||
font-size: 4rem;
|
||||
color: white;
|
||||
animation: float 2s ease-in-out infinite alternate;
|
||||
border: 5px double #654050;
|
||||
}
|
||||
|
||||
.game__item:hover {
|
||||
@keyframes float {
|
||||
from { transform: translateY(0); }
|
||||
to { transform: translateY(-20px); }
|
||||
}
|
||||
|
||||
/* /////////////////////////////////////////// */
|
||||
.box:hover {
|
||||
background: var(--color-surface-light);
|
||||
font-size: var(--font-size-lg);
|
||||
|
||||
animation: bobble 0.4s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.game__item--active {
|
||||
background: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
@keyframes bobble {
|
||||
from {
|
||||
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>
|
||||
<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">METS TON CHAT ICI</h1>
|
||||
<h1 id="helloText"></h1>
|
||||
|
||||
<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>
|
||||
</nav>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
const colors = ["#ff4b5c", "#56cfe1", "#80ed99", "#ffd166"];
|
||||
const text = document.getElementById("helloText");
|
||||
<nav class="login" aria-label="Game">
|
||||
<button class="box multicolor" aria-label="Home Page" id="login-button">Login</button>
|
||||
</nav>
|
||||
|
||||
setInterval(() => {
|
||||
const randomColor = colors[Math.floor(Math.random() * colors.length)];
|
||||
text.style.color = randomColor;
|
||||
}, 500);
|
||||
</script>
|
||||
<div id="login"></div>
|
||||
|
||||
<script type="module" src="./wiskas.js"></script>
|
||||
</body>
|
||||
</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() })
|
||||
|
||||
Reference in New Issue
Block a user