diff --git a/Transcendence/Transcendance-Test.zip b/Transcendence/Transcendance-Test.zip deleted file mode 100644 index 766bda1..0000000 Binary files a/Transcendence/Transcendance-Test.zip and /dev/null differ diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/db.js b/Transcendence/Transcendance-Test/Transcendence/srcs/backend/db.js deleted file mode 100755 index afc4f32..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/db.js +++ /dev/null @@ -1,184 +0,0 @@ -import 'dotenv/config'; -import { Pool } from 'pg'; - -const pool = new Pool -({ - user: process.env.POSTGRES_USER, - host: process.env.POSTGRES_HOST, - database: process.env.POSTGRES_DB, - password: process.env.POSTGRES_PASSWORD, - port: 5432, -}); - -async function waitForDb(retries = 10, delay = 2000) -{ - for (let i = 0; i < retries; i++) - { - try - { - await pool.query('SELECT 1'); - console.log('Database is ready!'); - return ; - } - catch (err) - { - await new Promise(r => setTimeout(r, delay)); - } - } - throw new Error('Could not connect to database after multiple attempts'); -} - -async function runMigrations() -{ - try - { - // Add total_points column if it doesn't exist - await pool.query(` - DO $$ - BEGIN - IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='total_points') THEN - ALTER TABLE users ADD COLUMN total_points INT DEFAULT 0; - END IF; - IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='games_played') THEN - ALTER TABLE users ADD COLUMN games_played INT DEFAULT 0; - END IF; - IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='games_won') THEN - ALTER TABLE users ADD COLUMN games_won INT DEFAULT 0; - END IF; - END $$; - `); - console.log('Migrations completed!'); - } - catch (err) - { - console.error('Error running migrations:', err); - } -} - -async function createTables() -{ - try - { - await pool.query(` - CREATE TABLE IF NOT EXISTS users ( - id SERIAL PRIMARY KEY, - username VARCHAR(50) UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - email VARCHAR(100), - avatar_url TEXT DEFAULT '/avatar/default.png', - total_points INT DEFAULT 0, - games_played INT DEFAULT 0, - games_won INT DEFAULT 0, - created_at TIMESTAMP DEFAULT NOW() - ); - - CREATE TABLE IF NOT EXISTS messages( - id SERIAL PRIMARY KEY, - sender_id INT REFERENCES users(id), - received_id INT REFERENCES users(id), - content TEXT, - created_at TIMESTAMP DEFAULT NOW() - ); - - CREATE TABLE IF NOT EXISTS friendship ( - id_user1 INT NOT NULL, - id_user2 INT NOT NULL, - status VARCHAR(20) NOT NULL, - created_at TIMESTAMP DEFAULT NOW(), - CHECK (id_user1 < id_user2), - PRIMARY KEY (id_user1, id_user2), - FOREIGN KEY (id_user1) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (id_user2) REFERENCES users(id) ON DELETE CASCADE - ); - - CREATE TABLE IF NOT EXISTS oauth_clients ( - id SERIAL PRIMARY KEY, - provider VARCHAR(50) NOT NULL, - client_id VARCHAR(200) NOT NULL, - client_secret TEXT, - redirect_uri VARCHAR(255), - created_at TIMESTAMP DEFAULT NOW(), - UNIQUE(provider, client_id) - ); - - CREATE TABLE IF NOT EXISTS game_rooms ( - id SERIAL PRIMARY KEY, - name VARCHAR(100) NOT NULL, - status VARCHAR(20) DEFAULT 'waiting', - max_players INT DEFAULT 8, - current_round INT DEFAULT 0, - max_rounds INT DEFAULT 3, - round_duration INT DEFAULT 90, - created_at TIMESTAMP DEFAULT NOW(), - started_at TIMESTAMP, - ended_at TIMESTAMP - ); - - CREATE TABLE IF NOT EXISTS game_players ( - id SERIAL PRIMARY KEY, - room_id INT REFERENCES game_rooms(id) ON DELETE CASCADE, - user_id INT REFERENCES users(id) ON DELETE CASCADE, - score INT DEFAULT 0, - is_drawing BOOLEAN DEFAULT FALSE, - joined_at TIMESTAMP DEFAULT NOW(), - UNIQUE(room_id, user_id) - ); - - CREATE TABLE IF NOT EXISTS words ( - id SERIAL PRIMARY KEY, - word VARCHAR(50) NOT NULL UNIQUE - ); - - CREATE TABLE IF NOT EXISTS game_rounds ( - id SERIAL PRIMARY KEY, - room_id INT REFERENCES game_rooms(id) ON DELETE CASCADE, - round_number INT NOT NULL, - word_id INT REFERENCES words(id), - drawer_id INT REFERENCES users(id), - started_at TIMESTAMP DEFAULT NOW(), - ended_at TIMESTAMP - ); - `); - console.log('Tables created!'); - } - catch (err) - { - console.error('Error creating tables:', err); - } -} - -async function query(text, params) -{ - return (pool.query(text, params)); -} - -async function ensureOauthClient(provider, client_id, client_secret, redirect_uri) -{ - try - { - const res = await pool.query( - `SELECT id FROM oauth_clients WHERE provider = $1 AND client_id = $2`, [provider, client_id] - ); - if (res.rows.length > 0) - return res.rows[0]; - const insert = await pool.query( - `INSERT INTO oauth_clients (provider, client_id, client_secret, redirect_uri) VALUES ($1, $2, $3, $4) RETURNING id`, - [provider, client_id, client_secret, redirect_uri] - ); - return insert.rows[0]; - } - catch (err) - { - console.error('Error ensuring oauth client:', err); - throw err; - } -} - -export -{ - waitForDb, - createTables, - runMigrations, - query, - ensureOauthClient -}; diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/dockerfile b/Transcendence/Transcendance-Test/Transcendence/srcs/backend/dockerfile deleted file mode 100755 index b660389..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM node:20-alpine - -WORKDIR /app - -COPY package*.json ./ - -RUN npm install - -COPY . . - -EXPOSE 3001 - -ENV NODE_ENV=development - -CMD ["node", "index.js"] diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/index.js b/Transcendence/Transcendance-Test/Transcendence/srcs/backend/index.js deleted file mode 100755 index c09c61b..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/index.js +++ /dev/null @@ -1,59 +0,0 @@ -import express from 'express'; -import http from 'http'; -import cors from 'cors'; -import {Server} from 'socket.io'; -import authRouter from './routes/auth.js'; -import chatRouter from './routes/global_chat.js'; -import gameRoomRouter from './routes/game_room.js'; -import avatarRouter from './routes/avatar.js'; -import friendsRouter from './routes/friends.js'; -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'; - -const app = express(); -const server = http.createServer(app); -const io = new Server(server, -{ - cors: - { - origin: "*", - methods: ["GET", "POST"] - } -}); - -app.use(cors()); -app.use(express.json()); - -setupSocketIO(io); - -async function startServer() -{ - await waitForDb(); - await createTables(); - await runMigrations(); - - // Ensure GitHub OAuth client is registered in DB - try { - await ensureOauthClient('github', process.env.GITHUB_CLIENT_ID, process.env.GITHUB_CLIENT_SECRET, process.env.GITHUB_CALLBACK_URL || process.env.GITHUB_REDIRECT_URI); - } catch (e) { - console.warn('OAuth client might already exist or failed to register:', e.message); - } - - app.use('/avatar', express.static(avatarService.AVATAR_DIR)); - app.use('/api/auth', authRouter); - app.use('/api/global_chat', chatRouter); - app.use('/api/rooms', gameRoomRouter); - app.use('/api/avatar', avatarRouter); - app.use('/api/friends', friendsRouter); - app.use('/api/stats', playerStatsRouter); - app.get('/api', (req, res) => res.send('Backend running')); - - server.listen(3001, () => - { - console.log('Server ready and listening'); - }); -} - -startServer(); \ No newline at end of file diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/package.json b/Transcendence/Transcendance-Test/Transcendence/srcs/backend/package.json deleted file mode 100755 index a83699d..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "type": "module", - "dependencies": - { - "express": "^4.18.2", - "pg": "^8.11.3", - "bcrypt": "^5.1.0", - "jsonwebtoken": "^9.0.2", - "dotenv": "^17.2.3", - "socket.io": "^4.6.1", - "cors": "^2.8.5", - "passport": "0.7.0", - "passport-github2": "0.1.12", - "express-session": "1.18.0", - "multer": "^1.4.5-lts.1", - "file-type": "^19.0.0" - } -} \ No newline at end of file diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/routes/auth.js b/Transcendence/Transcendance-Test/Transcendence/srcs/backend/routes/auth.js deleted file mode 100755 index f80ff21..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/routes/auth.js +++ /dev/null @@ -1,88 +0,0 @@ -import express from 'express'; -import authService from '../services/auth.js'; -import fetch from 'node-fetch'; -import bcrypt from 'bcrypt'; -import jwt from 'jsonwebtoken'; -import {query} from '../db.js'; -import crypto from 'crypto'; - -const router = express.Router(); - -router.post('/register', async(req, res) => -{ - const {username, password} = req.body; - if (!username || !password) - return (res.status(400).json({error: 'Missing fields'})); - - const result = await authService.register(username, password); - res.status(result.status).json(result.data); -}); - -router.post('/login', async(req, res) => -{ - console.log("received login!"); - const {username, password} = req.body; - const result = await authService.login(username, password); - res.status(result.status).json(result.data); -}); - -router.get('/github', (req, res) => { - const githubAuthUrl = `https://github.com/login/oauth/authorize?` + - `client_id=${process.env.GITHUB_CLIENT_ID}&` + - `redirect_uri=${encodeURIComponent(process.env.GITHUB_CALLBACK_URL || process.env.GITHUB_REDIRECT_URI)}&` + - `scope=user:email`; - - res.redirect(githubAuthUrl); -}); - -router.get('/github/callback', async (req, res) => { - const code = req.query.code; - if (!code) { - return res.status(400).send('Missing code'); - } - try { - const tokenResponse = await fetch('https://github.com/login/oauth/access_token', { - method: 'POST', - headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, - body: JSON.stringify({ - client_id: process.env.GITHUB_CLIENT_ID, - client_secret: process.env.GITHUB_CLIENT_SECRET, - code: code - }) - }); - const tokenData = await tokenResponse.json(); - const accessToken = tokenData.access_token; - if (!accessToken) throw new Error('No access token'); - - - const userResponse = await fetch('https://api.github.com/user', { - headers: { 'Authorization': `Bearer ${accessToken}`, 'User-Agent': 'Transcendence' } - }); - const ghUser = await userResponse.json(); - const ghUsername = ghUser.login || `github_${ghUser.id}`; - - - let result = await query(`SELECT id FROM users WHERE username = $1`, [ghUsername]); - let userId; - if (result.rows.length > 0) { - userId = result.rows[0].id; - } else { - const randomPwd = crypto.randomBytes(16).toString('hex'); - const passwordHash = await bcrypt.hash(randomPwd, 10); - await query(`INSERT INTO users (username, password_hash) VALUES ($1, $2)`, [ghUsername, passwordHash]); - const inserted = await query(`SELECT id FROM users WHERE username = $1`, [ghUsername]); - userId = inserted.rows[0].id; - } - - // Issue JWT - const token = jwt.sign({ userId: userId, username: ghUsername }, process.env.JWT_SECRET, { expiresIn: '1h' }); - - // Send token to opener window and close popup - res.send(``); - } catch (err) { - console.error(err); - res.status(500).send('GitHub OAuth error'); - } -}); - -export default router; diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/routes/avatar.js b/Transcendence/Transcendance-Test/Transcendence/srcs/backend/routes/avatar.js deleted file mode 100755 index 117f5e1..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/routes/avatar.js +++ /dev/null @@ -1,51 +0,0 @@ -import express from 'express'; -import multer from 'multer'; -import avatarService from '../services/avatar.js'; -import authenticateToken from '../middleware/auth.js'; - -const router = express.Router(); - -// Configue multer to use RAM -const storage = multer.memoryStorage(); -const upload = multer -({ - storage: storage, - limits: - { - fileSize: 5 * 1024 * 1024 // 5mb - } -}); - -router.post('/upload', authenticateToken, upload.single('avatar'), async(req, res) => -{ - if (!req.file) - return res.status(400).json({ error: 'No file uploaded' }); - - const result = await avatarService.uploadAvatar(req.user.userId, req.file); - res.status(result.status).json(result.data); -}); - -router.delete('/', authenticateToken, async(req, res) => -{ - const result = await avatarService.deleteAvatar(req.user.userId); - res.status(result.status).json(result.data); -}); - -router.get('/me', authenticateToken, async(req, res) => -{ - console.log('GET /me hit, user:', req.user); - const result = await avatarService.getAvatarUrl(req.user.userId); - res.status(result.status).json(result.data); -}); - -router.get('/user/:userId', async(req, res) => -{ - const userId = parseInt(req.params.userId); - if (isNaN(userId)) - return res.status(400).json({ error: 'Invalid user ID' }); - - const result = await avatarService.getAvatarUrl(userId); - res.status(result.status).json(result.data); -}); - -export default router; \ No newline at end of file diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/routes/friends.js b/Transcendence/Transcendance-Test/Transcendence/srcs/backend/routes/friends.js deleted file mode 100755 index 0722388..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/routes/friends.js +++ /dev/null @@ -1,69 +0,0 @@ -import express from 'express'; -import friendsService from '../services/friends.js'; -import authenticateToken from '../middleware/auth.js'; - -const router = express.Router(); - -// Get friends list -router.get('/', authenticateToken, async (req, res) => { - const result = await friendsService.getFriends(req.user.userId); - res.status(result.status).json(result.data); -}); - -// Get pending friend requests -router.get('/requests', authenticateToken, async (req, res) => { - const result = await friendsService.getPendingRequests(req.user.userId); - res.status(result.status).json(result.data); -}); - -// Search users -router.get('/search', authenticateToken, async (req, res) => { - const { q } = req.query; - if (!q || q.trim().length === 0) { - return res.status(400).json({ error: 'Search query required' }); - } - const result = await friendsService.searchUsers(req.user.userId, q.trim()); - res.status(result.status).json(result.data); -}); - -// Send friend request -router.post('/request/:userId', authenticateToken, async (req, res) => { - const toUserId = parseInt(req.params.userId); - if (isNaN(toUserId)) { - return res.status(400).json({ error: 'Invalid user ID' }); - } - const result = await friendsService.sendFriendRequest(req.user.userId, toUserId); - res.status(result.status).json(result.data); -}); - -// Accept friend request -router.post('/accept/:userId', authenticateToken, async (req, res) => { - const fromUserId = parseInt(req.params.userId); - if (isNaN(fromUserId)) { - return res.status(400).json({ error: 'Invalid user ID' }); - } - const result = await friendsService.acceptFriendRequest(req.user.userId, fromUserId); - res.status(result.status).json(result.data); -}); - -// Decline friend request -router.post('/decline/:userId', authenticateToken, async (req, res) => { - const fromUserId = parseInt(req.params.userId); - if (isNaN(fromUserId)) { - return res.status(400).json({ error: 'Invalid user ID' }); - } - const result = await friendsService.declineFriendRequest(req.user.userId, fromUserId); - res.status(result.status).json(result.data); -}); - -// Remove friend -router.delete('/:userId', authenticateToken, async (req, res) => { - const friendId = parseInt(req.params.userId); - if (isNaN(friendId)) { - return res.status(400).json({ error: 'Invalid user ID' }); - } - const result = await friendsService.removeFriend(req.user.userId, friendId); - res.status(result.status).json(result.data); -}); - -export default router; diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/routes/game_room.js b/Transcendence/Transcendance-Test/Transcendence/srcs/backend/routes/game_room.js deleted file mode 100755 index b144b71..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/routes/game_room.js +++ /dev/null @@ -1,185 +0,0 @@ -import express from 'express'; -import gameRoomService from '../services/game_room.js'; -import authenticateToken from '../middleware/auth.js'; -import { getIO, broadcastRoomsList } from '../services/socket.js'; -const router = express.Router(); - -router.get('/', authenticateToken, async(req, res) => -{ - try - { - const rooms = await gameRoomService.listActiveRooms(); - res.json(rooms); - } - catch (err) - { - console.error(err); - res.status(500).json({error: 'Server error'}); - } -}); - -// Get list of rooms currently being played (for spectators) -router.get('/playing', authenticateToken, async(req, res) => -{ - try - { - const rooms = await gameRoomService.listPlayingRooms(); - res.json(rooms); - } - catch (err) - { - console.error(err); - res.status(500).json({error: 'Server error'}); - } -}); - -// IMPORTANT: This route must be before /:roomId to avoid "current" being interpreted as a roomId -router.get('/current', authenticateToken, async(req, res) => -{ - try - { - const room = await gameRoomService.getCurrentRoom(req.user.userId); - if (!room) - return res.status(204).send(); // No content - user is not in any room - res.json(room); - } - catch(err) - { - console.error(err); - res.status(500).json({error: 'Server error'}); - } -}); - -router.get('/:roomId', authenticateToken, async(req, res) => -{ - try - { - const room = await gameRoomService.getRoomById(req.params.roomId); - if (!room) - return (res.status(404).json({error: 'Room not found'})); - res.json(room); - } - catch(err) - { - console.error(err); - res.status(500).json({error: 'Server error'}); - } -}); - -router.get('/:roomId/players', authenticateToken, async(req, res) => -{ - try - { - const players = await gameRoomService.getRoomPlayers(req.params.roomId); - res.json(players); - } - catch(err) - { - console.error(err); - res.status(500).json({error: 'Server error'}); - } -}); - -router.post('/', authenticateToken, async(req, res) => -{ - try - { - const {name} = req.body; - if (!name) - return (res.status(400).json({error: 'Room name required'})); - const room = await gameRoomService.createRoom(name, req.user.userId); - - // Broadcast updated rooms list to all clients - const io = getIO(); - if (io) { - broadcastRoomsList(io); - } - - res.status(201).json(room); - } - catch(err) - { - console.error(err); - res.status(500).json({error: 'Server error'}); - } -}); - -router.post('/:roomId/join', authenticateToken, async(req, res) => -{ - try - { - const player = await gameRoomService.joinRoom(req.params.roomId, req.user.userId); - - // Broadcast updated rooms list to all clients - const io = getIO(); - if (io) { - broadcastRoomsList(io); - } - - res.json(player); - } - catch(err) - { - console.error(err); - if (err.message.includes('full') || err.message.includes('already')) - res.status(400).json({error: err.message}); - else - res.status(500).json({error: err.message}); - } -}); - -router.post('/:roomId/leave', authenticateToken, async(req, res) => -{ - try - { - await gameRoomService.leaveRoom(req.params.roomId, req.user.userId); - - // Broadcast updated rooms list to all clients - const io = getIO(); - if (io) { - broadcastRoomsList(io); - } - - res.json({message: 'Left room successfully'}); - } - catch(err) - { - console.error(err); - res.status(500).json({error: 'Server error'}); - } -}); - -// Join a room as spectator -router.post('/:roomId/spectate', authenticateToken, async(req, res) => -{ - try - { - const room = await gameRoomService.spectateRoom(req.params.roomId, req.user.userId); - res.json(room); - } - catch(err) - { - console.error(err); - if (err.message.includes('not found') || err.message.includes('not in playing') || err.message.includes('already in')) - res.status(400).json({error: err.message}); - else - res.status(500).json({error: err.message}); - } -}); - -// Leave spectator mode -router.post('/:roomId/leave-spectate', authenticateToken, async(req, res) => -{ - try - { - await gameRoomService.leaveSpectateRoom(req.params.roomId, req.user.userId); - res.json({message: 'Left spectator mode successfully'}); - } - catch(err) - { - console.error(err); - res.status(500).json({error: 'Server error'}); - } -}); - -export default router; \ No newline at end of file diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/routes/global_chat.js b/Transcendence/Transcendance-Test/Transcendence/srcs/backend/routes/global_chat.js deleted file mode 100755 index 41279c6..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/routes/global_chat.js +++ /dev/null @@ -1,20 +0,0 @@ -import express from 'express'; -import chatService from '../services/global_chat.js'; -import authenticateToken from '../middleware/auth.js'; -const router = express.Router(); - -router.get('/messages', authenticateToken, async(req, res) => -{ - try - { - const messages = await chatService.getRecentMessages(50); - res.json(messages); - } - catch(err) - { - console.error(err); - res.status(500).json({error: 'Server error'}); - } -}); - -export default router; \ No newline at end of file diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/routes/player_stats.js b/Transcendence/Transcendance-Test/Transcendence/srcs/backend/routes/player_stats.js deleted file mode 100755 index 661d715..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/routes/player_stats.js +++ /dev/null @@ -1,46 +0,0 @@ -import express from 'express'; -import playerStatsService from '../services/player_stats.js'; -import authenticateToken from '../middleware/auth.js'; -const router = express.Router(); - -// Get current user's stats -router.get('/me', authenticateToken, async (req, res) => { - try { - const stats = await playerStatsService.getStatsByUserId(req.user.userId); - if (!stats) { - return res.status(404).json({ error: 'User not found' }); - } - res.json(stats); - } catch (err) { - console.error('Error getting user stats:', err); - res.status(500).json({ error: 'Server error' }); - } -}); - -// Get stats by username -router.get('/user/:username', authenticateToken, async (req, res) => { - try { - const stats = await playerStatsService.getStatsByUsername(req.params.username); - if (!stats) { - return res.status(404).json({ error: 'User not found' }); - } - res.json(stats); - } catch (err) { - console.error('Error getting user stats:', err); - res.status(500).json({ error: 'Server error' }); - } -}); - -// Get leaderboard -router.get('/leaderboard', authenticateToken, async (req, res) => { - try { - const limit = Math.min(parseInt(req.query.limit) || 10, 50); - const leaderboard = await playerStatsService.getLeaderboard(limit); - res.json(leaderboard); - } catch (err) { - console.error('Error getting leaderboard:', err); - res.status(500).json({ error: 'Server error' }); - } -}); - -export default router; diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/services/auth.js b/Transcendence/Transcendance-Test/Transcendence/srcs/backend/services/auth.js deleted file mode 100755 index ca36c25..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/services/auth.js +++ /dev/null @@ -1,63 +0,0 @@ -import bcrypt from 'bcrypt'; -import jwt from 'jsonwebtoken'; -import {query} from '../db.js'; - -async function login(username, password) -{ - try - { - const result = await query - ( - `SELECT id, password_hash FROM users WHERE username = $1`, - [username] - ); - if (result.rows.length === 0) - return ({status: 401, data: {error: 'Invalid credentials'}}); - - const user = result.rows[0]; - const match = await bcrypt.compare(password, user.password_hash); - if (!match) - return ({status: 401, data: {error: 'Invalid credentials'}}); - - const token = jwt.sign - ( - { - userId: user.id, - username: username - }, - process.env.JWT_SECRET, - {expiresIn: '1h'} - ); - - return ({status: 200, data: {token}}); - } - catch (err) - { - console.error(err); - return ({status: 500, data: {error: 'Server error'}}); - } -}; - -async function register(username, password) -{ - try - { - const password_hash = await bcrypt.hash(password, 10); - await query - ( - `INSERT INTO users (username, password_hash) VALUES ($1, $2)`, - [username, password_hash] - ); - return ({status: 201, data: {message: 'User created'}}); - } - catch (err) - { - if (err.code === '23505') - return ({status: 409, data: {error: 'Username already exists'}}); - - console.error(err); - return ({status: 500, data: {error: 'Server error'}}); - } -}; - -export default {register, login}; diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/services/avatar.js b/Transcendence/Transcendance-Test/Transcendence/srcs/backend/services/avatar.js deleted file mode 100755 index 05d3b66..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/services/avatar.js +++ /dev/null @@ -1,148 +0,0 @@ -import {query} from '../db.js'; -import path from 'path'; -import fs from 'fs'; -import {fileURLToPath} from 'url'; -import {fileTypeFromBuffer} from 'file-type'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const AVATAR_DIR = path.join(__dirname, '../avatar'); -const DEFAULT_AVATAR = '/avatar/default.png'; -const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5mb -const ALLOWED_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; - -// Verify that the avatar folder already exists -if (!fs.existsSync(AVATAR_DIR)) -{ - fs.mkdirSync(AVATAR_DIR, { recursive: true }); -} - -async function uploadAvatar(userId, file) -{ - try - { - // File check (type and size) - if (!file) - return ({status: 400, data: {error: 'No file provided'}}); - - if (!ALLOWED_TYPES.includes(file.mimetype)) - return ({status: 400, data: { error: 'Invalid file type. Only JPEG, PNG, GIF, and WebP allowed'}}); - - const fileType = await fileTypeFromBuffer(file.buffer); - if (!fileType || !['image/jpeg', 'image/png', 'image/gif', 'image/webp'].includes(fileType.mime)) - return ({status: 400, data: {error: 'Invalid file content. File does not match allowed image types'}}); - - if (file.size > MAX_FILE_SIZE) - return ({status: 400, data: {error: 'File too large. Maximum size is 5MB'}}); - - - const currentAvatar = await getCurrentAvatar(userId); - if (currentAvatar === null) - return ({status: 404, data: {error: 'User not found'}}); - - // Create a unique name for the new avatar to avoid duplicates - const fileExt = path.extname(file.originalname); - const fileName = `user_${userId}_${Date.now()}${fileExt}`; - const avatarPath = `/avatar/${fileName}`; - - // Save the new avatar in the folder - const filePath = path.join(AVATAR_DIR, fileName); - fs.writeFileSync(filePath, file.buffer); - - await setAvatar(avatarPath, userId); - - deleteNonDefault(currentAvatar); - return ({status: 200, data: {avatar_url: avatarPath}}); - } - catch (err) - { - console.error('Avatar upload error:', err); - return { status: 500, data: { error: 'Server error' } }; - } -} - -async function deleteAvatar(userId) { - try - { - const currentAvatar = await getCurrentAvatar(userId); - if (currentAvatar === null) - return ({status: 404, data: {error: 'User not found'}}); - - // Reset the avatar to the default one - await setAvatar(DEFAULT_AVATAR, userId); - - deleteNonDefault(currentAvatar); - return ({status: 200, data: {avatar_url: DEFAULT_AVATAR}}); - } - catch (err) - { - console.error('Avatar delete error:', err); - return ({status: 500, data: {error: 'Server error'}}); - } -} - -async function setAvatar(newAvatar, userId) -{ - await query - ( - 'UPDATE users SET avatar_url = $1 WHERE id = $2', - [newAvatar, userId] - ); -} - -async function getCurrentAvatar(userId) -{ - const res = await query - ( - 'SELECT avatar_url FROM users WHERE id = $1', - [userId] - ); - if (res.rows.length === 0) - return (null); - return (res.rows[0].avatar_url); -} - -function deleteNonDefault(curAvatar) -{ - if (curAvatar && curAvatar !== DEFAULT_AVATAR) - { - const fileName = path.basename(curAvatar); - const filePath = path.join(AVATAR_DIR, fileName); - - if (fs.existsSync(filePath)) - fs.unlinkSync(filePath); - } -} - -async function getAvatarUrl(userId) -{ - try - { - const result = await query - ( - 'SELECT avatar_url FROM users WHERE id = $1', - [userId] - ); - - if (result.rows.length === 0) - return ({status: 404, data: {error: 'User not found'}}); - - const avatarUrl = result.rows[0].avatar_url || DEFAULT_AVATAR; - return ({status: 200, data: {avatar_url: avatarUrl}}); - } - catch (err) - { - console.error('Get avatar error:', err); - return ({status: 500, data: {error: 'Server error'}}); - } -} - -export default -{ - uploadAvatar, - deleteAvatar, - getAvatarUrl, - AVATAR_DIR, - DEFAULT_AVATAR -}; diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/services/friends.js b/Transcendence/Transcendance-Test/Transcendence/srcs/backend/services/friends.js deleted file mode 100755 index f912188..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/services/friends.js +++ /dev/null @@ -1,240 +0,0 @@ -import { query } from '../db.js'; - -/** - * Get list of friends for a user - */ -async function getFriends(userId) { - try { - const result = await query( - `SELECT u.id, u.username, u.avatar_url, u.total_points, u.games_played, u.games_won - FROM friendship f - JOIN users u ON ( - CASE - WHEN f.id_user1 = $1 THEN f.id_user2 = u.id - ELSE f.id_user1 = u.id - END - ) - WHERE (f.id_user1 = $1 OR f.id_user2 = $1) - AND f.status = 'accepted'`, - [userId] - ); - return { status: 200, data: { friends: result.rows } }; - } catch (err) { - console.error('Get friends error:', err); - return { status: 500, data: { error: 'Server error' } }; - } -} - -/** - * Get pending friend requests received by user - */ -async function getPendingRequests(userId) { - try { - const result = await query( - `SELECT u.id, u.username, u.avatar_url, f.created_at - FROM friendship f - JOIN users u ON ( - CASE - WHEN f.id_user1 = $1 THEN f.id_user2 = u.id - ELSE f.id_user1 = u.id - END - ) - WHERE (f.id_user1 = $1 OR f.id_user2 = $1) - AND f.status = 'pending_' || $1::text`, - [userId] - ); - return { status: 200, data: { requests: result.rows } }; - } catch (err) { - console.error('Get pending requests error:', err); - return { status: 500, data: { error: 'Server error' } }; - } -} - -/** - * Search users by username - */ -async function searchUsers(userId, searchTerm) { - try { - const result = await query( - `SELECT id, username, avatar_url - FROM users - WHERE username ILIKE $1 - AND id != $2 - LIMIT 20`, - [`%${searchTerm}%`, userId] - ); - return { status: 200, data: { users: result.rows } }; - } catch (err) { - console.error('Search users error:', err); - return { status: 500, data: { error: 'Server error' } }; - } -} - -/** - * Send a friend request - */ -async function sendFriendRequest(fromUserId, toUserId) { - try { - if (fromUserId === toUserId) { - return { status: 400, data: { error: 'Cannot add yourself as friend' } }; - } - - // Check if user exists - const userCheck = await query('SELECT id FROM users WHERE id = $1', [toUserId]); - if (userCheck.rows.length === 0) { - return { status: 404, data: { error: 'User not found' } }; - } - - // Ensure id_user1 < id_user2 for the constraint - const id1 = Math.min(fromUserId, toUserId); - const id2 = Math.max(fromUserId, toUserId); - - // Check existing friendship - const existing = await query( - 'SELECT status FROM friendship WHERE id_user1 = $1 AND id_user2 = $2', - [id1, id2] - ); - - if (existing.rows.length > 0) { - const status = existing.rows[0].status; - if (status === 'accepted') { - return { status: 400, data: { error: 'Already friends' } }; - } - if (status.startsWith('pending_')) { - return { status: 400, data: { error: 'Friend request already exists' } }; - } - } - - // Status indicates who needs to accept: pending_ - const status = `pending_${toUserId}`; - - await query( - `INSERT INTO friendship (id_user1, id_user2, status) - VALUES ($1, $2, $3) - ON CONFLICT (id_user1, id_user2) - DO UPDATE SET status = $3`, - [id1, id2, status] - ); - - return { status: 200, data: { message: 'Friend request sent' } }; - } catch (err) { - console.error('Send friend request error:', err); - return { status: 500, data: { error: 'Server error' } }; - } -} - -/** - * Accept a friend request - */ -async function acceptFriendRequest(userId, fromUserId) { - try { - const id1 = Math.min(userId, fromUserId); - const id2 = Math.max(userId, fromUserId); - - const result = await query( - `UPDATE friendship - SET status = 'accepted' - WHERE id_user1 = $1 AND id_user2 = $2 - AND status = $3 - RETURNING *`, - [id1, id2, `pending_${userId}`] - ); - - if (result.rows.length === 0) { - return { status: 404, data: { error: 'Friend request not found' } }; - } - - return { status: 200, data: { message: 'Friend request accepted' } }; - } catch (err) { - console.error('Accept friend request error:', err); - return { status: 500, data: { error: 'Server error' } }; - } -} - -/** - * Decline a friend request - */ -async function declineFriendRequest(userId, fromUserId) { - try { - const id1 = Math.min(userId, fromUserId); - const id2 = Math.max(userId, fromUserId); - - const result = await query( - `DELETE FROM friendship - WHERE id_user1 = $1 AND id_user2 = $2 - AND status = $3 - RETURNING *`, - [id1, id2, `pending_${userId}`] - ); - - if (result.rows.length === 0) { - return { status: 404, data: { error: 'Friend request not found' } }; - } - - return { status: 200, data: { message: 'Friend request declined' } }; - } catch (err) { - console.error('Decline friend request error:', err); - return { status: 500, data: { error: 'Server error' } }; - } -} - -/** - * Get list of friend IDs for a user (for quick lookup) - */ -async function getFriendIds(userId) { - try { - const result = await query( - `SELECT - CASE - WHEN f.id_user1 = $1 THEN f.id_user2 - ELSE f.id_user1 - END as friend_id - FROM friendship f - WHERE (f.id_user1 = $1 OR f.id_user2 = $1) - AND f.status = 'accepted'`, - [userId] - ); - return result.rows.map(row => row.friend_id); - } catch (err) { - console.error('Get friend IDs error:', err); - return []; - } -} - -/** - * Remove a friend - */ -async function removeFriend(userId, friendId) { - try { - const id1 = Math.min(userId, friendId); - const id2 = Math.max(userId, friendId); - - const result = await query( - `DELETE FROM friendship - WHERE id_user1 = $1 AND id_user2 = $2 - AND status = 'accepted' - RETURNING *`, - [id1, id2] - ); - - if (result.rows.length === 0) { - return { status: 404, data: { error: 'Friendship not found' } }; - } - - return { status: 200, data: { message: 'Friend removed' } }; - } catch (err) { - console.error('Remove friend error:', err); - return { status: 500, data: { error: 'Server error' } }; - } -} - -export default { - getFriends, - getFriendIds, - getPendingRequests, - searchUsers, - sendFriendRequest, - acceptFriendRequest, - declineFriendRequest, - removeFriend -}; diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/services/game_room.js b/Transcendence/Transcendance-Test/Transcendence/srcs/backend/services/game_room.js deleted file mode 100755 index e4afaa3..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/services/game_room.js +++ /dev/null @@ -1,254 +0,0 @@ -import {query} from '../db.js'; - -// Create the room with name as the only parameter -// max_players, status and the other variables have their default values defined in db.js -async function createRoom(name, userId) -{ - const result = await query - ( - `INSERT INTO game_rooms (name) VALUES ($1) RETURNING *`, - [name] - ); - const room = result.rows[0]; - - await query - ( - 'INSERT INTO game_players (room_id, user_id) VALUES ($1, $2)', - [room.id, userId] - ); - return (room); -} - -async function getRoomById(roomId) -{ - const result = await query - ( - 'SELECT * FROM game_rooms WHERE id = $1', - [roomId] - ); - return (result.rows[0]); -} - -// List all the waiting rooms and the player amount in each of them -async function listActiveRooms() -{ - const result = await query - ( - `SELECT r.*, COUNT(p.id) as player_count - FROM game_rooms r - LEFT JOIN game_players p ON r.id = p.room_id - WHERE r.status = 'waiting' - GROUP BY r.id - ORDER BY player_count DESC, r.created_at DESC` - ); - return (result.rows); -} - -async function listPlayingRooms() -{ - const result = await query - ( - `SELECT r.*, COUNT(p.id) as player_count - FROM game_rooms r - LEFT JOIN game_players p ON r.id = p.room_id - WHERE r.status = 'playing' - GROUP BY r.id - ORDER BY player_count DESC, r.created_at DESC` - ); - return (result.rows); -} - -async function spectateRoom(roomId, userId) -{ - const room = await getRoomById(roomId); - if (!room) - throw new Error('Room not found'); - - if (room.status !== 'playing') - throw new Error('Room is not in playing status'); - - // Check if user is already a player in any active game - const playerInGame = await query - ( - `SELECT r.id, r.name, r.status - FROM game_rooms r - JOIN game_players gp ON r.id = gp.room_id - WHERE gp.user_id = $1 AND r.status IN ('waiting', 'playing') - LIMIT 1`, - [userId] - ); - - if (playerInGame.rows.length > 0) - { - const gameRoom = playerInGame.rows[0]; - if (gameRoom.id === parseInt(roomId)) - throw new Error('You cannot spectate a game you are playing in'); - else - throw new Error('You are already in an active game'); - } - - return (room); -} - -async function leaveSpectateRoom(roomId, userId) -{ - const playerCount = await query - ( - 'SELECT COUNT(*) FROM game_players WHERE room_id = $1', - [roomId] - ); - - if (parseInt(playerCount.rows[0].count) === 0) - { - await query - ( - 'DELETE FROM game_rooms WHERE id = $1', - [roomId] - ); - } -} - -async function joinRoom(roomId, userId) -{ - const room = await getRoomById(roomId); - if (!room) - throw new Error('Room not found'); - - const playerCount = await query - ( - 'SELECT COUNT(*) FROM game_players WHERE room_id = $1', - [roomId] - ); - - if (parseInt(playerCount.rows[0].count) >= 8) - throw new Error('Room is full'); - if (room.status !== 'waiting') - throw new Error('Game already started or ended'); - const result = await query - ( - 'INSERT INTO game_players (room_id, user_id) VALUES ($1, $2) RETURNING *', - [roomId, userId] - ); - return (result.rows[0]); -} - -async function leaveRoom(roomId, userId) -{ - await query - ( - 'DELETE FROM game_players WHERE room_id = $1 AND user_id = $2', - [roomId, userId] - ); - - const playerCount = await query - ( - 'SELECT COUNT(*) FROM game_players WHERE room_id = $1', - [roomId] - ); - - if (parseInt(playerCount.rows[0].count) === 0) - { - await query - ( - 'DELETE FROM game_rooms WHERE id = $1', - [roomId] - ); - } -} - -// List the players in the room and their scores -// Useful for the scoreboard and also tell which player is currently drawing -async function getRoomPlayers(roomId) -{ - const result = await query - ( - `SELECT gp.*, u.username, u.avatar_url, u.total_points, u.games_played, u.games_won - FROM game_players gp - JOIN users u ON gp.user_id = u.id - WHERE gp.room_id = $1 - ORDER BY gp.score DESC`, - [roomId] - ); - return (result.rows); -} - -// Get the current room of a user (if any) -async function getCurrentRoom(userId) -{ - const result = await query - ( - `SELECT r.* - FROM game_rooms r - JOIN game_players gp ON r.id = gp.room_id - WHERE gp.user_id = $1 AND r.status IN ('waiting', 'playing') - LIMIT 1`, - [userId] - ); - return (result.rows[0] || null); -} - -// Update room status (waiting, playing, ended) -async function updateRoomStatus(roomId, status) -{ - const validStatuses = ['waiting', 'playing', 'ended']; - if (!validStatuses.includes(status)) - throw new Error('Invalid status'); - - let updateQuery = 'UPDATE game_rooms SET status = $1'; - const params = [status, roomId]; - - if (status === 'playing') - { - updateQuery += ', started_at = NOW()'; - } - else if (status === 'ended') - { - updateQuery += ', ended_at = NOW()'; - } - - updateQuery += ' WHERE id = $2 RETURNING *'; - - const result = await query(updateQuery, params); - return (result.rows[0]); -} - -async function resetRoomScores(roomId) -{ - await query - ( - 'UPDATE game_players SET score = 0 WHERE room_id = $1', - [roomId] - ); -} - -async function cleanupEndedRooms() -{ - await query - ( - 'DELETE FROM game_players WHERE room_id IN (SELECT id FROM game_rooms WHERE status = $1)', - ['ended'] - ); - - await query - ( - 'DELETE FROM game_rooms WHERE status = $1', - ['ended'] - ); -} - -export default -{ - createRoom, - getRoomById, - listActiveRooms, - listPlayingRooms, - spectateRoom, - leaveSpectateRoom, - joinRoom, - leaveRoom, - getRoomPlayers, - getCurrentRoom, - updateRoomStatus, - resetRoomScores, - cleanupEndedRooms -}; \ No newline at end of file diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/services/github.js b/Transcendence/Transcendance-Test/Transcendence/srcs/backend/services/github.js deleted file mode 100755 index e69de29..0000000 diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/services/global_chat.js b/Transcendence/Transcendance-Test/Transcendence/srcs/backend/services/global_chat.js deleted file mode 100755 index 16e8065..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/services/global_chat.js +++ /dev/null @@ -1,27 +0,0 @@ -import {query} from '../db.js'; - -async function saveMessage(userId, content) -{ - const result = await query - ( - 'INSERT INTO messages (sender_id, content) VALUES ($1 ,$2) RETURNING *', - [userId, content] - ); - return (result.rows[0]); -} - -async function getRecentMessages(limit = 50) -{ - const result = await query - ( - `SELECT m.sender_id, m.content, m.created_at, u.username - FROM messages m - JOIN users u ON m.sender_id = u.id - ORDER BY m.created_at DESC - LIMIT $1`, - [limit] - ); - return (result.rows.reverse()); -} - -export default {saveMessage, getRecentMessages}; \ No newline at end of file diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/services/player_stats.js b/Transcendence/Transcendance-Test/Transcendence/srcs/backend/services/player_stats.js deleted file mode 100755 index 41d820a..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/services/player_stats.js +++ /dev/null @@ -1,88 +0,0 @@ -import { query } from '../db.js'; - -// Get player stats by user ID -async function getStatsByUserId(userId) { - const result = await query( - `SELECT id, username, avatar_url, total_points, games_played, games_won, created_at - FROM users WHERE id = $1`, - [userId] - ); - return result.rows[0] || null; -} - -// Get player stats by username -async function getStatsByUsername(username) { - const result = await query( - `SELECT id, username, avatar_url, total_points, games_played, games_won, created_at - FROM users WHERE username = $1`, - [username] - ); - return result.rows[0] || null; -} - -// Update player points (add points to total) -async function addPoints(userId, points) { - const result = await query( - `UPDATE users SET total_points = COALESCE(total_points, 0) + $1 WHERE id = $2 RETURNING total_points`, - [points, userId] - ); - return result.rows[0]?.total_points || 0; -} - -// Update player points by username -async function addPointsByUsername(username, points) { - const result = await query( - `UPDATE users SET total_points = COALESCE(total_points, 0) + $1 WHERE username = $2 RETURNING total_points`, - [points, username] - ); - return result.rows[0]?.total_points || 0; -} - -// Increment games played -async function incrementGamesPlayed(userId) { - await query( - `UPDATE users SET games_played = COALESCE(games_played, 0) + 1 WHERE id = $1`, - [userId] - ); -} - -// Increment games won -async function incrementGamesWon(userId) { - await query( - `UPDATE users SET games_won = COALESCE(games_won, 0) + 1 WHERE id = $1`, - [userId] - ); -} - -// Get leaderboard (top players by points) -async function getLeaderboard(limit = 10) { - const result = await query( - `SELECT id, username, avatar_url, total_points, games_played, games_won - FROM users - WHERE total_points > 0 - ORDER BY total_points DESC - LIMIT $1`, - [limit] - ); - return result.rows; -} - -// Get user ID by username -async function getUserIdByUsername(username) { - const result = await query( - `SELECT id FROM users WHERE username = $1`, - [username] - ); - return result.rows[0]?.id || null; -} - -export default { - getStatsByUserId, - getStatsByUsername, - addPoints, - addPointsByUsername, - incrementGamesPlayed, - incrementGamesWon, - getLeaderboard, - getUserIdByUsername -}; diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/services/socket.js b/Transcendence/Transcendance-Test/Transcendence/srcs/backend/services/socket.js deleted file mode 100755 index 5888950..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/backend/services/socket.js +++ /dev/null @@ -1,795 +0,0 @@ -import jwt from 'jsonwebtoken'; -import chatService from './global_chat.js'; -import friendsService from './friends.js'; -import gameRoomService from './game_room.js'; -import playerStatsService from './player_stats.js'; - -// Store game state per room -const gameRooms = new Map(); - -// Store tetris duel rooms { roomCode → Map } -const tetrisRooms = new Map(); - -// Store io instance globally for use in routes -let ioInstance = null; - -export function getIO() { - return ioInstance; -} - -// Broadcast rooms list to all connected clients -async function broadcastRoomsList(io) { - try { - const rooms = await gameRoomService.listActiveRooms(); - io.emit('game-rooms-updated', { rooms }); - } catch (err) { - console.error('Error broadcasting rooms list:', err); - } -} - -// Check if a playing game has only 1 player left and auto-stop it -async function checkAndStopSinglePlayerGame(io, roomId, dbRoomId) { - if (!dbRoomId) return; - - try { - // Check if room is in 'playing' status - const room = await gameRoomService.getRoomById(dbRoomId); - if (!room || room.status !== 'playing') return; - - // Count remaining players - const players = await gameRoomService.getRoomPlayers(dbRoomId); - if (players.length <= 1) { - console.log(`Room ${dbRoomId} has only ${players.length} player(s) left, ending game`); - - // Update room status to 'ended' - await gameRoomService.updateRoomStatus(dbRoomId, 'waiting'); - await gameRoomService.resetRoomScores(dbRoomId); - - // Remove from game state - gameRooms.delete(roomId); - - // Notify remaining player(s) - io.to(roomId).emit('game-ended'); - io.to(roomId).emit('game-message', { - message: 'La partie s\'est terminée car il ne reste qu\'un seul joueur', - type: 'info' - }); - - // Broadcast updated rooms list - broadcastRoomsList(io); - } - } catch (err) { - console.error('Error checking single player game:', err); - } -} - -// Save round points to database (only the difference from round start) -async function saveRoundPoints(currentScores, roundStartScores) { - for (const [username, currentPoints] of Object.entries(currentScores)) { - const startPoints = roundStartScores[username] || 0; - const pointsEarned = currentPoints - startPoints; - if (pointsEarned !== 0) { - try { - await playerStatsService.addPointsByUsername(username, pointsEarned); - console.log(`Saved ${pointsEarned} points for ${username}`); - } catch (err) { - console.error(`Error saving points for ${username}:`, err); - } - } - } -} - -function setupSocketIO(io) -{ - ioInstance = io; - io.use((socket, next) => - { - const token = socket.handshake.auth.token; - if (!token) - return (next(new Error('Authentication error'))); - - try - { - const decoded = jwt.verify(token, process.env.JWT_SECRET); - socket.user = decoded; - next(); - } - catch(err) - { - next(new Error('Authentication error')); - } - }); - - io.on('connection', async (socket) => - { - console.log(`User connected: ${socket.user.username}`); - - socket.join('general-chat'); - - // Send recent messages and friend IDs on connection - try { - const [recentMessages, friendIds] = await Promise.all([ - chatService.getRecentMessages(50), - friendsService.getFriendIds(socket.user.userId) - ]); - - socket.emit('chat-init', { - messages: recentMessages, - friendIds: friendIds - }); - } catch (err) { - console.error('Error fetching initial data:', err); - } - - socket.on('chat-message', async(data) => - { - try - { - const message = await chatService.saveMessage(socket.user.userId, data.content); - socket.broadcast.to('general-chat').emit('chat-message', - { - id: message.id, - sender_id: socket.user.userId, - username: socket.user.username, - content: message.content, - created_at: message.created_at - }); - } - catch (err) - { - console.error('Error saving message:', err); - socket.emit('error', {message: 'Failed to send message'}); - } - }); - - // ============================================ - // GAME ROOM EVENTS - // ============================================ - - // Join a game room - socket.on('game-join-room', async (data) => { - console.log('Received game-join-room from', socket.user.username, 'data:', data); - const roomId = `game-room-${data.roomId}`; - socket.join(roomId); - socket.gameRoomId = roomId; - socket.gameRoomDbId = data.roomId; - console.log(`${socket.user.username} joined ${roomId}, socket.gameRoomId set to:`, socket.gameRoomId); - - // Send confirmation to the socket that joined - socket.emit('game-room-joined', { - roomId: data.roomId, - success: true - }); - - // Get updated player list from DB - try { - const players = await gameRoomService.getRoomPlayers(data.roomId); - // Notify ALL players in the room (including the one who joined) with updated player list - io.to(roomId).emit('game-players-updated', { players }); - } catch (err) { - console.error('Error getting room players:', err); - } - - // Notify others in the room that someone joined - socket.to(roomId).emit('game-player-joined', { - username: socket.user.username, - userId: socket.user.userId - }); - - // Broadcast rooms list update to everyone - broadcastRoomsList(io); - - // Send current game state if game is in progress - const gameState = gameRooms.get(roomId); - if (gameState && gameState.isPlaying) { - socket.emit('game-state-sync', { - isPlaying: gameState.isPlaying, - drawer: gameState.drawer, - wordLength: gameState.currentWord ? gameState.currentWord.length : 0, - revealedLetters: gameState.revealedLetters, - revealedWord: gameState.revealedWord || [], - guessedLetters: gameState.guessedLetters, - players: gameState.players - }); - } - }); - - // Leave a game room - socket.on('game-leave-room', async () => { - if (socket.gameRoomId) { - const roomId = socket.gameRoomId; - const dbRoomId = socket.gameRoomDbId; - - socket.to(roomId).emit('game-player-left', { - username: socket.user.username, - userId: socket.user.userId - }); - socket.leave(roomId); - console.log(`${socket.user.username} left ${roomId}`); - - // Get updated player list and broadcast to remaining players - if (dbRoomId) { - try { - const players = await gameRoomService.getRoomPlayers(dbRoomId); - io.to(roomId).emit('game-players-updated', { players }); - } catch (err) { - // Room may have been deleted - console.log('Room may have been deleted:', err.message); - } - } - - socket.gameRoomId = null; - socket.gameRoomDbId = null; - - // Check if game should auto-stop due to single player - await checkAndStopSinglePlayerGame(io, roomId, dbRoomId); - // Broadcast updated rooms list - broadcastRoomsList(io); - } - }); - - // Join a game room as spectator - socket.on('game-spectate-room', async (data) => { - console.log('Received game-spectate-room from', socket.user.username, 'data:', data); - const roomId = `game-room-${data.roomId}`; - - // Verify room exists and is in playing status, and user is not already in a game - try { - const room = await gameRoomService.spectateRoom(data.roomId, socket.user.userId); - - socket.join(roomId); - socket.gameRoomId = roomId; - socket.gameRoomDbId = data.roomId; - socket.isSpectator = true; - console.log(`${socket.user.username} joined ${roomId} as spectator`); - - // Send confirmation - socket.emit('game-spectate-joined', { - roomId: data.roomId, - success: true - }); - - // Notify others that a spectator joined - socket.to(roomId).emit('game-spectator-joined', { - username: socket.user.username - }); - - // Send current game state - const gameState = gameRooms.get(roomId); - if (gameState && gameState.isPlaying) { - socket.emit('game-state-sync', { - isPlaying: gameState.isPlaying, - drawer: gameState.drawer, - wordLength: gameState.currentWord ? gameState.currentWord.length : 0, - revealedLetters: gameState.revealedLetters, - revealedWord: gameState.revealedWord || [], - guessedLetters: gameState.guessedLetters, - players: gameState.players, - scores: gameState.scores || {} - }); - } - } catch (err) { - console.error('Error joining as spectator:', err); - socket.emit('game-spectate-error', { - error: err.message || 'Cannot spectate this room' - }); - } - }); - - // Leave spectator mode - socket.on('game-leave-spectate', () => { - if (socket.gameRoomId && socket.isSpectator) { - const roomId = socket.gameRoomId; - - socket.to(roomId).emit('game-spectator-left', { - username: socket.user.username - }); - - socket.leave(roomId); - console.log(`${socket.user.username} left spectator mode in ${roomId}`); - - socket.gameRoomId = null; - socket.gameRoomDbId = null; - socket.isSpectator = false; - } - }); - - - // Start the game - socket.on('game-start', async (data) => { - console.log('Received game-start event from', socket.user.username); - console.log('socket.gameRoomId:', socket.gameRoomId); - - // Security check: need at least 2 players - if (!data.players || data.players.length < 2) { - console.log('Game start rejected: not enough players'); - socket.emit('game-start-error', { - error: 'Il faut au moins 2 joueurs pour commencer' - }); - return; - } - - const gameStartedData = { - drawer: data.drawer, - players: data.players - }; - - const roomId = socket.gameRoomId; - - // If no roomId, still start the game for this socket only - if (!roomId) { - console.log('WARNING: No roomId for socket, starting game for this socket only'); - socket.emit('game-started', gameStartedData); - return; - } - - // Verify player count from database - const dbRoomId = socket.gameRoomDbId; - if (dbRoomId) { - try { - const players = await gameRoomService.getRoomPlayers(dbRoomId); - if (players.length < 2) { - console.log(`Game start rejected: only ${players.length} player(s) in room`); - socket.emit('game-start-error', { - error: 'Il faut au moins 2 joueurs pour commencer' - }); - return; - } - } catch (err) { - console.error('Error checking player count:', err); - } - } - - // Update room status to 'playing' in database - if (dbRoomId) { - try { - await gameRoomService.updateRoomStatus(dbRoomId, 'playing'); - console.log(`Room ${dbRoomId} status updated to 'playing'`); - } catch (err) { - console.error('Error updating room status to playing:', err); - } - } - - // Initialize scores for all players - const scores = {}; - data.players.forEach(p => scores[p] = 0); - - const gameState = { - isPlaying: true, - currentWord: '', - revealedLetters: [], - drawer: data.drawer, - players: data.players, - currentPlayerIndex: 0, - guessedLetters: [], - scores: scores, - roundStartScores: { ...scores } - }; - gameRooms.set(roomId, gameState); - - // Emit to OTHER players in the room - socket.to(roomId).emit('game-started', gameStartedData); - - // Emit directly to this socket (the one who started the game) - socket.emit('game-started', gameStartedData); - - console.log(`Game started in ${roomId} by ${socket.user.username}`); - - // Broadcast updated rooms list (this room should no longer appear) - broadcastRoomsList(io); - }); - - // Drawer sets the word - socket.on('game-set-word', (data) => { - const roomId = socket.gameRoomId; - if (!roomId) return; - - const gameState = gameRooms.get(roomId); - if (!gameState) return; - - gameState.currentWord = data.word.toLowerCase(); - gameState.revealedLetters = new Array(data.word.length).fill(false); - gameState.revealedWord = new Array(data.word.length).fill('_'); - gameState.guessedLetters = []; - gameState.wrongGuesses = 0; - - // Initialize scores if not already done - if (!gameState.scores) { - gameState.scores = {}; - gameState.players.forEach(p => gameState.scores[p] = 0); - } - - // Notify all players (without revealing the word) - io.to(roomId).emit('game-word-set', { - wordLength: data.word.length, - drawer: socket.user.username, - revealedWord: gameState.revealedWord, - scores: gameState.scores - }); - }); - - // Drawing data (real-time) - socket.on('game-draw', (data) => { - const roomId = socket.gameRoomId; - if (!roomId) return; - - // Spectators cannot draw - if (socket.isSpectator) { - console.log(`Spectator ${socket.user.username} tried to draw - blocked`); - return; - } - - // Broadcast drawing to all other players in the room - socket.to(roomId).emit('game-draw', { - x1: data.x1, - y1: data.y1, - x2: data.x2, - y2: data.y2, - color: data.color, - lineWidth: data.lineWidth - }); - }); - - // Clear canvas - socket.on('game-clear-canvas', () => { - const roomId = socket.gameRoomId; - if (!roomId) return; - - // Spectators cannot clear canvas - if (socket.isSpectator) return; - - socket.to(roomId).emit('game-clear-canvas'); - }); - - // Player makes a guess - socket.on('game-guess', (data) => { - const roomId = socket.gameRoomId; - if (!roomId) return; - - // Spectators cannot make guesses - if (socket.isSpectator) { - console.log(`Spectator ${socket.user.username} tried to guess - blocked`); - return; - } - - const gameState = gameRooms.get(roomId); - if (!gameState || !gameState.currentWord) return; - - const guess = data.guess.toLowerCase(); - const isLetter = guess.length === 1; - let success = false; - let points = 0; - const username = socket.user.username; - - // Initialize scores if needed - if (!gameState.scores) { - gameState.scores = {}; - gameState.players.forEach(p => gameState.scores[p] = 0); - } - if (!gameState.scores[username]) { - gameState.scores[username] = 0; - } - - if (isLetter) { - // Check if letter was already guessed - if (gameState.guessedLetters.includes(guess)) { - socket.emit('game-guess-result', { - guess, - success: false, - type: 'letter', - message: 'Lettre deja proposee', - username: username, - scores: gameState.scores - }); - return; - } - - gameState.guessedLetters.push(guess); - - // Check each position and reveal the actual letter - let lettersFound = 0; - for (let i = 0; i < gameState.currentWord.length; i++) { - if (gameState.currentWord[i] === guess) { - gameState.revealedLetters[i] = true; - gameState.revealedWord[i] = guess; - success = true; - lettersFound++; - } - } - - // Points: 10 per letter found, -5 for wrong guess - if (success) { - points = lettersFound * 10; - gameState.scores[username] += points; - } else { - points = -5; - gameState.scores[username] += points; - gameState.wrongGuesses++; - } - } else { - // Full word guess - success = guess === gameState.currentWord; - if (success) { - gameState.revealedLetters = gameState.revealedLetters.map(() => true); - gameState.revealedWord = gameState.currentWord.split(''); - // Bonus points for guessing the whole word - const remainingLetters = gameState.revealedLetters.filter(r => !r).length; - points = 50 + (remainingLetters * 5); - gameState.scores[username] += points; - } else { - points = -10; - gameState.scores[username] += points; - gameState.wrongGuesses++; - } - } - - // Broadcast result to all players with the revealed word (actual letters) - io.to(roomId).emit('game-guess-result', { - guess, - success, - type: isLetter ? 'letter' : 'word', - username: username, - revealedLetters: gameState.revealedLetters, - revealedWord: gameState.revealedWord, - points: points, - scores: gameState.scores - }); - - // Check if word is complete - if (gameState.revealedLetters.every(r => r)) { - // Bonus points for the drawer - const drawerBonus = Math.max(0, 30 - (gameState.wrongGuesses * 5)); - if (gameState.scores[gameState.drawer]) { - gameState.scores[gameState.drawer] += drawerBonus; - } - - // Save points to database for all players - saveRoundPoints(gameState.scores, gameState.roundStartScores || {}); - // Update round start scores for next round - gameState.roundStartScores = { ...gameState.scores }; - - io.to(roomId).emit('game-word-found', { - word: gameState.currentWord, - winner: username, - scores: gameState.scores, - drawerBonus: drawerBonus - }); - } - }); - - // Next round - socket.on('game-next-round', (data) => { - const roomId = socket.gameRoomId; - if (!roomId) return; - - const gameState = gameRooms.get(roomId); - if (!gameState) return; - - gameState.currentWord = ''; - gameState.revealedLetters = []; - gameState.guessedLetters = []; - gameState.drawer = data.drawer; - - io.to(roomId).emit('game-new-round', { - drawer: data.drawer - }); - }); - - socket.on('leave-room-during-game', async () => { - const roomId = socket.gameRoomId; - const dbRoomId = socket.gameRoomDbId; - const userId = socket.user.userId; - const username = socket.user.username; - - if (!roomId || !dbRoomId || !userId) return; - - console.log(`Player ${username} leaving room ${roomId} during game`); - - try - { - socket.leave(roomId); - - await gameRoomService.leaveRoom(dbRoomId, userId); - - io.to(roomId).emit('game-player-left', { - username: username, - message: `${username} a quitté la partie` - }); - - const gameState = gameRooms.get(roomId); - if (gameState) - { - gameState.players = gameState.players.filter(p => p !== username); - delete gameState.scores[username]; - - io.to(roomId).emit('scores-updated', gameState.scores); - } - - await checkAndStopSinglePlayerGame(io, roomId, dbRoomId); - - socket.gameRoomId = null; - socket.gameRoomDbId = null; - - broadcastRoomsList(io); - } - catch (err) - { - console.error('Error leaving room during game:', err); - } - }); - - // End game - socket.on('game-end', async () => { - const roomId = socket.gameRoomId; - if (!roomId) return; - - // Update room status to 'waiting' in database - const dbRoomId = socket.gameRoomDbId; - if (dbRoomId) { - try { - await gameRoomService.updateRoomStatus(dbRoomId, 'waiting'); - await gameRoomService.resetRoomScores(dbRoomId); - console.log(`Room ${dbRoomId} status updated to 'waiting'`); - } catch (err) { - console.error('Error updating room status to waiting:', err); - } - } - - gameRooms.delete(roomId); - io.to(roomId).emit('game-ended'); - - // Broadcast updated rooms list - broadcastRoomsList(io); - }); - - // ============================================ - // TETRIS DUEL EVENTS - // ============================================ - - socket.on('tetris:join', ({ roomCode }) => { - const code = String(roomCode).toUpperCase().slice(0, 8); - - // Quitter l'ancienne room tetris si besoin - if (socket.tetrisRoomCode) { - _tetrisLeave(socket); - } - - if (!tetrisRooms.has(code)) { - tetrisRooms.set(code, new Map()); - } - const room = tetrisRooms.get(code); - - if (room.size >= 2) { - socket.emit('tetris:room-status', { status: 'full', players: [] }); - return; - } - - room.set(socket.id, socket); - socket.tetrisRoomCode = code; - - const players = [...room.values()].map(s => s.user.username); - - if (room.size === 1) { - socket.emit('tetris:room-status', { status: 'waiting', players }); - } else { - // Notifier les deux joueurs - for (const s of room.values()) { - s.emit('tetris:room-status', { status: 'ready', players }); - } - // Notifier l'adversaire qu'un nouveau joueur a rejoint - for (const [id, s] of room) { - if (id !== socket.id) { - s.emit('tetris:opponent-joined', { username: socket.user.username }); - } - } - } - }); - - socket.on('tetris:leave', () => { - _tetrisLeave(socket); - }); - - // Relay pur : grid-update → adversaire uniquement - socket.on('tetris:grid-update', (data) => { - _tetrisRelayToOpponent(socket, 'tetris:grid-update', data); - }); - - // Relay pur : lines-cleared → adversaire uniquement - socket.on('tetris:lines-cleared', (data) => { - _tetrisRelayToOpponent(socket, 'tetris:lines-cleared', data); - }); - - // start-duel → relayé aux DEUX joueurs de la room (inclut l'émetteur) - socket.on('tetris:start-duel', () => { - const code = socket.tetrisRoomCode; - if (!code) return; - const room = tetrisRooms.get(code); - if (!room || room.size < 2) return; - for (const s of room.values()) { - s.emit('tetris:start-duel'); - } - }); - - // game-over → relayé en opponent-game-over chez l'adversaire - socket.on('tetris:game-over', (data) => { - _tetrisRelayToOpponent(socket, 'tetris:opponent-game-over', data); - }); - - socket.on('disconnect', async () => - { - // Nettoyage room tetris - if (socket.tetrisRoomCode) { - _tetrisLeave(socket); - } - - console.log(`User disconnected: ${socket.user.username}`); - - // Notify game room if player/spectator was in one - if (socket.gameRoomId) { - const roomId = socket.gameRoomId; - const dbRoomId = socket.gameRoomDbId; - - // If spectator, just notify and leave - if (socket.isSpectator) { - socket.to(roomId).emit('game-spectator-left', { - username: socket.user.username - }); - console.log(`Spectator ${socket.user.username} disconnected from ${roomId}`); - } else { - // Regular player disconnect - socket.to(roomId).emit('game-player-left', { - username: socket.user.username, - userId: socket.user.userId - }); - - // Get updated player list and broadcast - if (dbRoomId) { - try { - const players = await gameRoomService.getRoomPlayers(dbRoomId); - io.to(roomId).emit('game-players-updated', { players }); - } catch (err) { - console.log('Room may have been deleted on disconnect:', err.message); - } - } - - // Check if game should auto-stop due to single player - await checkAndStopSinglePlayerGame(io, roomId, dbRoomId); - - // Broadcast updated rooms list - broadcastRoomsList(io); - } - } - }); - }); -} - -// ── Helpers tetris duel ────────────────────────────────────────────────── - -function _tetrisLeave(socket) { - const code = socket.tetrisRoomCode; - if (!code) return; - const room = tetrisRooms.get(code); - if (room) { - room.delete(socket.id); - // Notifier l'adversaire restant - for (const s of room.values()) { - s.emit('tetris:opponent-left'); - s.emit('tetris:room-status', { status: 'waiting', players: [s.user.username] }); - } - if (room.size === 0) tetrisRooms.delete(code); - } - socket.tetrisRoomCode = null; -} - -function _tetrisRelayToOpponent(socket, event, data) { - const code = socket.tetrisRoomCode; - if (!code) return; - const room = tetrisRooms.get(code); - if (!room) return; - for (const [id, s] of room) { - if (id !== socket.id) s.emit(event, data); - } -} - -export { broadcastRoomsList }; -export default setupSocketIO; diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/dockerfile b/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/dockerfile deleted file mode 100755 index 310e2ed..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -FROM nginx:alpine -COPY src /usr/share/nginx/html -COPY nginx.conf /etc/nginx/conf.d/default.conf -EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/nginx.conf b/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/nginx.conf deleted file mode 100755 index 273e6d8..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/nginx.conf +++ /dev/null @@ -1,38 +0,0 @@ -server { - listen 80; - - root /usr/share/nginx/html; - index index.html; - - # Frontend - location / { - try_files $uri /index.html; - } - - # Backend API - location /api/ { - proxy_pass http://backend:3001; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - } - - # Socket.IO WebSocket proxying - location /socket.io/ { - proxy_pass http://backend:3001; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - location /avatar/ { - proxy_pass http://backend:3001/avatar/; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_hide_header Content-Type; - add_header Cache-Control "public, max-age=3600"; - } -} diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/ajout du multiplayer b/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/ajout du multiplayer deleted file mode 100755 index a02e738..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/ajout du multiplayer +++ /dev/null @@ -1,45 +0,0 @@ -Je veux faire un mode duel du tetris, -il est fonctionnel est solo. - -Pour commencer, je vais devoir -creer une div qui sera le rendu -du joueur second joueur. il sera a droite de la -div principale qui lui meme sera legerement decale vers la gauche, -cette div sera grossierement identitique en style qui la div principale - -je ne vais pas voir en temps reelle -les pieces du joueur qui tombe. -A la place quand le joueur 2 a mis une piece, il envoie un signal, -le joueur 1 envoie un signal egalement quand la piece tombe. - -en mode duel, quand une ligne est clear, il envoie la ligne -moins la cellule qui a provoquer le clear (donc avec un trou pile -la ou la derniere piece est arrive) au joueur opposant. - -Ce qui a pour effet de decaler toute les lignes vers le haut -a l'opposant pour recevoir la ligne recu avec le trou. - -Pour se faire je vais devoir connecter les deux joueurs. - -Il me faudra : - -une class Duel il aura pour methode et membre: - -action_queue: c'est un tableau qui repertorie tous les -signaux a traiter, c'est un tableau qui sera partager -entre le joueur1 et le joueur2 - - -Syncronise_game: fonction qui traite les -actions de action_queue et qui verifie l'integrite du duel, -il va par exemple regarder l'etat du jeu de chaque joueur -pour voir s'il correspond bien a ce qui est attendu - -il y aura different type de signaux. -Le signal bloc pose avec le type de bloc, -sa rotation et as position - -le signal line cleared, avec le nombre de ligne -cleared et on ajoute le trou aussi - -Aucune idee de si je dois utiliser web socket ou autre diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/app.js b/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/app.js deleted file mode 100755 index 02998e6..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/app.js +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Application entry point - * Initializes windows and handles menu interactions - */ -import { windowRegistry } from './windows.js'; -import { LoginWindow } from './login.js'; -import { GlobalChat } from './global_chat.js'; -import { AvatarWindow } from './avatar.js'; -import { FriendsWindow } from './friends.js'; -import { GameRoomWindow } from './game_room.js'; - -/** - * Main application class - * Handles initialization and menu interactions - */ -class App { - constructor() { - this.initWindows(); - this.initMenu(); - this.initPage(); - this.initEasterEgg(); - } - - /** - * Initializes all windows - */ - initWindows() { - new LoginWindow(); - new GlobalChat(); - new AvatarWindow(); - new FriendsWindow(); - new GameRoomWindow(); - } - - /** - * Initializes the main menu - * Uses event delegation instead of IDs - */ - initMenu() { - const menu = document.querySelector('.menu'); - if (!menu) { - console.warn('Menu not found'); - return; - } - - const actionMap = { - 'login': 'login', - 'chat': 'chat', - 'avatar': 'avatar', - 'friends': 'friends' - }; - - // Event delegation on the menu - menu.addEventListener('click', (e) => { - const button = e.target.closest('.menu__item'); - if (!button) return; - - const action = button.dataset.action; - - // Actions with associated windows - if (actionMap[action]) { - windowRegistry.toggle(actionMap[action]); - return; - } - - }); - } - - initPage() { - const page = document.querySelector('.page'); - if (!page) { - return; - } - - const actionMap = { - 'gameroom': 'gameroom' - }; - - // Event delegation on the menu - page.addEventListener('click', (e) => { - const button = e.target.closest('.page__item'); - if (!button) return; - - const action = button.dataset.action; - - // Actions with associated windows - if (actionMap[action]) { - windowRegistry.toggle(actionMap[action]); - return; - } - - }); - } - - /** - * Initializes the easter egg button - */ - initEasterEgg() { - const easterEgg = document.querySelector('.easter-egg'); - if (easterEgg) { - easterEgg.addEventListener('click', () => { - alert('DONT CLICK!'); - }); - } - } -} - -// Start the application when DOM is ready -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => new App()); -} else { - new App(); -} diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/assets/background.png b/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/assets/background.png deleted file mode 100755 index 8afd0a3..0000000 Binary files a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/assets/background.png and /dev/null differ diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/assets/grwweg.png b/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/assets/grwweg.png deleted file mode 100755 index 716affb..0000000 Binary files a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/assets/grwweg.png and /dev/null differ diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/avatar.js b/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/avatar.js deleted file mode 100755 index 77dccf3..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/avatar.js +++ /dev/null @@ -1,271 +0,0 @@ -import { Window } from './windows.js'; -import { API, STORAGE_KEYS, CSS } from './config.js'; -import { eventBus, Events } from './events.js'; - -/** - * Avatar management window - * Allows viewing and modifying the user's avatar - */ -export class AvatarWindow extends Window { - constructor() { - super({ - name: 'avatar', - title: 'Avatar', - cssClasses: ['avatar-window'] - }); - - this.buildUI(); - this.bindEvents(); - if (localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN)) { - this.loadAvatar(); - } - - // Listen for login events - eventBus.on(Events.USER_LOGGED_IN, () => this.loadAvatar()); - } - - /** - * Builds the user interface - */ - buildUI() { - // Avatar preview - this.preview = this.createElement('img', CSS.AVATAR_PREVIEW, { - alt: 'Avatar' - }); - - // Username display - this.username = this.createElement('div', CSS.AVATAR_USERNAME); - - // Stats display - this.statsContainer = this.createElement('div', 'avatar__stats'); - this.pointsDisplay = this.createElement('div', 'avatar__stat'); - this.gamesPlayedDisplay = this.createElement('div', 'avatar__stat'); - this.gamesWonDisplay = this.createElement('div', 'avatar__stat'); - this.statsContainer.append(this.pointsDisplay, this.gamesPlayedDisplay, this.gamesWonDisplay); - - // Hidden file input - this.fileInput = this.createElement('input', 'avatar__file-input', { - type: 'file', - accept: 'image/*' - }); - - // Controls - this.controls = this.createElement('div', CSS.AVATAR_CONTROLS); - - this.chooseBtn = this.createElement('button', [CSS.BTN, CSS.BTN_SECONDARY], { - text: 'Choose image' - }); - - this.saveBtn = this.createElement('button', [CSS.BTN, CSS.BTN_PRIMARY], { - text: 'Save avatar' - }); - - this.refreshBtn = this.createElement('button', [CSS.BTN, CSS.BTN_SECONDARY], { - text: 'Refresh' - }); - - this.controls.append(this.chooseBtn, this.saveBtn, this.refreshBtn); - - // Feedback message - this.message = this.createElement('div', CSS.MESSAGE); - - // Assembly - this.body.append( - this.preview, - this.username, - this.statsContainer, - this.fileInput, - this.controls, - this.message - ); - } - - /** - * Attaches event handlers - */ - bindEvents() { - this.fileInput.addEventListener('change', (e) => this.handleFileSelect(e)); - this.chooseBtn.addEventListener('click', () => this.fileInput.click()); - this.saveBtn.addEventListener('click', () => this.uploadAvatar()); - this.refreshBtn.addEventListener('click', () => this.loadAvatar()); - } - - /** - * Handles file selection - * @param {Event} e - */ - handleFileSelect(e) { - const file = e.target.files?.[0]; - if (!file) return; - - const reader = new FileReader(); - reader.onload = (ev) => { - this.preview.src = ev.target.result; - }; - reader.readAsDataURL(file); - } - - /** - * Decodes a JWT token and returns the payload - * @param {string} token - * @returns {object|null} - */ - decodeToken(token) { - try { - const payload = token.split('.')[1]; - return JSON.parse(atob(payload)); - } catch { - return null; - } - } - - /** - * Loads avatar from the server - */ - async loadAvatar() { - const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); - if (!token) { - console.log('No token, skipping avatar load'); - return; - } - - // Extract username from JWT token - const tokenData = this.decodeToken(token); - if (tokenData?.username) { - this.username.textContent = tokenData.username; - } - - try { - const response = await fetch(API.AVATAR.GET, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${token}` - } - }); - - if (!response.ok) { - console.warn('Failed to load avatar, status:', response.status); - return; - } - - const data = await response.json(); - - if (data?.avatar_url) { - this.preview.src = data.avatar_url; - } else { - console.warn('Avatar URL not found in response'); - } - } catch (error) { - console.error('Error loading avatar:', error); - } - - // Load stats - await this.loadStats(); - } - - /** - * Loads player stats from the server - */ - async loadStats() { - const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); - if (!token) return; - - try { - const response = await fetch(API.STATS.ME, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${token}` - } - }); - - if (!response.ok) { - console.warn('Failed to load stats, status:', response.status); - return; - } - - const data = await response.json(); - this.updateStatsDisplay(data); - } catch (error) { - console.error('Error loading stats:', error); - } - } - - /** - * Updates the stats display - * @param {object} stats - */ - updateStatsDisplay(stats) { - this.pointsDisplay.innerHTML = `Points: ${stats.total_points || 0}`; - this.gamesPlayedDisplay.innerHTML = `Parties: ${stats.games_played || 0}`; - this.gamesWonDisplay.innerHTML = `Victoires: ${stats.games_won || 0}`; - } - - /** - * Uploads avatar to the server - */ - async uploadAvatar() { - const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); - if (!token) { - this.showMessage('You must be logged in', 'error'); - return; - } - - const file = this.fileInput.files?.[0]; - if (!file) { - this.showMessage('Select an image first', 'error'); - return; - } - - const formData = new FormData(); - formData.append('avatar', file); - - try { - this.showMessage('Uploading...', 'info'); - - const response = await fetch(API.AVATAR.UPLOAD, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}` - }, - body: formData - }); - - const data = await response.json(); - - if (!response.ok) { - const errorMsg = data?.error || data?.message || 'Upload failed'; - this.showMessage(errorMsg, 'error'); - return; - } - - if (data?.avatar_url) { - this.preview.src = data.avatar_url; - } - - this.showMessage('Avatar saved!', 'success'); - eventBus.emit(Events.AVATAR_UPDATED, { url: data?.avatar_url }); - - } catch (error) { - console.error('Avatar upload error:', error); - this.showMessage('Upload error', 'error'); - } - } - - /** - * Displays a feedback message - * @param {string} text - Message text - * @param {'success'|'error'|'info'} type - Message type - */ - showMessage(text, type = 'info') { - this.message.textContent = text; - this.message.className = CSS.MESSAGE; - - if (type === 'success') { - this.message.classList.add(CSS.MESSAGE_SUCCESS); - } else if (type === 'error') { - this.message.classList.add(CSS.MESSAGE_ERROR); - } else { - this.message.classList.add(CSS.MESSAGE_INFO); - } - } -} diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/config.js b/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/config.js deleted file mode 100755 index aa39164..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/config.js +++ /dev/null @@ -1,145 +0,0 @@ -/** - * Centralized application configuration - */ - -// API Endpoints -export const API = { - AUTH: { - LOGIN: '/api/auth/login', - REGISTER: '/api/auth/register', - GITHUB: '/api/auth/github' - }, - AVATAR: { - GET: '/api/avatar/me', - UPLOAD: '/api/avatar/upload' - }, - FRIENDS: { - LIST: '/api/friends', - REQUESTS: '/api/friends/requests', - SEARCH: '/api/friends/search', - REQUEST: '/api/friends/request', - ACCEPT: '/api/friends/accept', - DECLINE: '/api/friends/decline' - }, - ROOMS: { - LIST: '/api/rooms', - PLAYING: '/api/rooms/playing', - CREATE: '/api/rooms', - GET: (id) => `/api/rooms/${id}`, - PLAYERS: (id) => `/api/rooms/${id}/players`, - JOIN: (id) => `/api/rooms/${id}/join`, - LEAVE: (id) => `/api/rooms/${id}/leave`, - SPECTATE: (id) => `/api/rooms/${id}/spectate`, - LEAVE_SPECTATE: (id) => `/api/rooms/${id}/leave-spectate`, - CURRENT: '/api/rooms/current' - }, - STATS: { - ME: '/api/stats/me', - USER: (username) => `/api/stats/user/${username}`, - LEADERBOARD: '/api/stats/leaderboard' - } -}; - -// localStorage keys -export const STORAGE_KEYS = { - AUTH_TOKEN: 'auth_token' -}; - -// Window configuration -export const WINDOW_CONFIG = { - DEFAULT_WIDTH: 320, - DEFAULT_HEIGHT: 220, - Z_INDEX_BASE: 100 -}; - -// CSS classes (BEM convention) -export const CSS = { - // Menu - MENU: 'menu', - MENU_ITEM: 'menu__item', - MENU_ITEM_ACTIVE: 'menu__item--active', - - // Windows - WINDOW: 'window', - WINDOW_VISIBLE: 'window--visible', - WINDOW_HEADER: 'window__header', - WINDOW_TITLE: 'window__title', - WINDOW_CLOSE: 'window__close', - WINDOW_BODY: 'window__body', - - // Buttons - BTN: 'btn', - BTN_PRIMARY: 'btn--primary', - BTN_SECONDARY: 'btn--secondary', - BTN_SUCCESS: 'btn--success', - BTN_DANGER: 'btn--danger', - BTN_GITHUB: 'btn--github', - - // Forms - INPUT: 'input', - INPUT_GROUP: 'input-group', - - // Messages - MESSAGE: 'message', - MESSAGE_SUCCESS: 'message--success', - MESSAGE_ERROR: 'message--error', - MESSAGE_INFO: 'message--info', - - // Chat - CHAT: 'chat', - CHAT_OUTPUT: 'chat__output', - CHAT_INPUT: 'chat__input', - CHAT_CONTROLS: 'chat__controls', - CHAT_MESSAGE: 'chat__message', - CHAT_SYSTEM: 'chat__system', - - // Avatar - AVATAR: 'avatar', - AVATAR_PREVIEW: 'avatar__preview', - AVATAR_CONTROLS: 'avatar__controls', - AVATAR_USERNAME: 'avatar__username', - - // Friends - FRIENDS: 'friends', - FRIENDS_TABS: 'friends__tabs', - FRIENDS_TAB: 'friends__tab', - FRIENDS_TAB_ACTIVE: 'friends__tab--active', - FRIENDS_CONTENT: 'friends__content', - FRIENDS_LIST: 'friends__list', - FRIENDS_ITEM: 'friends__item', - FRIENDS_AVATAR: 'friends__avatar', - FRIENDS_NAME: 'friends__name', - FRIENDS_ACTIONS: 'friends__actions', - FRIENDS_SEARCH: 'friends__search', - FRIENDS_EMPTY: 'friends__empty', - - // Game Rooms - GAMEROOM: 'gameroom', - GAMEROOM_TABS: 'gameroom__tabs', - GAMEROOM_TAB: 'gameroom__tab', - GAMEROOM_TAB_ACTIVE: 'gameroom__tab--active', - GAMEROOM_CONTENT: 'gameroom__content', - GAMEROOM_LIST: 'gameroom__list', - GAMEROOM_ITEM: 'gameroom__item', - GAMEROOM_NAME: 'gameroom__name', - GAMEROOM_PLAYERS: 'gameroom__players', - GAMEROOM_ACTIONS: 'gameroom__actions', - GAMEROOM_CREATE: 'gameroom__create', - GAMEROOM_LOBBY: 'gameroom__lobby', - GAMEROOM_PLAYER_LIST: 'gameroom__player-list', - GAMEROOM_PLAYER: 'gameroom__player', - GAMEROOM_PLAYER_AVATAR: 'gameroom__player-avatar', - GAMEROOM_PLAYER_NAME: 'gameroom__player-name', - GAMEROOM_PLAYER_SCORE: 'gameroom__player-score' -}; - -// Colors (for reference, mainly used in CSS) -export const COLORS = { - PRIMARY: '#0066cc', - SUCCESS: '#3cff01', - ERROR: '#ff4d4d', - GITHUB: '#24292e', - BACKGROUND: '#000', - SURFACE: '#222', - TEXT: '#fff' -}; diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/duel.js b/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/duel.js deleted file mode 100755 index 1df4822..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/duel.js +++ /dev/null @@ -1,148 +0,0 @@ -// ───────────────────────────────────────────── -// DUEL -// ───────────────────────────────────────────── - -class Duel { - constructor(socket, tetrisGame, onStatusChange, onStart) { - this.socket = socket; - this.tetrisGame = tetrisGame; - this.onStatusChange = onStatusChange; // (status, opponentName) => void - this.onStart = onStart; // () => void — déclenche le début du jeu local - - this.action_queue = []; - this.opponentGrid = this._emptyGrid(); - this.opponentScore = 0; - this.roomCode = null; - this.isReady = false; - - this._bindSocketEvents(); - } - - // ─── Connexion ──────────────────────────── - - join(roomCode) { - this.roomCode = roomCode; - this.socket.emit('tetris:join', { roomCode }); - } - - startDuel() { - if (!this.isReady) return; - this.socket.emit('tetris:start-duel'); - } - - leave() { - if (!this.roomCode) return; - this.socket.emit('tetris:leave'); - this.roomCode = null; - this.isReady = false; - this.opponentGrid = this._emptyGrid(); - this.opponentScore = 0; - } - - // ─── Hooks appelés par tetris.js ────────── - - onLocalBlockPlaced(grid, score) { - if (!this.isReady) return; - this.socket.emit('tetris:grid-update', { grid, score }); - } - - onLocalLinesCleared(count, holeCol) { - if (!this.isReady) return; - const garbageLines = []; - for (let i = 0; i < count; i++) - garbageLines.push(this._buildGarbageLine(holeCol)); - this.socket.emit('tetris:lines-cleared', { count, holeCol, garbageLines }); - } - - onLocalGameOver(score) { - if (!this.isReady) return; - this.socket.emit('tetris:game-over', { score }); - } - - // ─── Traitement de la queue ─────────────── - - synchronize_game() { - while (this.action_queue.length > 0) { - const action = this.action_queue.shift(); - this._processAction(action); - } - } - - _processAction(action) { - switch (action.type) { - case 'GRID_UPDATE': - this.opponentGrid = action.grid; - this.opponentScore = action.score; - document.getElementById('opponent-score').textContent = action.score; - renderOpponent(this.opponentGrid); - break; - - case 'LINES_CLEARED': - this.tetrisGame.addGarbageLines(action.garbageLines); - break; - - case 'OPPONENT_GAME_OVER': - this._showOpponentOverlay('YOU WIN', action.score); - break; - } - } - - // ─── Liaison socket ─────────────────────── - - _bindSocketEvents() { - this.socket.on('tetris:room-status', (data) => { - this.isReady = data.status === 'ready'; - const opponentName = data.players.find(p => p !== this.socket.username) || 'Adversaire'; - document.getElementById('opponent-name').textContent = opponentName; - this.onStatusChange(data.status, opponentName); - }); - - this.socket.on('tetris:opponent-joined', (data) => { - document.getElementById('opponent-name').textContent = data.username; - }); - - this.socket.on('tetris:opponent-left', () => { - this.isReady = false; - this.onStatusChange('waiting', null); - this._showOpponentOverlay('DÉCONNECTÉ'); - }); - - this.socket.on('tetris:grid-update', (data) => { - this.action_queue.push({ type: 'GRID_UPDATE', grid: data.grid, score: data.score }); - }); - - this.socket.on('tetris:lines-cleared', (data) => { - this.action_queue.push({ type: 'LINES_CLEARED', garbageLines: data.garbageLines }); - }); - - this.socket.on('tetris:opponent-game-over', (data) => { - this.action_queue.push({ type: 'OPPONENT_GAME_OVER', score: data.score }); - }); - - this.socket.on('tetris:start-duel', () => { - if (this.onStart) this.onStart(); - }); - } - - // ─── Utilitaires ───────────────────────── - - _buildGarbageLine(holeCol) { - return Array.from({ length: 10 }, (_, i) => i === holeCol ? 0 : 8); - } - - _emptyGrid() { - return Array.from({ length: 20 }, () => Array(10).fill(0)); - } - - _showOpponentOverlay(title, score) { - const overlayEl = document.getElementById('overlay-opponent'); - document.getElementById('overlay-opponent-title').textContent = title; - const scoreEl = document.getElementById('overlay-opponent-score'); - if (scoreEl) scoreEl.textContent = score !== undefined ? `Score : ${score}` : ''; - overlayEl.classList.add('visible'); - } - - hideOpponentOverlay() { - document.getElementById('overlay-opponent').classList.remove('visible'); - } -} diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/element.js b/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/element.js deleted file mode 100755 index e46e637..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/element.js +++ /dev/null @@ -1,114 +0,0 @@ -/** - * DOM element utilities - * This module provides helper functions for creating elements - * without depending on specific HTML IDs - */ - -/** - * Creates a DOM element with options - * @param {string} tag - HTML tag - * @param {Object} options - Configuration options - * @param {string|string[]} [options.classes] - CSS classes - * @param {string} [options.text] - Element text - * @param {string} [options.html] - Inner HTML - * @param {Object} [options.attrs] - Additional attributes - * @param {Object} [options.style] - Inline styles (avoid using) - * @param {Object} [options.events] - Events to attach - * @param {HTMLElement[]} [options.children] - Child elements - * @returns {HTMLElement} - */ -export function createElement(tag, options = {}) { - const element = document.createElement(tag); - - // Classes - if (options.classes) { - const classes = Array.isArray(options.classes) ? options.classes : [options.classes]; - element.className = classes.filter(Boolean).join(' '); - } - - // Text - if (options.text) { - element.textContent = options.text; - } - - // HTML - if (options.html) { - element.innerHTML = options.html; - } - - // Attributes - if (options.attrs) { - Object.entries(options.attrs).forEach(([key, value]) => { - element.setAttribute(key, value); - }); - } - - // Styles (use sparingly) - if (options.style) { - Object.assign(element.style, options.style); - } - - // Events - if (options.events) { - Object.entries(options.events).forEach(([event, handler]) => { - element.addEventListener(event, handler); - }); - } - - // Children - if (options.children) { - options.children.forEach(child => { - if (child) element.appendChild(child); - }); - } - - return element; -} - -/** - * Selects an element by its data-attribute - * @param {string} attr - Attribute name (without 'data-') - * @param {string} value - Value to search for - * @param {HTMLElement} [parent=document] - Parent element - * @returns {HTMLElement|null} - */ -export function findByData(attr, value, parent = document) { - return parent.querySelector(`[data-${attr}="${value}"]`); -} - -/** - * Selects all elements by their data-attribute - * @param {string} attr - Attribute name (without 'data-') - * @param {string} [value] - Value to search for (optional) - * @param {HTMLElement} [parent=document] - Parent element - * @returns {HTMLElement[]} - */ -export function findAllByData(attr, value, parent = document) { - const selector = value ? `[data-${attr}="${value}"]` : `[data-${attr}]`; - return Array.from(parent.querySelectorAll(selector)); -} - -/** - * Adds or removes a class based on a condition - * @param {HTMLElement} element - * @param {string} className - * @param {boolean} condition - */ -export function toggleClass(element, className, condition) { - if (condition) { - element.classList.add(className); - } else { - element.classList.remove(className); - } -} - -/** - * Escapes HTML to prevent XSS - * @param {string} text - * @returns {string} - */ -export function escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; -} diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/events.js b/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/events.js deleted file mode 100755 index e82be3c..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/events.js +++ /dev/null @@ -1,98 +0,0 @@ -/** - * EventBus - Centralized event system - * Enables communication between modules without circular dependencies - */ -class EventBus { - constructor() { - this.listeners = new Map(); - } - - /** - * Subscribe to an event - * @param {string} event - Event name - * @param {Function} callback - Function to call - * @returns {Function} Unsubscribe function - */ - on(event, callback) { - if (!this.listeners.has(event)) { - this.listeners.set(event, new Set()); - } - this.listeners.get(event).add(callback); - - return () => this.off(event, callback); - } - - /** - * Subscribe to an event once - * @param {string} event - Event name - * @param {Function} callback - Function to call - */ - once(event, callback) { - const wrapper = (data) => { - this.off(event, wrapper); - callback(data); - }; - this.on(event, wrapper); - } - - /** - * Unsubscribe from an event - * @param {string} event - Event name - * @param {Function} callback - Function to remove - */ - off(event, callback) { - if (this.listeners.has(event)) { - this.listeners.get(event).delete(callback); - } - } - - /** - * Emit an event - * @param {string} event - Event name - * @param {*} data - Data to transmit - */ - emit(event, data) { - console.log(`EventBus: Emitting event "${event}"`, data); - if (this.listeners.has(event)) { - const listeners = this.listeners.get(event); - console.log(`EventBus: "${event}" has ${listeners.size} listener(s)`); - this.listeners.get(event).forEach(callback => { - try { - callback(data); - } catch (error) { - console.error(`Error in listener for "${event}":`, error); - } - }); - } else { - console.warn(`EventBus: No listeners for event "${event}"`); - } - } -} - -// Exported singleton instance -export const eventBus = new EventBus(); - -// Available events (for documentation and autocompletion) -export const Events = { - // Authentication - USER_LOGGED_IN: 'user:logged-in', - USER_LOGGED_OUT: 'user:logged-out', - USER_REGISTERED: 'user:registered', - - // Windows - WINDOW_OPENED: 'window:opened', - WINDOW_CLOSED: 'window:closed', - - // Avatar - AVATAR_UPDATED: 'avatar:updated', - - // Chat - CHAT_CONNECTED: 'chat:connected', - CHAT_DISCONNECTED: 'chat:disconnected', - CHAT_MESSAGE_RECEIVED: 'chat:message-received', - - // Game Rooms - ROOM_JOINED: 'room:joined', - ROOM_LEFT: 'room:left', - ROOM_CREATED: 'room:created' -}; diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/friends.js b/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/friends.js deleted file mode 100755 index 19d042e..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/friends.js +++ /dev/null @@ -1,453 +0,0 @@ -import { Window } from './windows.js'; -import { API, STORAGE_KEYS, CSS } from './config.js'; -import { eventBus, Events } from './events.js'; - -/** - * Friends management window - * Allows viewing friends, requests, and searching users - */ -export class FriendsWindow extends Window { - constructor() { - super({ - name: 'friends', - title: 'Amis', - cssClasses: ['friends-window'] - }); - - this.currentTab = 'friends'; - this.buildUI(); - this.bindEvents(); - - eventBus.on(Events.USER_LOGGED_IN, () => this.loadCurrentTab()); - } - - /** - * Builds the user interface - */ - buildUI() { - // Tabs - this.tabs = this.createElement('div', CSS.FRIENDS_TABS); - - this.friendsTab = this.createElement('button', [CSS.FRIENDS_TAB, CSS.FRIENDS_TAB_ACTIVE], { - text: 'Amis' - }); - this.friendsTab.dataset.tab = 'friends'; - - this.requestsTab = this.createElement('button', CSS.FRIENDS_TAB, { - text: 'Demandes' - }); - this.requestsTab.dataset.tab = 'requests'; - - this.searchTab = this.createElement('button', CSS.FRIENDS_TAB, { - text: 'Rechercher' - }); - this.searchTab.dataset.tab = 'search'; - - this.tabs.append(this.friendsTab, this.requestsTab, this.searchTab); - - // Content area - this.content = this.createElement('div', CSS.FRIENDS_CONTENT); - - // Search input (hidden by default) - this.searchContainer = this.createElement('div', CSS.FRIENDS_SEARCH); - this.searchInput = this.createElement('input', CSS.INPUT, { - type: 'text', - placeholder: 'Rechercher un utilisateur...' - }); - this.searchBtn = this.createElement('button', [CSS.BTN, CSS.BTN_PRIMARY], { - text: 'Chercher' - }); - this.searchContainer.append(this.searchInput, this.searchBtn); - this.searchContainer.style.display = 'none'; - - // List container - this.list = this.createElement('div', CSS.FRIENDS_LIST); - - // Message - this.message = this.createElement('div', CSS.MESSAGE); - - this.content.append(this.searchContainer, this.list, this.message); - - // Assembly - this.body.append(this.tabs, this.content); - } - - /** - * Attaches event handlers - */ - bindEvents() { - this.tabs.addEventListener('click', (e) => { - const tab = e.target.closest(`.${CSS.FRIENDS_TAB}`); - if (tab) { - this.switchTab(tab.dataset.tab); - } - }); - - this.searchBtn.addEventListener('click', () => this.searchUsers()); - this.searchInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter') this.searchUsers(); - }); - } - - /** - * Switches between tabs - */ - switchTab(tabName) { - this.currentTab = tabName; - - // Update tab styles - [this.friendsTab, this.requestsTab, this.searchTab].forEach(tab => { - tab.classList.toggle(CSS.FRIENDS_TAB_ACTIVE, tab.dataset.tab === tabName); - }); - - // Show/hide search - this.searchContainer.style.display = tabName === 'search' ? 'flex' : 'none'; - - this.loadCurrentTab(); - } - - /** - * Loads data for current tab - */ - loadCurrentTab() { - switch (this.currentTab) { - case 'friends': - this.loadFriends(); - break; - case 'requests': - this.loadRequests(); - break; - case 'search': - this.list.innerHTML = ''; - this.showMessage('Entrez un nom pour rechercher', 'info'); - break; - } - } - - /** - * Gets auth headers - */ - getHeaders() { - const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); - return { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }; - } - - /** - * Loads friends list - */ - async loadFriends() { - const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); - if (!token) { - this.showMessage('Connectez-vous pour voir vos amis', 'info'); - return; - } - - try { - const response = await fetch(API.FRIENDS.LIST, { - headers: this.getHeaders() - }); - const data = await response.json(); - - if (!response.ok) { - this.showMessage(data.error || 'Erreur', 'error'); - return; - } - - this.renderFriendsList(data.friends || []); - } catch (error) { - console.error('Load friends error:', error); - this.showMessage('Erreur de connexion', 'error'); - } - } - - /** - * Loads pending requests - */ - async loadRequests() { - const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); - if (!token) { - this.showMessage('Connectez-vous pour voir les demandes', 'info'); - return; - } - - try { - const response = await fetch(API.FRIENDS.REQUESTS, { - headers: this.getHeaders() - }); - const data = await response.json(); - - if (!response.ok) { - this.showMessage(data.error || 'Erreur', 'error'); - return; - } - - this.renderRequestsList(data.requests || []); - } catch (error) { - console.error('Load requests error:', error); - this.showMessage('Erreur de connexion', 'error'); - } - } - - /** - * Searches users - */ - async searchUsers() { - const query = this.searchInput.value.trim(); - if (!query) { - this.showMessage('Entrez un nom pour rechercher', 'info'); - return; - } - - const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); - if (!token) { - this.showMessage('Connectez-vous pour rechercher', 'info'); - return; - } - - try { - const response = await fetch(`${API.FRIENDS.SEARCH}?q=${encodeURIComponent(query)}`, { - headers: this.getHeaders() - }); - const data = await response.json(); - - if (!response.ok) { - this.showMessage(data.error || 'Erreur', 'error'); - return; - } - - this.renderSearchResults(data.users || []); - } catch (error) { - console.error('Search error:', error); - this.showMessage('Erreur de connexion', 'error'); - } - } - - /** - * Renders friends list - */ - renderFriendsList(friends) { - this.list.innerHTML = ''; - this.message.textContent = ''; - - if (friends.length === 0) { - this.showMessage('Aucun ami pour le moment', 'info'); - return; - } - - friends.forEach(friend => { - const item = this.createFriendItem(friend, 'friend'); - this.list.appendChild(item); - }); - } - - /** - * Renders requests list - */ - renderRequestsList(requests) { - this.list.innerHTML = ''; - this.message.textContent = ''; - - if (requests.length === 0) { - this.showMessage('Aucune demande en attente', 'info'); - return; - } - - requests.forEach(request => { - const item = this.createFriendItem(request, 'request'); - this.list.appendChild(item); - }); - } - - /** - * Renders search results - */ - renderSearchResults(users) { - this.list.innerHTML = ''; - this.message.textContent = ''; - - if (users.length === 0) { - this.showMessage('Aucun utilisateur trouve', 'info'); - return; - } - - users.forEach(user => { - const item = this.createFriendItem(user, 'search'); - this.list.appendChild(item); - }); - } - - /** - * Creates a friend/user item - */ - createFriendItem(user, type) { - const item = this.createElement('div', CSS.FRIENDS_ITEM); - - const avatar = this.createElement('img', CSS.FRIENDS_AVATAR, { - alt: user.username - }); - avatar.src = user.avatar_url || '/avatar/default.png'; - - const infoContainer = this.createElement('div', 'friends__info'); - - const name = this.createElement('span', CSS.FRIENDS_NAME, { - text: user.username - }); - - infoContainer.appendChild(name); - - // Show stats for friends - if (type === 'friend' && user.total_points !== undefined) { - const stats = this.createElement('span', 'friends__stats', { - text: `${user.total_points || 0} pts` - }); - infoContainer.appendChild(stats); - } - - const actions = this.createElement('div', CSS.FRIENDS_ACTIONS); - - if (type === 'friend') { - const removeBtn = this.createElement('button', [CSS.BTN, CSS.BTN_DANGER], { - text: 'Retirer' - }); - removeBtn.addEventListener('click', () => this.removeFriend(user.id)); - actions.appendChild(removeBtn); - } else if (type === 'request') { - const acceptBtn = this.createElement('button', [CSS.BTN, CSS.BTN_SUCCESS], { - text: 'Accepter' - }); - acceptBtn.addEventListener('click', () => this.acceptRequest(user.id)); - - const declineBtn = this.createElement('button', [CSS.BTN, CSS.BTN_DANGER], { - text: 'Refuser' - }); - declineBtn.addEventListener('click', () => this.declineRequest(user.id)); - - actions.append(acceptBtn, declineBtn); - } else if (type === 'search') { - const addBtn = this.createElement('button', [CSS.BTN, CSS.BTN_PRIMARY], { - text: 'Ajouter' - }); - addBtn.addEventListener('click', () => this.sendRequest(user.id, addBtn)); - actions.appendChild(addBtn); - } - - item.append(avatar, infoContainer, actions); - return item; - } - - /** - * Sends a friend request - */ - async sendRequest(userId, button) { - try { - const response = await fetch(`${API.FRIENDS.REQUEST}/${userId}`, { - method: 'POST', - headers: this.getHeaders() - }); - const data = await response.json(); - - if (!response.ok) { - this.showMessage(data.error || 'Erreur', 'error'); - return; - } - - button.textContent = 'Envoye'; - button.disabled = true; - this.showMessage('Demande envoyee', 'success'); - } catch (error) { - console.error('Send request error:', error); - this.showMessage('Erreur', 'error'); - } - } - - /** - * Accepts a friend request - */ - async acceptRequest(userId) { - try { - const response = await fetch(`${API.FRIENDS.ACCEPT}/${userId}`, { - method: 'POST', - headers: this.getHeaders() - }); - const data = await response.json(); - - if (!response.ok) { - this.showMessage(data.error || 'Erreur', 'error'); - return; - } - - this.showMessage('Ami ajoute', 'success'); - this.loadRequests(); - } catch (error) { - console.error('Accept request error:', error); - this.showMessage('Erreur', 'error'); - } - } - - /** - * Declines a friend request - */ - async declineRequest(userId) { - try { - const response = await fetch(`${API.FRIENDS.DECLINE}/${userId}`, { - method: 'POST', - headers: this.getHeaders() - }); - const data = await response.json(); - - if (!response.ok) { - this.showMessage(data.error || 'Erreur', 'error'); - return; - } - - this.showMessage('Demande refusee', 'success'); - this.loadRequests(); - } catch (error) { - console.error('Decline request error:', error); - this.showMessage('Erreur', 'error'); - } - } - - /** - * Removes a friend - */ - async removeFriend(userId) { - try { - const response = await fetch(`${API.FRIENDS.LIST}/${userId}`, { - method: 'DELETE', - headers: this.getHeaders() - }); - const data = await response.json(); - - if (!response.ok) { - this.showMessage(data.error || 'Erreur', 'error'); - return; - } - - this.showMessage('Ami retire', 'success'); - this.loadFriends(); - } catch (error) { - console.error('Remove friend error:', error); - this.showMessage('Erreur', 'error'); - } - } - - /** - * Shows a message - */ - showMessage(text, type = 'info') { - this.message.textContent = text; - this.message.className = CSS.MESSAGE; - - if (type === 'success') { - this.message.classList.add(CSS.MESSAGE_SUCCESS); - } else if (type === 'error') { - this.message.classList.add(CSS.MESSAGE_ERROR); - } else { - this.message.classList.add(CSS.MESSAGE_INFO); - } - } -} diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/game.css b/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/game.css deleted file mode 100755 index b40f7ff..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/game.css +++ /dev/null @@ -1,1021 +0,0 @@ -:root { - --color-primary: #0066cc; - --color-primary-hover: #0052a3; - --color-success: #3cff01; - --color-success-dark: #28a745; - --color-error: #ff4d4d; - --color-warning: #ffc107; - --color-github: #24292e; - - --color-bg: #000; - - --app-background-base: radial-gradient( - circle at top, - #1b2735, - #090a0f - ); - - /* --app-background-image: url("./assets/background.png"); */ - - --color-surface: #222; - --color-surface-light: #333; - --color-text: #fff; - --color-text-muted: #aaa; - - --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; -} - -/* ============================================ - RESET & BASE - ============================================ */ -*, -*::before, -*::after { - box-sizing: border-box; -} - -html { - height: 100%; - background-image: - var(--app-background-base); - - background-size: - contain, - cover; - - background-position: - center, - center; - - background-repeat: - no-repeat, - no-repeat; -} - -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - color: var(--color-text); - line-height: 1.5; -} - -.app { - position: relative; - width: 70%; - min-width: 800px; - margin: 0 auto; -} - - -/* ============================================ - TYPOGRAPHY - ============================================ */ -.title { - position: absolute; - top: 0; - left: 50%; - transform: translateX(-50%); - text-transform: uppercase; - display: flex; - align-items: center; - justify-content: center; - gap: 20px; - font-size: var(--font-size-xl); - text-align: center; - text-shadow: 2px 2px 10px black; - z-index: 1; - font-family: "Cinzel Decorative", cursive; - color: var(--color-success); - margin: 0; - padding: var(--spacing-md); -} - -/* ============================================ - MENU - ============================================ */ - -.menu { - position: fixed; - top: 0; - left: 50px; - padding: 0; - margin: 0; - z-index: var(--z-menu); - display: flex; - flex-direction: column; - gap: var(--spacing-xs); -} - -.menu__item { - background: var(--color-surface); - color: var(--color-text); - border: 1px solid var(--color-surface-light); - padding: var(--spacing-sm) var(--spacing-md); - font-size: var(--font-size-md); - cursor: pointer; - transition: all var(--transition-fast); - text-align: left; -} - -.menu__item:hover { - background: var(--color-surface-light); - font-size: var(--font-size-lg); -} - -.menu__item--active { - background: var(--color-primary); - border-color: var(--color-primary); -} - -/* ============================================ - GAME - ============================================ */ - -.game { - position: fixed; - top: 0; - right: 50px; - padding: 0; - margin: 0; - z-index: var(--z-menu); - display: flex; - flex-direction: column; - gap: var(--spacing-xs); -} - -.game__item { - background: var(--color-surface); - color: var(--color-text); - border: 1px solid var(--color-surface-light); - padding: var(--spacing-sm) var(--spacing-md); - font-size: var(--font-size-md); - cursor: pointer; - transition: all var(--transition-fast); - text-align: right; -} - -.game__item:hover { - background: var(--color-surface-light); - font-size: var(--font-size-lg); -} - -.game__item--active { - background: var(--color-primary); - border-color: var(--color-primary); -} - -/* ============================================ - PAGES - ============================================ */ - -.page { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - - display: flex; - flex-direction: column; - gap: var(--spacing-xs); - - z-index: var(--z-menu); -} - -.page__item { - background: var(--color-surface); - color: var(--color-text); - border: 1px solid var(--color-surface-light); - padding: var(--spacing-sm) var(--spacing-md); - font-size: var(--font-size-md); - cursor: pointer; - transition: all var(--transition-fast); - text-align: right; -} - -.page__item:hover { - background: var(--color-surface-light); - font-size: var(--font-size-lg); -} - -.page__item--active { - background: var(--color-primary); - border-color: var(--color-primary); -} - -/* ============================================ - BUTTONS - ============================================ */ - -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - padding: var(--spacing-sm) var(--spacing-md); - font-size: var(--font-size-md); - font-weight: 500; - border: none; - border-radius: var(--radius-md); - cursor: pointer; - transition: all var(--transition-fast); - text-decoration: none; -} - -.btn:hover { - opacity: 0.9; - transform: translateY(-1px); -} - -.btn:active { - transform: translateY(0); -} - -.btn--primary { - background: var(--color-primary); - color: var(--color-text); -} - -.btn--primary:hover { - background: var(--color-primary-hover); -} - -.btn--secondary { - background: var(--color-surface-light); - color: var(--color-text); -} - -.btn--success { - background: var(--color-success-dark); - color: var(--color-text); -} - -.btn--danger { - background: var(--color-error); - color: var(--color-text); -} - -.btn--github { - background: var(--color-github); - color: var(--color-text); -} - -.btn--ghost { - background: transparent; - color: var(--color-text); - border: 1px solid var(--color-surface-light); -} - -/* ============================================ - INPUTS - ============================================ */ -.input { - width: 100%; - padding: var(--spacing-sm) var(--spacing-md); - font-size: var(--font-size-md); - background: var(--color-surface); - color: var(--color-text); - border: 1px solid var(--color-surface-light); - border-radius: var(--radius-md); - transition: border-color var(--transition-fast); -} - -.input:focus { - outline: none; - border-color: var(--color-primary); -} - -.input::placeholder { - color: var(--color-text-muted); -} - -.input-group { - display: flex; - flex-direction: column; - gap: var(--spacing-sm); -} - -/* ============================================ - WINDOWS - ============================================ */ -.window { - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background: var(--color-bg); - border: 2px ridge var(--color-text); - color: var(--color-text); - z-index: var(--z-window); - display: none; - flex-direction: column; - min-width: 280px; - box-shadow: var(--shadow-lg); -} - -.window--visible { - display: flex; -} - -.window--left { - left: 25%; -} - -.window--right { - left: 75%; -} - -.window__header { - display: flex; - justify-content: space-between; - align-items: center; - padding: var(--spacing-sm) var(--spacing-md); - background: var(--color-surface); - cursor: move; - user-select: none; -} - -.window__title { - font-weight: 500; - font-size: var(--font-size-md); -} - -.window__close { - cursor: pointer; - font-size: var(--font-size-lg); - opacity: 0.8; - transition: opacity var(--transition-fast); - background: none; - border: none; - color: var(--color-text); - padding: 0; - line-height: 1; -} - -.window__close:hover { - opacity: 1; -} - -.window__body { - padding: var(--spacing-md); - display: flex; - flex-direction: column; - gap: var(--spacing-sm); - flex: 1; - overflow: auto; -} - -/* ============================================ - MESSAGES - ============================================ */ -.message { - font-size: var(--font-size-sm); - padding: var(--spacing-xs); - border-radius: var(--radius-sm); -} - -.message--success { - color: var(--color-success); -} - -.message--error { - color: var(--color-error); -} - -.message--info { - color: var(--color-text-muted); -} - -/* ============================================ - LOGIN WINDOW - ============================================ */ -.login { - width: 320px; -} - -.login__form { - display: flex; - flex-direction: column; - gap: var(--spacing-sm); -} - -.login__actions { - display: flex; - gap: var(--spacing-sm); - margin-top: var(--spacing-xs); -} - -.login__divider { - display: flex; - align-items: center; - gap: var(--spacing-sm); - color: var(--color-text-muted); - font-size: var(--font-size-sm); - margin: var(--spacing-sm) 0; -} - -.login__divider::before, -.login__divider::after { - content: ''; - flex: 1; - height: 1px; - background: var(--color-surface-light); -} - -/* ============================================ - CHAT WINDOW - ============================================ */ -.chat { - width: 380px; - height: 400px; -} - -.chat__output { - flex: 1; - overflow-y: auto; - padding: var(--spacing-sm); - background: var(--color-surface); - border-radius: var(--radius-md); - display: flex; - flex-direction: column; - gap: var(--spacing-sm); - min-height: 150px; -} - -.chat__message { - padding: var(--spacing-xs) var(--spacing-sm); - background: var(--color-surface-light); - border-radius: var(--radius-sm); - font-size: var(--font-size-sm); -} - -.chat__message--own { - background: var(--color-primary); - align-self: flex-end; -} - -.chat__friend-indicator { - display: inline-block; - width: 8px; - height: 8px; - background-color: var(--color-success); - border-radius: 50%; - margin-right: var(--spacing-xs); - vertical-align: middle; -} - -.chat__system { - color: var(--color-text-muted); - font-size: var(--font-size-sm); - font-style: italic; - text-align: center; -} - -.chat__system--error { - color: var(--color-error); -} - -.chat__system--success { - color: var(--color-success); -} - -.chat__input-container { - display: flex; - gap: var(--spacing-sm); - margin-top: var(--spacing-sm); -} - -.chat__input { - flex: 1; -} - -.chat__controls { - display: flex; - gap: var(--spacing-sm); - margin-top: var(--spacing-sm); -} - -/* ============================================ - AVATAR WINDOW - ============================================ */ -.avatar-window { - width: 360px; -} - -.avatar__preview { - width: 120px; - height: 120px; - object-fit: cover; - border-radius: var(--radius-full); - border: 3px solid var(--color-text); - box-shadow: var(--shadow-md); - background: var(--color-surface); - align-self: center; -} - -.avatar__username { - font-size: var(--font-size-lg); - font-weight: 600; - text-align: center; - color: var(--color-text); - margin-top: var(--spacing-sm); -} - -.avatar__controls { - display: flex; - flex-direction: column; - gap: var(--spacing-sm); - align-items: center; -} - -.avatar__file-input { - display: none; -} - -/* ============================================ - EASTER EGG BUTTON - ============================================ */ -/* .easter-egg { - position: absolute; - top: 20%; - left: 50%; - transform: translateX(-50%); - z-index: 1; - background: var(--color-surface); - color: var(--color-text); - border: 1px solid var(--color-surface-light); - padding: var(--spacing-sm) var(--spacing-md); - cursor: pointer; - font-size: var(--font-size-md); - border-radius: var(--radius-md); - transition: all var(--transition-fast); -} - -.easter-egg:hover { - background: var(--color-error); - border-color: var(--color-error); -} */ - -/* ============================================ - UTILITIES - ============================================ */ -.hidden { - display: none !important; -} - -.visually-hidden { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - border: 0; -} - -.text-center { - text-align: center; -} - -.flex-center { - display: flex; - align-items: center; - justify-content: center; -} - -/* ============================================ - FRIENDS WINDOW - ============================================ */ -.friends-window { - width: 400px; - height: 450px; -} - -.friends__tabs { - display: flex; - gap: var(--spacing-xs); - margin-bottom: var(--spacing-sm); -} - -.friends__tab { - flex: 1; - padding: var(--spacing-sm); - background: var(--color-surface); - border: 1px solid var(--color-surface-light); - color: var(--color-text); - cursor: pointer; - font-size: var(--font-size-sm); - transition: all var(--transition-fast); -} - -.friends__tab:hover { - background: var(--color-surface-light); -} - -.friends__tab--active { - background: var(--color-primary); - border-color: var(--color-primary); -} - -.friends__content { - display: flex; - flex-direction: column; - flex: 1; - overflow: hidden; -} - -.friends__search { - display: flex; - gap: var(--spacing-sm); - margin-bottom: var(--spacing-sm); -} - -.friends__search .input { - flex: 1; -} - -.friends__list { - flex: 1; - overflow-y: auto; - display: flex; - flex-direction: column; - gap: var(--spacing-sm); -} - -.friends__item { - display: flex; - align-items: center; - gap: var(--spacing-sm); - padding: var(--spacing-sm); - background: var(--color-surface); - border-radius: var(--radius-md); -} - -.friends__avatar { - width: 40px; - height: 40px; - border-radius: var(--radius-full); - object-fit: cover; - border: 2px solid var(--color-surface-light); -} - -.friends__name { - flex: 1; - font-size: var(--font-size-md); - font-weight: 500; -} - -.friends__actions { - display: flex; - gap: var(--spacing-xs); -} - -.friends__actions .btn { - padding: var(--spacing-xs) var(--spacing-sm); - font-size: var(--font-size-sm); -} - -.friends__empty { - text-align: center; - color: var(--color-text-muted); - padding: var(--spacing-lg); -} - -/* ============================================ - GAME ROOM WINDOW - ============================================ */ -.gameroom-window { - width: 600px; - height: 800px; -} - -.gameroom__tabs { - display: flex; - gap: var(--spacing-xs); - margin-bottom: var(--spacing-sm); -} - -.gameroom__tab { - flex: 1; - padding: var(--spacing-sm); - background: var(--color-surface); - border: 1px solid var(--color-surface-light); - color: var(--color-text); - cursor: pointer; - font-size: var(--font-size-sm); - transition: all var(--transition-fast); -} - -.gameroom__tab:hover { - background: var(--color-surface-light); -} - -.gameroom__tab--active { - background: var(--color-primary); - border-color: var(--color-primary); -} - -.gameroom__content { - display: flex; - flex-direction: column; - flex: 1; - overflow: hidden; -} - -.gameroom__create { - display: flex; - flex-direction: column; - gap: var(--spacing-sm); - margin-bottom: var(--spacing-sm); -} - -.gameroom__list { - flex: 1; - overflow-y: auto; - display: flex; - flex-direction: column; - gap: var(--spacing-sm); -} - -.gameroom__item { - display: flex; - align-items: center; - gap: var(--spacing-sm); - padding: var(--spacing-sm); - background: var(--color-surface); - border-radius: var(--radius-md); -} - -.gameroom__name { - flex: 1; - font-size: var(--font-size-md); - font-weight: 500; -} - -.gameroom__players { - font-size: var(--font-size-sm); - color: var(--color-text-muted); - padding: var(--spacing-xs) var(--spacing-sm); - background: var(--color-surface-light); - border-radius: var(--radius-sm); -} - -.gameroom__actions { - display: flex; - gap: var(--spacing-xs); -} - -.gameroom__actions .btn { - padding: var(--spacing-xs) var(--spacing-sm); - font-size: var(--font-size-sm); -} - -.gameroom__lobby { - display: flex; - flex-direction: column; - flex: 1; - gap: var(--spacing-sm); -} - -.gameroom__lobby-title { - margin: 0; - font-size: var(--font-size-lg); - text-align: center; - color: var(--color-success); -} - -.gameroom__player-list { - flex: 1; - overflow-y: auto; - display: flex; - flex-direction: column; - gap: var(--spacing-sm); - background: var(--color-surface); - border-radius: var(--radius-md); - padding: var(--spacing-sm); -} - -.gameroom__player { - display: flex; - align-items: center; - gap: var(--spacing-sm); - padding: var(--spacing-xs) var(--spacing-sm); - background: var(--color-surface-light); - border-radius: var(--radius-sm); -} - -.gameroom__player-avatar { - width: 32px; - height: 32px; - border-radius: var(--radius-full); - object-fit: cover; - border: 2px solid var(--color-surface-light); -} - -.gameroom__player-name { - flex: 1; - font-size: var(--font-size-md); -} - -.gameroom__player-stats { - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 2px; -} - -.gameroom__player-score { - font-size: var(--font-size-sm); - color: var(--color-success); - font-weight: 500; -} - -.gameroom__player-total { - font-size: var(--font-size-sm); - color: var(--color-text-muted); -} - -.gameroom__empty { - text-align: center; - color: var(--color-text-muted); - padding: var(--spacing-lg); -} - -/* ============================================ - GAME - JEU DU PENDU/DESSIN - ============================================ */ - -.gameroom__lobby-buttons { - display: flex; - gap: var(--spacing-sm); - margin-top: auto; -} - -.gameroom__lobby-buttons .btn { - flex: 1; -} - -.gameroom__game { - display: flex; - flex-direction: column; - gap: var(--spacing-sm); - flex: 1; -} - -.gameroom__game-info { - text-align: center; -} - -.gameroom__drawer-info { - font-size: var(--font-size-md); - color: var(--color-text-muted); - padding: var(--spacing-xs); -} - -.gameroom__scores-display { - font-size: var(--font-size-sm); - color: var(--color-success); - padding: var(--spacing-xs); - background: var(--color-surface); - border-radius: var(--radius-sm); - margin-top: var(--spacing-xs); -} - -.gameroom__drawer-info--winner { - color: var(--color-success); - font-weight: bold; - animation: pulse 0.5s ease-in-out 3; -} - -@keyframes pulse { - 0%, 100% { transform: scale(1); } - 50% { transform: scale(1.05); } -} - -.gameroom__word-display { - font-size: var(--font-size-xl); - font-family: monospace; - text-align: center; - letter-spacing: 8px; - padding: var(--spacing-md); - background: var(--color-surface); - border-radius: var(--radius-md); - min-height: 60px; - display: flex; - align-items: center; - justify-content: center; - color: var(--color-success); -} - -.gameroom__canvas-container { - display: flex; - justify-content: center; -} - -.gameroom__canvas { - background: var(--color-surface-light); - border-radius: var(--radius-md); - cursor: crosshair; - border: 2px solid var(--color-surface-light); -} - -.gameroom__draw-tools { - display: flex; - gap: var(--spacing-sm); - justify-content: center; - align-items: center; -} - -.gameroom__color-picker { - width: 40px; - height: 32px; - border: none; - border-radius: var(--radius-sm); - cursor: pointer; - background: transparent; -} - -.gameroom__word-input-container, -.gameroom__guess-container { - display: flex; - gap: var(--spacing-sm); -} - -.gameroom__word-input-container .input, -.gameroom__guess-container .input { - flex: 1; -} - -.gameroom__guess-container .input:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.gameroom__guess-container .btn:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.gameroom__guess-history { - flex: 1; - min-height: 60px; - max-height: 100px; - overflow-y: auto; - background: var(--color-surface); - border-radius: var(--radius-md); - padding: var(--spacing-sm); - display: flex; - flex-direction: column; - gap: var(--spacing-xs); -} - -.gameroom__guess-item { - font-size: var(--font-size-sm); - padding: var(--spacing-xs) var(--spacing-sm); - border-radius: var(--radius-sm); -} - -.gameroom__guess-item--success { - background: rgba(60, 255, 1, 0.2); - color: var(--color-success); -} - -.gameroom__guess-item--fail { - background: rgba(255, 77, 77, 0.2); - color: var(--color-error); -} - -.gameroom__game-buttons { - display: flex; - gap: var(--spacing-sm); - margin-top: var(--spacing-sm); -} - -.gameroom__game-buttons .btn { - flex: 1; -} diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/game.html b/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/game.html deleted file mode 100755 index 51aef37..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/game.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - Lobby - - - - - - -

Lobby

- - - - - -
- -
- - - - - \ No newline at end of file diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/game_room.js b/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/game_room.js deleted file mode 100755 index d5881e9..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/game_room.js +++ /dev/null @@ -1,1580 +0,0 @@ -import { Window } from './windows.js'; -import { API, STORAGE_KEYS, CSS } from './config.js'; -import { eventBus, Events } from './events.js'; - -export class GameRoomWindow extends Window { - constructor() { - super({ - name: 'gameroom', - title: 'Game Rooms', - cssClasses: ['gameroom-window'] - }); - - this.currentTab = 'browse'; - this.currentRoom = null; - this.roomsList = []; - this.socket = null; - this.isSpectating = false; - this.messageTimeout = null; - this.buildUI(); - this.bindEvents(); - - // Handle page close/refresh to disconnect socket - window.addEventListener('beforeunload', () => { - if (this.socket?.connected) { - this.socket.disconnect(); - } - }); - - eventBus.on(Events.USER_LOGGED_IN, () => { - this.updateTabsAccess(); - this.checkCurrentRoom(); - }); - eventBus.on(Events.USER_LOGGED_OUT, () => { - this.handleLogout(); - }); - - this.updateTabsAccess(); - - // Verifier si l'utilisateur est deja dans un salon au chargement - const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); - if (token) - this.checkCurrentRoom(); - } - - buildUI() { - this.tabs = this.createElement('div', CSS.GAMEROOM_TABS); - - this.browseTab = this.createElement('button', [CSS.GAMEROOM_TAB, CSS.GAMEROOM_TAB_ACTIVE], { - text: 'Salons' - }); - this.browseTab.dataset.tab = 'browse'; - - this.spectatorTab = this.createElement('button', CSS.GAMEROOM_TAB, { - text: 'Spectateur' - }); - this.spectatorTab.dataset.tab = 'spectator'; - - this.createTab = this.createElement('button', CSS.GAMEROOM_TAB, { - text: 'Creer' - }); - this.createTab.dataset.tab = 'create'; - - this.lobbyTab = this.createElement('button', CSS.GAMEROOM_TAB, { - text: 'Lobby' - }); - this.lobbyTab.dataset.tab = 'lobby'; - this.lobbyTab.style.display = 'none'; - - this.tabs.append(this.browseTab, this.spectatorTab, this.createTab, this.lobbyTab); - - this.content = this.createElement('div', CSS.GAMEROOM_CONTENT); - - this.createContainer = this.createElement('div', CSS.GAMEROOM_CREATE); - this.roomNameInput = this.createElement('input', CSS.INPUT, { - type: 'text', - placeholder: 'Nom du salon...' - }); - this.createBtn = this.createElement('button', [CSS.BTN, CSS.BTN_PRIMARY], { - text: 'Creer le salon' - }); - this.createContainer.append(this.roomNameInput, this.createBtn); - this.createContainer.style.display = 'none'; - - this.lobbyContainer = this.createElement('div', CSS.GAMEROOM_LOBBY); - this.lobbyTitle = this.createElement('h3', 'gameroom__lobby-title', { text: '' }); - this.playerList = this.createElement('div', CSS.GAMEROOM_PLAYER_LIST); - - // Boutons du lobby - this.lobbyButtons = this.createElement('div', 'gameroom__lobby-buttons'); - this.startGameBtn = this.createElement('button', [CSS.BTN, CSS.BTN_SUCCESS], { - text: 'Lancer le jeu' - }); - this.leaveBtn = this.createElement('button', [CSS.BTN, CSS.BTN_DANGER], { - text: 'Quitter' - }); - this.lobbyButtons.append(this.startGameBtn, this.leaveBtn); - - // Container du jeu (caché par défaut) - this.gameContainer = this.createElement('div', 'gameroom__game'); - this.gameContainer.style.display = 'none'; - this.buildGameUI(); - - this.lobbyContainer.append(this.lobbyTitle, this.playerList, this.lobbyButtons, this.gameContainer); - this.lobbyContainer.style.display = 'none'; - - this.list = this.createElement('div', CSS.GAMEROOM_LIST); - - this.spectatorList = this.createElement('div', CSS.GAMEROOM_LIST); - this.spectatorList.style.display = 'none'; - - this.message = this.createElement('div', CSS.MESSAGE); - - this.content.append(this.createContainer, this.lobbyContainer, this.list, this.spectatorList, this.message); - - this.body.append(this.tabs, this.content); - } - - buildGameUI() { - // Zone d'info du jeu - 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); - - // Affichage du mot caché - this.wordDisplay = this.createElement('div', 'gameroom__word-display'); - - // Canvas de dessin - this.canvasContainer = this.createElement('div', 'gameroom__canvas-container'); - this.canvas = document.createElement('canvas'); - this.canvas.className = 'gameroom__canvas'; - this.canvas.width = 380; - this.canvas.height = 200; - this.ctx = this.canvas.getContext('2d'); - this.canvasContainer.appendChild(this.canvas); - - // Outils de dessin - this.drawTools = this.createElement('div', 'gameroom__draw-tools'); - this.colorPicker = this.createElement('input', 'gameroom__color-picker'); - this.colorPicker.type = 'color'; - this.colorPicker.value = '#ffffff'; - this.clearCanvasBtn = this.createElement('button', [CSS.BTN, CSS.BTN_SECONDARY], { text: 'Effacer' }); - this.drawTools.append(this.colorPicker, this.clearCanvasBtn); - this.drawTools.style.display = 'none'; - - // Zone pour choisir le mot (pour le dessinateur) - this.wordInputContainer = this.createElement('div', 'gameroom__word-input-container'); - this.wordInput = this.createElement('input', CSS.INPUT, { - type: 'text', - placeholder: 'Entrez le mot a faire deviner...' - }); - this.confirmWordBtn = this.createElement('button', [CSS.BTN, CSS.BTN_PRIMARY], { text: 'OK' }); - this.wordInputContainer.append(this.wordInput, this.confirmWordBtn); - this.wordInputContainer.style.display = 'none'; - - // Zone pour deviner (pour les autres joueurs) - this.guessContainer = this.createElement('div', 'gameroom__guess-container'); - this.letterInput = this.createElement('input', CSS.INPUT, { - type: 'text', - placeholder: 'Proposez une lettre ou le mot...', - maxLength: '50' - }); - this.guessBtn = this.createElement('button', [CSS.BTN, CSS.BTN_PRIMARY], { text: 'Deviner' }); - this.guessContainer.append(this.letterInput, this.guessBtn); - this.guessContainer.style.display = 'none'; - - // Historique des tentatives - this.guessHistory = this.createElement('div', 'gameroom__guess-history'); - - // Boutons du jeu - this.gameButtons = this.createElement('div', 'gameroom__game-buttons'); - this.backToLobbyBtn = this.createElement('button', [CSS.BTN, CSS.BTN_SECONDARY], { text: 'Quitter la partie' }); - this.endRoundBtn = this.createElement('button', [CSS.BTN, CSS.BTN_DANGER], { text: 'Terminer le jeu' }); - this.gameButtons.append(this.backToLobbyBtn, this.endRoundBtn); - - this.gameContainer.append( - this.gameInfo, - this.wordDisplay, - this.canvasContainer, - this.drawTools, - this.wordInputContainer, - this.guessContainer, - this.guessHistory, - this.gameButtons - ); - - // Initialiser les variables du jeu - this.gameState = { - isPlaying: false, - currentWord: '', - wordLength: 0, - revealedLetters: [], - revealedWord: [], - drawer: null, - players: [], - currentPlayerIndex: 0, - guessedLetters: [], - scores: {} - }; - - this.initDrawing(); - } - - initDrawing() { - this.isDrawing = false; - this.lastX = 0; - this.lastY = 0; - - this.canvas.addEventListener('mousedown', (e) => { - if (!this.gameState.isPlaying || !this.isCurrentUserDrawer() || this.isSpectating) return; - this.isDrawing = true; - [this.lastX, this.lastY] = [e.offsetX, e.offsetY]; - }); - - this.canvas.addEventListener('mousemove', (e) => { - if (!this.isDrawing) return; - - const x1 = this.lastX; - const y1 = this.lastY; - const x2 = e.offsetX; - const y2 = e.offsetY; - const color = this.colorPicker.value; - const lineWidth = 3; - - // Dessiner localement - this.drawLine(x1, y1, x2, y2, color, lineWidth); - - // Envoyer aux autres joueurs via WebSocket - if (this.socket?.connected) { - this.socket.emit('game-draw', { x1, y1, x2, y2, color, lineWidth }); - } - - [this.lastX, this.lastY] = [x2, y2]; - }); - - this.canvas.addEventListener('mouseup', () => this.isDrawing = false); - this.canvas.addEventListener('mouseout', () => this.isDrawing = false); - - this.clearCanvasBtn.addEventListener('click', () => { - this.clearCanvas(); - // Notifier les autres - if (this.socket?.connected) { - this.socket.emit('game-clear-canvas'); - } - }); - } - - drawLine(x1, y1, x2, y2, color, lineWidth) { - this.ctx.beginPath(); - this.ctx.strokeStyle = color; - this.ctx.lineWidth = lineWidth; - this.ctx.lineCap = 'round'; - this.ctx.moveTo(x1, y1); - this.ctx.lineTo(x2, y2); - this.ctx.stroke(); - } - - clearCanvas() { - this.ctx.fillStyle = '#333'; - this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); - } - - bindEvents() { - this.tabs.addEventListener('click', (e) => { - const tab = e.target.closest(`.${CSS.GAMEROOM_TAB}`); - if (tab) { - this.switchTab(tab.dataset.tab); - } - }); - - this.createBtn.addEventListener('click', () => this.createRoom()); - this.roomNameInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter') this.createRoom(); - }); - - this.leaveBtn.addEventListener('click', () => this.leaveRoom()); - this.startGameBtn.addEventListener('click', () => this.startGame()); - - // Events du jeu - this.confirmWordBtn.addEventListener('click', () => this.confirmWord()); - this.wordInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter') this.confirmWord(); - }); - - this.guessBtn.addEventListener('click', () => this.makeGuess()); - this.letterInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter') this.makeGuess(); - }); - - this.backToLobbyBtn.addEventListener('click', () => this.backToLobby()); - this.endRoundBtn.addEventListener('click', () => this.endGame()); - } - - // ============================================ - // SOCKET.IO CONNECTION - // ============================================ - - async connectToGameSocket() { - const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); - if (!token || !this.currentRoom) return; - - // Ensure socket is connected - await this.ensureSocketConnected(); - - // Join the room - if (this.socket?.connected) { - console.log('Socket connected, joining room:', this.currentRoom.id); - this.socket.emit('game-join-room', { roomId: this.currentRoom.id }); - } - } - - async loadSocketIO() { - if (window.io) return; - - return new Promise((resolve, reject) => { - const script = document.createElement('script'); - script.src = '/socket.io/socket.io.js'; - - script.onload = () => { - console.log('Socket.IO loaded for game'); - resolve(); - }; - - script.onerror = () => { - console.error('Failed to load Socket.IO'); - reject(new Error('Socket.IO load failed')); - }; - - document.head.appendChild(script); - }); - } - - setupGameSocketListeners() { - this.socketReady = false; - - this.socket.on('connect', () => { - console.log('Game socket connected, id:', this.socket.id); - if (this.currentRoom) { - console.log('Joining room:', this.currentRoom.id); - this.socket.emit('game-join-room', { roomId: this.currentRoom.id }); - } - }); - - this.socket.on('connect_error', (err) => { - console.error('Game socket connection error:', err.message); - }); - - // Confirmation that we joined the room - this.socket.on('game-room-joined', (data) => { - console.log('Successfully joined game room:', data.roomId); - this.socketReady = true; - }); - - // Real-time rooms list update - this.socket.on('game-rooms-updated', (data) => { - console.log('Rooms list updated:', data.rooms?.length, 'rooms'); - if (this.currentTab === 'browse') { - this.roomsList = data.rooms || []; - this.renderRoomsList(this.roomsList); - } - }); - - // Real-time player list update in lobby - this.socket.on('game-players-updated', (data) => { - console.log('Players list updated:', data.players?.length, 'players'); - if (this.currentRoom) { - this.renderPlayersList(data.players || []); - } - }); - - // Player joined/left - this.socket.on('game-player-joined', (data) => { - console.log(`${data.username} joined the room`); - }); - - this.socket.on('game-player-left', (data) => { - this.showMessage(`${data.username} a quitté le salon`, 'info'); - console.log(`${data.username} joined the room`); - - if (this.gameState.isPlaying) - { - if (this.gameState.players) - this.gameState.players = this.gameState.players.filter(p => p !== data.username); - } - - if (this.gameState.scores) - { - delete this.gameState.scores[data.username]; - this.updateScoresDisplay(this.gameState.scores); - } - - if (this.gameState.drawer === data.username) - { - this.showMessage('Le dessinateur a quitté la partie, fin du jeu', 'info'); - } - - if (this.currentRoom && !this.gameState.isPlaying) - this.loadLobby(); - }); - - // Game started - this.socket.on('game-started', (data) => { - console.log('Received game-started event:', data); - this.gameState.isPlaying = true; - this.gameState.drawer = data.drawer; - this.gameState.players = data.players; - this.gameState.currentPlayerIndex = data.players.indexOf(data.drawer); - - // Initialize scores - this.gameState.scores = {}; - data.players.forEach(p => this.gameState.scores[p] = 0); - - this.showGameUI(); - this.setupRound(); - }); - - // Game start error - this.socket.on('game-start-error', (data) => { - console.error('Game start error:', data.error); - this.showMessage(data.error || 'Impossible de démarrer la partie', 'error'); - }); - - // Word was set by drawer - this.socket.on('game-word-set', (data) => { - console.log(`Word set by ${data.drawer}, length: ${data.wordLength}`); - this.gameState.wordLength = data.wordLength; - this.gameState.revealedLetters = new Array(data.wordLength).fill(false); - this.gameState.revealedWord = data.revealedWord || new Array(data.wordLength).fill('_'); - - if (data.scores) { - this.updateScoresDisplay(data.scores); - } - - this.updateWordDisplay(); - - // Don't change UI for spectators - if (this.isSpectating) { - this.currentDrawerInfo.textContent = '👁️ MODE SPECTATEUR - Vous regardez la partie'; - return; - } - - this.currentDrawerInfo.textContent = `${data.drawer} dessine (${data.wordLength} lettres)`; - - // Enable guess input for non-drawers - if (!this.isCurrentUserDrawer()) { - this.guessContainer.style.display = 'flex'; - this.letterInput.disabled = false; - this.guessBtn.disabled = false; - this.letterInput.placeholder = 'Proposez une lettre ou le mot...'; - this.letterInput.focus(); - } - }); - - // Drawing received - this.socket.on('game-draw', (data) => { - this.drawLine(data.x1, data.y1, data.x2, data.y2, data.color, data.lineWidth); - }); - - // Clear canvas - this.socket.on('game-clear-canvas', () => { - this.clearCanvas(); - }); - - // Guess result - this.socket.on('game-guess-result', (data) => { - this.addGuessToHistory(data.guess, data.success, data.type, data.username, data.points || 0); - - if (data.revealedLetters) { - this.gameState.revealedLetters = data.revealedLetters; - } - if (data.revealedWord) { - this.gameState.revealedWord = data.revealedWord; - } - if (data.scores) { - this.updateScoresDisplay(data.scores); - } - this.updateWordDisplay(); - }); - - // Word found - this.socket.on('game-word-found', (data) => { - if (data.scores) { - this.updateScoresDisplay(data.scores); - } - this.wordFound(data.word, data.winner, data.drawerBonus || 0); - }); - - // New round - this.socket.on('game-new-round', (data) => { - this.gameState.drawer = data.drawer; - this.gameState.currentPlayerIndex = this.gameState.players.indexOf(data.drawer); - this.setupRound(); - }); - - // Game ended - this.socket.on('game-ended', () => { - // If spectating, return to spectator list - if (this.isSpectating) { - this.resetGameUI(); - this.currentRoom = null; - this.isSpectating = false; - this.switchTab('spectator'); - this.showMessage('La partie est terminée', 'info'); - } else { - this.resetGameUI(); - this.loadLobby(); - } - }); - - // Game message from server - this.socket.on('game-message', (data) => { - this.showMessage(data.message, data.type || 'info'); - }); - - // Sync state for late joiners - this.socket.on('game-state-sync', (data) => { - if (data.isPlaying) { - this.gameState.isPlaying = true; - this.gameState.drawer = data.drawer; - this.gameState.wordLength = data.wordLength; - this.gameState.revealedLetters = data.revealedLetters || []; - this.gameState.revealedWord = data.revealedWord || new Array(data.wordLength).fill('_'); - this.gameState.players = data.players; - this.gameState.scores = data.scores || {}; - - this.showGameUI(); - this.updateWordDisplay(); - - // Update scores display - if (data.scores) { - this.updateScoresDisplay(data.scores); - } - - this.currentDrawerInfo.textContent = `${data.drawer} dessine (${data.wordLength} lettres)`; - - // Don't enable input for spectators - if (this.isSpectating) { - this.guessContainer.style.display = 'none'; - this.wordInputContainer.style.display = 'none'; - this.drawTools.style.display = 'none'; - this.currentDrawerInfo.textContent = '👁️ MODE SPECTATEUR - Vous regardez la partie'; - } else if (!this.isCurrentUserDrawer()) { - this.guessContainer.style.display = 'flex'; - if (data.wordLength > 0) { - this.letterInput.disabled = false; - this.guessBtn.disabled = false; - this.letterInput.placeholder = 'Proposez une lettre ou le mot...'; - } else { - this.letterInput.disabled = true; - this.guessBtn.disabled = true; - this.letterInput.placeholder = 'En attente du mot...'; - } - } - } - }); - - // Spectator events - this.socket.on('game-spectate-joined', (data) => { - console.log('Successfully joined as spectator:', data.roomId); - this.isSpectating = true; - - // Prepare UI for spectating - this.spectatorList.style.display = 'none'; - this.list.style.display = 'none'; - this.createContainer.style.display = 'none'; - this.lobbyContainer.style.display = 'flex'; - - // Hide lobby elements, keep game container for when state syncs - this.playerList.style.display = 'none'; - this.lobbyButtons.style.display = 'none'; - this.lobbyTitle.textContent = 'Mode Spectateur'; - - this.showMessage('Vous regardez la partie...', 'success'); - // The game state will be synced via game-state-sync event - }); - - this.socket.on('game-spectate-error', (data) => { - console.error('Spectate error:', data.error); - this.showMessage(data.error || 'Impossible de regarder cette partie', 'error'); - }); - - this.socket.on('game-spectator-joined', (data) => { - console.log(`Spectator ${data.username} joined`); - }); - - this.socket.on('game-spectator-left', (data) => { - console.log(`Spectator ${data.username} left`); - }); - } - - disconnectGameSocket() { - if (this.socket) { - if (this.isSpectating) { - this.socket.emit('game-leave-spectate'); - } else { - this.socket.emit('game-leave-room'); - } - } - } - - // ============================================ - // UI HELPERS - // ============================================ - - isLoggedIn() { - return !!localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); - } - - updateTabsAccess() { - const loggedIn = this.isLoggedIn(); - - this.createTab.disabled = !loggedIn; - this.createTab.style.opacity = loggedIn ? '1' : '0.5'; - this.createTab.title = loggedIn ? '' : 'Connectez-vous pour creer un salon'; - - if (!loggedIn && this.currentTab === 'create') { - this.switchTab('browse'); - } - } - - handleLogout() { - this.disconnectGameSocket(); - if (this.currentRoom) { - this.exitLobby(); - } - this.updateTabsAccess(); - if (this.currentTab !== 'browse') { - this.switchTab('browse'); - } - } - - switchTab(tabName) { - if (tabName === 'lobby' && !this.currentRoom) { - return; - } - - if (tabName === 'create' && !this.isLoggedIn()) { - this.showMessage('Connectez-vous pour creer un salon', 'info'); - return; - } - - this.currentTab = tabName; - - [this.browseTab, this.spectatorTab, this.createTab, this.lobbyTab].forEach(tab => { - tab.classList.toggle(CSS.GAMEROOM_TAB_ACTIVE, tab.dataset.tab === tabName); - }); - - this.createContainer.style.display = tabName === 'create' ? 'flex' : 'none'; - this.lobbyContainer.style.display = tabName === 'lobby' ? 'flex' : 'none'; - this.list.style.display = tabName === 'browse' ? 'flex' : 'none'; - this.spectatorList.style.display = tabName === 'spectator' ? 'flex' : 'none'; - - this.loadCurrentTab(); - } - - loadCurrentTab() { - switch (this.currentTab) { - case 'browse': - this.loadRooms(); - // Connect to socket to receive real-time room updates - this.ensureSocketConnected(); - break; - case 'spectator': - this.loadPlayingRooms(); - this.ensureSocketConnected(); - break; - case 'create': - this.message.textContent = ''; - this.ensureSocketConnected(); - break; - case 'lobby': - if (this.currentRoom) { - this.loadLobby(); - } - break; - } - } - - async ensureSocketConnected() { - if (!this.isLoggedIn()) return; - if (this.socket?.connected) return; - - const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); - if (!token) return; - - await this.loadSocketIO(); - - const ioConfig = { - auth: { token }, - reconnection: true, - reconnectionAttempts: 5, - reconnectionDelay: 1000, - transports: ['websocket', 'polling'] - }; - - const altPort = window.GLOBAL_CHAT_ALT_PORT; - if (altPort) { - const host = location.hostname || 'localhost'; - this.socket = io(`http://${host}:${altPort}`, ioConfig); - } else { - this.socket = io(ioConfig); - } - - this.setupGameSocketListeners(); - } - - getHeaders() { - const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); - return { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }; - } - - async loadRooms() { - const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); - if (!token) { - this.showMessage('Connectez-vous pour voir les salons', 'info'); - return; - } - - try { - const response = await fetch(API.ROOMS.LIST, { - headers: this.getHeaders() - }); - const data = await response.json(); - - if (!response.ok) { - this.showMessage(data.error || 'Erreur', 'error'); - return; - } - - this.roomsList = data || []; - this.renderRoomsList(this.roomsList); - } catch (error) { - console.error('Load rooms error:', error); - this.showMessage('Erreur de connexion', 'error'); - } - } - - async checkCurrentRoom() { - const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); - if (!token) { - return null; - } - - try { - const response = await fetch(API.ROOMS.CURRENT, { - headers: this.getHeaders() - }); - - // 204 No Content means user is not in any room - if (response.status === 204) { - return null; - } - - if (!response.ok) { - return null; - } - - const data = await response.json(); - if (data && data.id) { - this.currentRoom = data; - this.enterLobby(data); - return data; - } - return null; - } catch (error) { - console.error('Check current room error:', error); - return null; - } - } - - roomNameExists(name) { - const normalizedName = name.toLowerCase().trim(); - return this.roomsList.some(room => room.name.toLowerCase().trim() === normalizedName); - } - - renderRoomsList(rooms) { - this.list.innerHTML = ''; - this.message.textContent = ''; - - if (rooms.length === 0) { - this.showMessage('Aucun salon disponible', 'info'); - return; - } - - rooms.forEach(room => { - const item = this.createRoomItem(room); - this.list.appendChild(item); - }); - } - - createRoomItem(room) { - const item = this.createElement('div', CSS.GAMEROOM_ITEM); - - const name = this.createElement('span', CSS.GAMEROOM_NAME, { - text: room.name - }); - - const players = this.createElement('span', CSS.GAMEROOM_PLAYERS, { - text: `${room.player_count || 0}/${room.max_players || 8}` - }); - - const actions = this.createElement('div', CSS.GAMEROOM_ACTIONS); - - const joinBtn = this.createElement('button', [CSS.BTN, CSS.BTN_SUCCESS], { - text: 'Rejoindre' - }); - joinBtn.addEventListener('click', () => this.joinRoom(room.id)); - actions.appendChild(joinBtn); - - item.append(name, players, actions); - return item; - } - - createSpectatorRoomItem(room) { - const item = this.createElement('div', CSS.GAMEROOM_ITEM); - - const name = this.createElement('span', CSS.GAMEROOM_NAME, { - text: room.name - }); - - const players = this.createElement('span', CSS.GAMEROOM_PLAYERS, { - text: `${room.player_count || 0}/${room.max_players || 8}` - }); - - const status = this.createElement('span', 'gameroom__status', { - text: '🎮 En cours' - }); - status.style.color = '#4CAF50'; - status.style.fontWeight = 'bold'; - - const actions = this.createElement('div', CSS.GAMEROOM_ACTIONS); - - const spectateBtn = this.createElement('button', [CSS.BTN, CSS.BTN_PRIMARY], { - text: 'Regarder' - }); - spectateBtn.addEventListener('click', () => this.spectateRoom(room.id)); - actions.appendChild(spectateBtn); - - item.append(name, players, status, actions); - return item; - } - - async loadPlayingRooms() { - const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); - if (!token) { - this.showMessage('Connectez-vous pour voir les parties en cours', 'info'); - return; - } - - try { - const response = await fetch(API.ROOMS.PLAYING, { - headers: this.getHeaders() - }); - const data = await response.json(); - - if (!response.ok) { - this.showMessage(data.error || 'Erreur', 'error'); - return; - } - - this.renderPlayingRoomsList(data || []); - } catch (error) { - console.error('Load playing rooms error:', error); - this.showMessage('Erreur de connexion', 'error'); - } - } - - renderPlayingRoomsList(rooms) { - this.spectatorList.innerHTML = ''; - this.message.textContent = ''; - - if (rooms.length === 0) { - this.showMessage('Aucune partie en cours', 'info'); - return; - } - - rooms.forEach(room => { - const item = this.createSpectatorRoomItem(room); - this.spectatorList.appendChild(item); - }); - } - - async spectateRoom(roomId) { - const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); - if (!token) { - this.showMessage('Connectez-vous pour regarder', 'info'); - return; - } - - // Check if user is already in a room as a player - if (this.currentRoom && !this.isSpectating) { - this.showMessage('Vous êtes déjà dans un salon. Quittez-le d\'abord.', 'error'); - return; - } - - // Check if already spectating another game - if (this.isSpectating && this.currentRoom && this.currentRoom.id !== roomId) { - this.showMessage('Vous regardez déjà une autre partie', 'error'); - return; - } - - try { - const response = await fetch(API.ROOMS.SPECTATE(roomId), { - method: 'POST', - headers: this.getHeaders() - }); - const data = await response.json(); - - if (!response.ok) { - this.showMessage(data.error || 'Impossible de regarder cette partie', 'error'); - return; - } - - // Store room info and mark as spectating - this.currentRoom = data; - this.isSpectating = true; - - // Join as spectator via socket - await this.ensureSocketConnected(); - if (this.socket?.connected) { - this.socket.emit('game-spectate-room', { roomId: roomId }); - } - - this.showMessage('Connexion à la partie...', 'info'); - } catch (error) { - console.error('Spectate room error:', error); - this.showMessage('Erreur de connexion', 'error'); - } - } - - async createRoom() { - const name = this.roomNameInput.value.trim(); - if (!name) { - this.showMessage('Entrez un nom pour le salon', 'error'); - return; - } - - const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); - if (!token) { - this.showMessage('Connectez-vous pour creer un salon', 'info'); - return; - } - - if (this.currentRoom) { - this.showMessage('Vous etes deja dans un salon. Quittez-le d\'abord.', 'error'); - return; - } - - try { - const currentResponse = await fetch(API.ROOMS.CURRENT, { - headers: this.getHeaders() - }); - if (currentResponse.ok && currentResponse.status !== 204) { - const currentData = await currentResponse.json(); - if (currentData && currentData.id) { - this.currentRoom = currentData; - this.enterLobby(currentData); - this.showMessage('Vous etes deja dans un salon', 'error'); - return; - } - } - } catch (e) { - // Continue - } - - try { - const listResponse = await fetch(API.ROOMS.LIST, { - headers: this.getHeaders() - }); - if (listResponse.ok) { - this.roomsList = await listResponse.json() || []; - } - } catch (e) { - // Continue - } - - if (this.roomNameExists(name)) { - this.showMessage('Un salon avec ce nom existe deja', 'error'); - return; - } - - try { - const response = await fetch(API.ROOMS.CREATE, { - method: 'POST', - headers: this.getHeaders(), - body: JSON.stringify({ name }) - }); - const data = await response.json(); - - if (!response.ok) { - this.showMessage(data.error || 'Erreur', 'error'); - return; - } - - this.roomNameInput.value = ''; - this.currentRoom = data; - this.showMessage('Salon cree', 'success'); - eventBus.emit(Events.ROOM_CREATED, data); - this.enterLobby(data); - } catch (error) { - console.error('Create room error:', error); - this.showMessage('Erreur de connexion', 'error'); - } - } - - async joinRoom(roomId) { - const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); - if (!token) { - this.showMessage('Connectez-vous pour rejoindre', 'info'); - return; - } - - if (this.currentRoom) { - this.showMessage('Vous etes deja dans un salon. Quittez-le d\'abord.', 'error'); - return; - } - - try { - const currentResponse = await fetch(API.ROOMS.CURRENT, { - headers: this.getHeaders() - }); - if (currentResponse.ok && currentResponse.status !== 204) { - const currentData = await currentResponse.json(); - if (currentData && currentData.id) { - this.currentRoom = currentData; - this.enterLobby(currentData); - this.showMessage('Vous etes deja dans un salon', 'error'); - return; - } - } - } catch (e) { - // Continue - } - - try { - const response = await fetch(API.ROOMS.JOIN(roomId), { - method: 'POST', - headers: this.getHeaders() - }); - const data = await response.json(); - - if (!response.ok) { - this.showMessage(data.error || 'Erreur', 'error'); - return; - } - - const roomResponse = await fetch(API.ROOMS.GET(roomId), { - headers: this.getHeaders() - }); - const roomData = await roomResponse.json(); - - this.currentRoom = roomData; - eventBus.emit(Events.ROOM_JOINED, roomData); - this.enterLobby(roomData); - } catch (error) { - console.error('Join room error:', error); - this.showMessage('Erreur de connexion', 'error'); - } - } - - enterLobby(room) { - this.currentRoom = room; - this.lobbyTab.style.display = 'block'; - this.lobbyTitle.textContent = room.name; - this.switchTab('lobby'); - - // Connect to WebSocket for real-time sync - this.connectToGameSocket(); - } - - async loadLobby() { - if (!this.currentRoom) return; - - this.gameState.scores = {}; - - try { - const response = await fetch(API.ROOMS.PLAYERS(this.currentRoom.id), { - headers: this.getHeaders() - }); - const data = await response.json(); - - if (!response.ok) { - this.showMessage(data.error || 'Erreur', 'error'); - return; - } - - this.renderPlayersList(data || []); - } catch (error) { - console.error('Load lobby error:', error); - this.showMessage('Erreur de connexion', 'error'); - } - } - - renderPlayersList(players) { - this.playerList.innerHTML = ''; - - if (players.length === 0) { - const empty = this.createElement('div', 'gameroom__empty', { - text: 'Aucun joueur' - }); - this.playerList.appendChild(empty); - // Disable start button if no players - this.startGameBtn.disabled = true; - this.startGameBtn.style.opacity = '0.5'; - this.startGameBtn.title = 'Il faut au moins 2 joueurs'; - return; - } - - players.forEach(player => { - const item = this.createElement('div', CSS.GAMEROOM_PLAYER); - - const avatar = this.createElement('img', CSS.GAMEROOM_PLAYER_AVATAR, { - alt: player.username - }); - avatar.src = player.avatar_url || '/avatar/default.png'; - - const name = this.createElement('span', CSS.GAMEROOM_PLAYER_NAME, { - text: player.username - }); - - const statsContainer = this.createElement('div', 'gameroom__player-stats'); - - const score = this.createElement('span', CSS.GAMEROOM_PLAYER_SCORE, { - text: `${player.score || 0} pts` - }); - - const totalPoints = this.createElement('span', 'gameroom__player-total', { - text: `Total: ${player.total_points || 0}` - }); - - statsContainer.append(score, totalPoints); - item.append(avatar, name, statsContainer); - this.playerList.appendChild(item); - }); - - // Enable/disable start button based on player count - if (players.length < 2) { - this.startGameBtn.disabled = true; - this.startGameBtn.style.opacity = '0.5'; - this.startGameBtn.title = 'Il faut au moins 2 joueurs'; - } else { - this.startGameBtn.disabled = false; - this.startGameBtn.style.opacity = '1'; - this.startGameBtn.title = ''; - } - } - - async leaveRoom() { - if (!this.currentRoom) return; - - // End game if playing - if (this.gameState.isPlaying) { - this.endGame(); - } - - this.disconnectGameSocket(); - - try { - const response = await fetch(API.ROOMS.LEAVE(this.currentRoom.id), { - method: 'POST', - headers: this.getHeaders() - }); - - if (!response.ok) { - const data = await response.json(); - this.showMessage(data.error || 'Erreur', 'error'); - return; - } - - eventBus.emit(Events.ROOM_LEFT, this.currentRoom); - this.exitLobby(); - } catch (error) { - console.error('Leave room error:', error); - this.showMessage('Erreur de connexion', 'error'); - } - } - - exitLobby() { - this.currentRoom = null; - this.lobbyTab.style.display = 'none'; - this.playerList.innerHTML = ''; - this.lobbyTitle.textContent = ''; - this.resetGameUI(); - this.switchTab('browse'); - } - - showMessage(text, type = 'info') { - // Clear any existing timeout - if (this.messageTimeout) { - clearTimeout(this.messageTimeout); - } - - this.message.textContent = text; - this.message.className = CSS.MESSAGE; - - if (type === 'success') { - this.message.classList.add(CSS.MESSAGE_SUCCESS); - } else if (type === 'error') { - this.message.classList.add(CSS.MESSAGE_ERROR); - } else { - this.message.classList.add(CSS.MESSAGE_INFO); - } - - // Auto-clear message after 5 seconds - this.messageTimeout = setTimeout(() => { - this.message.textContent = ''; - this.message.className = CSS.MESSAGE; - }, 5000); - } - - // ============================================ - // LOGIQUE DU JEU - // ============================================ - - getCurrentUsername() { - const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); - if (!token) return null; - try { - const payload = JSON.parse(atob(token.split('.')[1])); - return payload.username || payload.sub || 'Joueur'; - } catch { - return 'Joueur'; - } - } - - isCurrentUserDrawer() { - return this.gameState.drawer === this.getCurrentUsername(); - } - - showGameUI() { - this.gameContainer.style.display = 'flex'; - this.playerList.style.display = 'none'; - this.lobbyButtons.style.display = 'none'; - this.clearCanvas(); - this.guessHistory.innerHTML = ''; - - // If spectating, show indicator and disable interactions - if (this.isSpectating) { - this.currentDrawerInfo.textContent = '👁️ MODE SPECTATEUR - Vous regardez la partie'; - this.currentDrawerInfo.style.backgroundColor = '#2196F3'; - this.currentDrawerInfo.style.color = 'white'; - this.currentDrawerInfo.style.padding = '8px'; - this.currentDrawerInfo.style.borderRadius = '4px'; - this.currentDrawerInfo.style.textAlign = 'center'; - - // Change button text for spectators - this.backToLobbyBtn.textContent = 'Arrêter de regarder'; - this.endRoundBtn.style.display = 'none'; // Hide end game button for spectators - } else { - this.backToLobbyBtn.textContent = 'Quitter la partie'; - this.endRoundBtn.style.display = 'inline-block'; - } - } - - resetGameUI() { - this.gameState.isPlaying = false; - this.gameState.currentWord = ''; - this.gameState.wordLength = 0; - this.gameState.revealedLetters = []; - this.gameState.revealedWord = []; - this.gameState.drawer = null; - this.isSpectating = false; - - this.gameState.scores = {}; - this.gameState.players = []; - this.gameState.currentPlayerIndex = 0; - this.gameState.guessedLetters = []; - - // Clear scores display - if (this.scoresDisplay) - this.scoresDisplay.textContent = ''; - - if (this.guessHistory) - this.guessHistory.innerHTML = ''; - - this.clearCanvas(); - - this.gameContainer.style.display = 'none'; - this.playerList.style.display = 'flex'; - this.lobbyButtons.style.display = 'flex'; - - this.wordInputContainer.style.display = 'none'; - this.guessContainer.style.display = 'none'; - this.drawTools.style.display = 'none'; - - // Reset spectator styling - this.currentDrawerInfo.style.backgroundColor = ''; - this.currentDrawerInfo.style.color = ''; - this.currentDrawerInfo.style.padding = ''; - this.currentDrawerInfo.style.borderRadius = ''; - this.currentDrawerInfo.style.textAlign = ''; - this.currentDrawerInfo.classList.remove('gameroom__drawer-info--winner'); - } - - async startGame() { - console.log('startGame called'); - - // Load player list - await this.loadLobby(); - - const playerElements = this.playerList.querySelectorAll('.gameroom__player-name'); - const players = Array.from(playerElements).map(el => el.textContent); - - console.log('Players found:', players); - - if (players.length < 2) { - this.showMessage('Il faut au moins 2 joueurs pour commencer', 'error'); - return; - } - - const drawer = players[0]; - - console.log('Socket connected:', this.socket?.connected, 'Socket ready:', this.socketReady); - - // Send start game event via WebSocket - if (this.socket?.connected) { - console.log('Emitting game-start event'); - this.socket.emit('game-start', { drawer, players }); - } else { - console.log('No socket, using local fallback'); - // Fallback local - start immediately - this.gameState.isPlaying = true; - this.gameState.players = players; - this.gameState.drawer = drawer; - this.gameState.currentPlayerIndex = 0; - this.showGameUI(); - this.setupRound(); - } - } - - setupRound() { - this.gameState.currentWord = ''; - this.gameState.wordLength = 0; - this.gameState.revealedLetters = []; - this.gameState.revealedWord = []; - this.gameState.guessedLetters = []; - - this.currentDrawerInfo.textContent = `C'est au tour de ${this.gameState.drawer} de dessiner`; - this.currentDrawerInfo.classList.remove('gameroom__drawer-info--winner'); - this.wordDisplay.textContent = ''; - this.guessHistory.innerHTML = ''; - this.clearCanvas(); - - // Spectators cannot interact - if (this.isSpectating) { - this.wordInputContainer.style.display = 'none'; - this.guessContainer.style.display = 'none'; - this.drawTools.style.display = 'none'; - this.currentDrawerInfo.textContent = '👁️ MODE SPECTATEUR - Vous regardez la partie'; - return; - } - - if (this.isCurrentUserDrawer()) { - // Drawer chooses a word - this.wordInputContainer.style.display = 'flex'; - this.guessContainer.style.display = 'none'; - this.drawTools.style.display = 'none'; - this.currentDrawerInfo.textContent = 'Choisissez un mot a faire deviner'; - } else { - // Others see the guess input (disabled while waiting for word) - this.wordInputContainer.style.display = 'none'; - this.guessContainer.style.display = 'flex'; - this.drawTools.style.display = 'none'; - this.letterInput.disabled = true; - this.guessBtn.disabled = true; - this.letterInput.placeholder = 'En attente du mot...'; - this.currentDrawerInfo.textContent = `${this.gameState.drawer} choisit un mot...`; - } - } - - confirmWord() { - const word = this.wordInput.value.trim().toLowerCase(); - if (!word || word.length < 2) { - this.showMessage('Le mot doit faire au moins 2 lettres', 'error'); - return; - } - - if (!/^[a-z]+$/.test(word)) { - this.showMessage('Le mot ne doit contenir que des lettres', 'error'); - return; - } - - this.gameState.currentWord = word; - this.gameState.wordLength = word.length; - this.gameState.revealedLetters = new Array(word.length).fill(false); - this.gameState.revealedWord = new Array(word.length).fill('_'); - - this.wordInput.value = ''; - this.wordInputContainer.style.display = 'none'; - this.drawTools.style.display = 'flex'; - - // Send word to server via WebSocket - if (this.socket?.connected) { - this.socket.emit('game-set-word', { word }); - } - - this.updateWordDisplay(); - this.currentDrawerInfo.textContent = `Dessinez pour faire deviner le mot (${word.length} lettres)`; - } - - updateWordDisplay() { - // If drawer, show from currentWord - if (this.isCurrentUserDrawer() && this.gameState.currentWord) { - let display = ''; - for (let i = 0; i < this.gameState.currentWord.length; i++) { - if (this.gameState.revealedLetters && this.gameState.revealedLetters[i]) { - display += this.gameState.currentWord[i] + ' '; - } else { - display += '_ '; - } - } - this.wordDisplay.textContent = display.trim(); - return; - } - - // For guessers, use revealedWord from server - if (this.gameState.revealedWord && this.gameState.revealedWord.length > 0) { - this.wordDisplay.textContent = this.gameState.revealedWord.join(' '); - return; - } - - // Fallback: show underscores based on wordLength - if (this.gameState.wordLength > 0) { - this.wordDisplay.textContent = '_ '.repeat(this.gameState.wordLength).trim(); - } - } - - makeGuess() { - const guess = this.letterInput.value.trim().toLowerCase(); - if (!guess) return; - - this.letterInput.value = ''; - - // Send guess via WebSocket - if (this.socket?.connected) { - this.socket.emit('game-guess', { guess }); - } else { - // Fallback local (for testing) - this.processGuessLocally(guess); - } - } - - processGuessLocally(guess) { - const username = this.getCurrentUsername(); - - if (guess.length > 1) { - const success = guess === this.gameState.currentWord; - this.addGuessToHistory(guess, success, 'word', username); - if (success) { - this.gameState.revealedWord = this.gameState.currentWord.split(''); - this.wordFound(this.gameState.currentWord, username); - } - return; - } - - if (this.gameState.guessedLetters.includes(guess)) { - this.showMessage('Lettre deja proposee', 'info'); - return; - } - - this.gameState.guessedLetters.push(guess); - - let found = false; - for (let i = 0; i < this.gameState.currentWord.length; i++) { - if (this.gameState.currentWord[i] === guess) { - this.gameState.revealedLetters[i] = true; - this.gameState.revealedWord[i] = guess; - found = true; - } - } - - this.addGuessToHistory(guess, found, 'letter', username); - this.updateWordDisplay(); - - if (this.gameState.revealedLetters.every(r => r)) { - this.wordFound(this.gameState.currentWord, username); - } - } - - addGuessToHistory(guess, success, type, username, points = 0) { - const item = this.createElement('div', 'gameroom__guess-item'); - item.classList.add(success ? 'gameroom__guess-item--success' : 'gameroom__guess-item--fail'); - - const typeText = type === 'letter' ? 'lettre' : 'mot'; - const pointsText = points !== 0 ? ` (${points > 0 ? '+' : ''}${points} pts)` : ''; - - if (success) { - item.textContent = `${username}: "${guess}" - Bon ${typeText}!${pointsText}`; - } else { - item.textContent = `${username}: "${guess}" - Mauvais ${typeText}${pointsText}`; - } - - this.guessHistory.appendChild(item); - this.guessHistory.scrollTop = this.guessHistory.scrollHeight; - } - - updateScoresDisplay(scores) { - if (!scores) return; - this.gameState.scores = scores; - - // Update scores display in game UI - if (this.scoresDisplay) { - const sortedScores = Object.entries(scores) - .sort((a, b) => b[1] - a[1]) - .map(([name, score]) => `${name}: ${score}`) - .join(' | '); - this.scoresDisplay.textContent = sortedScores; - } - - // Update player list with scores if visible - const playerItems = this.playerList.querySelectorAll('.gameroom__player'); - playerItems.forEach(item => { - const nameEl = item.querySelector('.gameroom__player-name'); - const scoreEl = item.querySelector('.gameroom__player-score'); - if (nameEl && scoreEl) { - const playerName = nameEl.textContent; - const score = scores[playerName] || 0; - scoreEl.textContent = `${score} pts`; - } - }); - } - - wordFound(word, winner, drawerBonus = 0) { - let message = `${winner} a trouve le mot: ${word}!`; - if (drawerBonus > 0 && this.gameState.drawer) { - message += ` (${this.gameState.drawer} +${drawerBonus} pts)`; - } - this.currentDrawerInfo.textContent = message; - this.currentDrawerInfo.classList.add('gameroom__drawer-info--winner'); - - this.guessContainer.style.display = 'none'; - this.drawTools.style.display = 'none'; - - // Reveal full word - this.wordDisplay.textContent = word.split('').join(' '); - - // Auto next round after delay - setTimeout(() => { - if (this.gameState.isPlaying) { - this.nextRound(); - } - }, 3000); - } - - nextRound() { - // Move to next player - this.gameState.currentPlayerIndex = (this.gameState.currentPlayerIndex + 1) % this.gameState.players.length; - const nextDrawer = this.gameState.players[this.gameState.currentPlayerIndex]; - - if (this.socket?.connected) { - this.socket.emit('game-next-round', { drawer: nextDrawer }); - } else { - this.gameState.drawer = nextDrawer; - this.setupRound(); - } - } - - backToLobby() { - if (this.socket?.connected) { - this.socket.emit('leave-room-during-game'); - } - - // Return to lobby without ending game for others - this.resetGameUI(); - this.exitLobby(); - this.showMessage('Vous avez quitté la partie', 'info'); - } - - endGame() { - if (this.socket?.connected) { - this.socket.emit('game-end'); - } - this.resetGameUI(); - this.showMessage('Jeu termine', 'info'); - - } -} diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/global_chat.js b/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/global_chat.js deleted file mode 100755 index 2936321..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/global_chat.js +++ /dev/null @@ -1,320 +0,0 @@ -import { Window } from './windows.js'; -import { STORAGE_KEYS, CSS } from './config.js'; -import { eventBus, Events } from './events.js'; - -/** - * Global chat window - * Uses Socket.IO for real-time communication - */ -export class GlobalChat extends Window { - constructor() { - super({ - name: 'chat', - title: 'Global Chat', - cssClasses: ['chat'] - }); - - this.socket = null; - this.connected = false; - this.friendIds = new Set(); - this.currentUserId = null; - this.currentUsername = null; - - this.buildUI(); - this.bindEvents(); - } - - /** - * Builds the user interface - */ - buildUI() { - // Message display area - this.output = this.createElement('div', CSS.CHAT_OUTPUT); - - // Input container - this.inputContainer = this.createElement('div', 'chat__input-container'); - - this.input = this.createElement('input', [CSS.INPUT, CSS.CHAT_INPUT], { - type: 'text', - placeholder: 'Type your message...' - }); - - this.sendBtn = this.createElement('button', [CSS.BTN, CSS.BTN_PRIMARY], { - text: 'Send' - }); - - this.inputContainer.append(this.input, this.sendBtn); - - // Connection controls - this.controls = this.createElement('div', CSS.CHAT_CONTROLS); - - this.connectBtn = this.createElement('button', [CSS.BTN, CSS.BTN_SUCCESS], { - text: 'Connect' - }); - - this.reconnectBtn = this.createElement('button', [CSS.BTN, CSS.BTN_PRIMARY], { - text: 'Reconnect' - }); - - this.controls.append(this.connectBtn, this.reconnectBtn); - - // Assembly - this.body.append(this.output, this.inputContainer, this.controls); - } - - /** - * Attaches event handlers - */ - bindEvents() { - this.sendBtn.addEventListener('click', () => this.sendMessage()); - this.connectBtn.addEventListener('click', () => this.connect()); - this.reconnectBtn.addEventListener('click', () => this.reconnect()); - - // Send with Enter - this.input.addEventListener('keypress', (e) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - this.sendMessage(); - } - }); - } - - /** - * Displays a system message - * @param {string} text - Message text - * @param {'info'|'error'|'success'} type - Message type - */ - addSystemMessage(text, type = 'info') { - const msg = this.createElement('div', CSS.CHAT_SYSTEM); - - if (type === 'error') { - msg.classList.add('chat__system--error'); - } else if (type === 'success') { - msg.classList.add('chat__system--success'); - } - - msg.textContent = text; - this.output.appendChild(msg); - this.scrollToBottom(); - } - - /** - * Displays a chat message - * @param {string} username - User name - * @param {string} content - Message content - * @param {boolean} isOwn - Is this the current user's message - * @param {boolean} isFriend - Is this user a friend - */ - addChatMessage(username, content, isOwn = false, isFriend = false) { - const msg = this.createElement('div', CSS.CHAT_MESSAGE); - - if (isOwn) { - msg.classList.add('chat__message--own'); - } - - const friendIndicator = isFriend ? '' : ''; - msg.innerHTML = `${friendIndicator}${this.escapeHtml(username)}: ${this.escapeHtml(content)}`; - this.output.appendChild(msg); - this.scrollToBottom(); - } - - /** - * Escapes HTML to prevent XSS - * @param {string} text - * @returns {string} - */ - escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; - } - - /** - * Scrolls the message area to the bottom - */ - scrollToBottom() { - this.output.scrollTop = this.output.scrollHeight; - } - - /** - * Sends a message - */ - sendMessage() { - const content = this.input.value.trim(); - if (!content) return; - - if (!this.socket?.connected) { - this.addSystemMessage('Error: you are not connected to the global chat', 'error'); - return; - } - - this.socket.emit('chat-message', { content }); - this.addChatMessage('Me', content, true); - this.input.value = ''; - } - - /** - * Reconnects to the server - */ - async reconnect() { - if (this.socket) { - try { - this.socket.close(); - } catch (e) { - // Ignore - } - this.socket = null; - } - - this.connected = false; - this.addSystemMessage('Reconnecting...'); - await this.connect(); - } - - decodeToken(token) - { - try - { - const payload = token.split('.')[1]; - return (JSON.parse(atob(payload))); - } - catch - { - return (null); - } - } - - /** - * Connects to the Socket.IO server - */ - async connect() { - const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); - - if (!token) { - this.addSystemMessage('Error: you must be logged in to use the global chat', 'error'); - return; - } - - const tokenData = this.decodeToken(token); - - if (tokenData) { - this.currentUserId = tokenData.id || tokenData.userId || tokenData.user_id || tokenData.sub || null; - this.currentUsername = tokenData.username || tokenData.name || null; - } - - if (this.socket?.connected) { - this.addSystemMessage('Already connected to global chat'); - return; - } - - // Load Socket.IO if needed - await this.loadSocketIO(); - - const ioConfig = { - auth: { token }, - reconnection: true, - reconnectionAttempts: 5, - reconnectionDelay: 1000, - transports: ['websocket', 'polling'] - }; - - // Optional alternative port - const altPort = window.GLOBAL_CHAT_ALT_PORT; - if (altPort) { - const host = location.hostname || 'localhost'; - this.socket = io(`http://${host}:${altPort}`, ioConfig); - } else { - this.socket = io(ioConfig); - } - - this.setupSocketListeners(); - } - - /** - * Loads the Socket.IO script if needed - */ - async loadSocketIO() { - if (window.io) return; - - return new Promise((resolve, reject) => { - const script = document.createElement('script'); - script.src = '/socket.io/socket.io.js'; - - script.onload = () => { - console.log('Socket.IO loaded'); - resolve(); - }; - - script.onerror = () => { - console.error('Failed to load Socket.IO'); - reject(new Error('Socket.IO load failed')); - }; - - document.head.appendChild(script); - }); - } - - /** - * Sets up Socket.IO listeners - */ - setupSocketListeners() { - this.socket.on('connect', () => { - console.log('Socket connected, ID:', this.socket.id); - this.connected = true; - this.output.innerHTML = ''; - this.addSystemMessage('Connected to global chat', 'success'); - eventBus.emit(Events.CHAT_CONNECTED, { socketId: this.socket.id }); - }); - - this.socket.on('connect_error', (err) => { - console.error('Socket connection error:', err.message); - this.addSystemMessage(`Connection error: ${err.message}`, 'error'); - }); - - this.socket.on('disconnect', (reason) => { - console.log('Socket disconnected:', reason); - this.connected = false; - this.addSystemMessage(`Disconnected (${reason})`); - eventBus.emit(Events.CHAT_DISCONNECTED, { reason }); - }); - - this.socket.on('chat-init', (data) => { - this.friendIds = new Set(data.friendIds || []); - - // Display recent messages - data.messages.forEach(msg => { - const isOwn = this.isOwnMessage(msg); - const isFriend = !isOwn && this.friendIds.has(msg.sender_id); - const displayUsername = isOwn ? 'Me' : msg.username; - this.addChatMessage(displayUsername, msg.content, isOwn, isFriend); - }); - }); - - this.socket.on('chat-message', (msg) => { - const isOwn = this.isOwnMessage(msg); - if (isOwn) - return; - - const isFriend = this.friendIds.has(msg.sender_id); - this.addChatMessage(msg.username, msg.content, false, isFriend); - eventBus.emit(Events.CHAT_MESSAGE_RECEIVED, msg); - }); - } - - isOwnMessage(msg) - { - if (this.currentUserId !== null && msg.sender_id !== undefined && msg.sender_id !== null) - { - if (String(this.currentUserId) === String(msg.sender_id)) - return (true); - } - - if (this.currentUsername && msg.username) - { - if (this.currentUsername.toLowerCase() === msg.username.toLowerCase()) - return (true); - } - - return (false); - } -} diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/index.css b/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/index.css deleted file mode 100755 index 3e260a9..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/index.css +++ /dev/null @@ -1,682 +0,0 @@ -/* ============================================ - TRANSCENDENCE - Main Stylesheet - Convention: BEM (Block__Element--Modifier) - ============================================ */ - -/* ============================================ - CSS VARIABLES - ============================================ */ -:root { - --color-primary: #0066cc; - --color-primary-hover: #0052a3; - --color-success: #3cff01; - --color-success-dark: #28a745; - --color-error: #ff4d4d; - --color-warning: #ffc107; - --color-github: #24292e; - - --color-bg: #a3a3a3; - - --app-background-base: radial-gradient( - circle at top, - #000000, - #4d4d4d - ); - - --app-background-image: url("./assets/background.png"); - - --color-surface: #222; - --color-surface-light: #333; - --color-text: #fff; - --color-text-muted: #aaa; - - --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; -} - -/* ============================================ - RESET & BASE - ============================================ */ -*, -*::before, -*::after { - box-sizing: border-box; -} - -html { - height: 100%; - background-image: - var(--app-background-image), - var(--app-background-base); - - background-size: - contain, - cover; - - background-position: - center, - center; - - background-repeat: - no-repeat, - no-repeat; -} - -body { - margin: 0; - width: 70%; - min-width: 800px; - margin: 0 auto; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - color: var(--color-text); - line-height: 1.5; -} - -/* ============================================ - TYPOGRAPHY - ============================================ */ - -.title { - position: absolute; - top: 20px; - left: 50%; - transform: translateX(-50%); - text-transform: uppercase; - - display: flex; - align-items: center; - justify-content: center; - gap: 20px; - - font-size: var(--font-size-xl); - text-align: center; - text-shadow: 2px 2px 10px black; - z-index: 1; - font-family: "Cinzel Decorative", cursive; - color: rgba(248, 252, 2, 0.6); - - margin: 0; - padding: var(--spacing-md); - - /* Rectangle + rounded corners */ - background-color: rgba(247, 7, 67, 0.6); - border: 2px solid rgba(0, 0, 0, 0.6); - border-radius: 15px; -} - - -/* ============================================ - MENU - ============================================ */ - -.menu { - position: fixed; - top: 0; - left: 50px; - padding: 0; - margin: 0; - z-index: var(--z-menu); - display: flex; - flex-direction: column; - gap: var(--spacing-xs); -} - -.menu__item { - background: var(--color-surface); - color: var(--color-text); - border: 1px solid var(--color-surface-light); - padding: var(--spacing-sm) var(--spacing-md); - font-size: var(--font-size-md); - cursor: pointer; - transition: all var(--transition-fast); - text-align: left; -} - -.menu__item:hover { - background: var(--color-surface-light); - font-size: var(--font-size-lg); -} - -.menu__item--active { - background: var(--color-primary); - border-color: var(--color-primary); -} - -/* ============================================ - GAME - ============================================ */ - -.game { - position: fixed; - top: 0; - right: 50px; - padding: 0; - margin: 0; - z-index: var(--z-menu); - display: flex; - flex-direction: column; - gap: var(--spacing-xs); -} - -.game__item { - background: var(--color-surface); - color: var(--color-text); - border: 1px solid var(--color-surface-light); - padding: var(--spacing-sm) var(--spacing-md); - font-size: var(--font-size-md); - cursor: pointer; - transition: all var(--transition-fast); - text-align: right; -} - -.game__item:hover { - background: var(--color-surface-light); - font-size: var(--font-size-lg); -} - -.game__item--active { - background: var(--color-primary); - border-color: var(--color-primary); -} - -/* ============================================ - BUTTONS - ============================================ */ -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - padding: var(--spacing-sm) var(--spacing-md); - font-size: var(--font-size-md); - font-weight: 500; - border: none; - border-radius: var(--radius-md); - cursor: pointer; - transition: all var(--transition-fast); - text-decoration: none; -} - -.btn:hover { - opacity: 0.9; - transform: translateY(-1px); -} - -.btn:active { - transform: translateY(0); -} - -.btn--primary { - background: var(--color-primary); - color: var(--color-text); -} - -.btn--primary:hover { - background: var(--color-primary-hover); -} - -.btn--secondary { - background: var(--color-surface-light); - color: var(--color-text); -} - -.btn--success { - background: var(--color-success-dark); - color: var(--color-text); -} - -.btn--danger { - background: var(--color-error); - color: var(--color-text); -} - -.btn--github { - background: var(--color-github); - color: var(--color-text); -} - -.btn--ghost { - background: transparent; - color: var(--color-text); - border: 1px solid var(--color-surface-light); -} - -/* ============================================ - INPUTS - ============================================ */ -.input { - width: 100%; - padding: var(--spacing-sm) var(--spacing-md); - font-size: var(--font-size-md); - background: var(--color-surface); - color: var(--color-text); - border: 1px solid var(--color-surface-light); - border-radius: var(--radius-md); - transition: border-color var(--transition-fast); -} - -.input:focus { - outline: none; - border-color: var(--color-primary); -} - -.input::placeholder { - color: var(--color-text-muted); -} - -.input-group { - display: flex; - flex-direction: column; - gap: var(--spacing-sm); -} - -/* ============================================ - WINDOWS - ============================================ */ -.window { - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background: var(--color-bg); - border: 2px ridge var(--color-text); - color: var(--color-text); - z-index: var(--z-window); - display: none; - flex-direction: column; - min-width: 280px; - box-shadow: var(--shadow-lg); -} - -.window--visible { - display: flex; -} - -.window--left { - left: 25%; -} - -.window--right { - left: 75%; -} - -.window__header { - display: flex; - justify-content: space-between; - align-items: center; - padding: var(--spacing-sm) var(--spacing-md); - background: var(--color-surface); - cursor: move; - user-select: none; -} - -.window__title { - font-weight: 500; - font-size: var(--font-size-md); -} - -.window__close { - cursor: pointer; - font-size: var(--font-size-lg); - opacity: 0.8; - transition: opacity var(--transition-fast); - background: none; - border: none; - color: var(--color-text); - padding: 0; - line-height: 1; -} - -.window__close:hover { - opacity: 1; -} - -.window__body { - padding: var(--spacing-md); - display: flex; - flex-direction: column; - gap: var(--spacing-sm); - flex: 1; - overflow: auto; -} - -/* ============================================ - MESSAGES - ============================================ */ -.message { - font-size: var(--font-size-sm); - padding: var(--spacing-xs); - border-radius: var(--radius-sm); -} - -.message--success { - color: var(--color-success); -} - -.message--error { - color: var(--color-error); -} - -.message--info { - color: var(--color-text-muted); -} - -/* ============================================ - LOGIN WINDOW - ============================================ */ -.login { - width: 320px; -} - -.login__form { - display: flex; - flex-direction: column; - gap: var(--spacing-sm); -} - -.login__actions { - display: flex; - gap: var(--spacing-sm); - margin-top: var(--spacing-xs); -} - -.login__divider { - display: flex; - align-items: center; - gap: var(--spacing-sm); - color: var(--color-text-muted); - font-size: var(--font-size-sm); - margin: var(--spacing-sm) 0; -} - -.login__divider::before, -.login__divider::after { - content: ''; - flex: 1; - height: 1px; - background: var(--color-surface-light); -} - -/* ============================================ - CHAT WINDOW - ============================================ */ -.chat { - width: 380px; - height: 400px; -} - -.chat__output { - flex: 1; - overflow-y: auto; - padding: var(--spacing-sm); - background: var(--color-surface); - border-radius: var(--radius-md); - display: flex; - flex-direction: column; - gap: var(--spacing-sm); - min-height: 150px; -} - -.chat__message { - padding: var(--spacing-xs) var(--spacing-sm); - background: var(--color-surface-light); - border-radius: var(--radius-sm); - font-size: var(--font-size-sm); -} - -.chat__message--own { - background: var(--color-primary); - align-self: flex-end; -} - -.chat__friend-indicator { - display: inline-block; - width: 8px; - height: 8px; - background-color: var(--color-success); - border-radius: 50%; - margin-right: var(--spacing-xs); - vertical-align: middle; -} - -.chat__system { - color: var(--color-text-muted); - font-size: var(--font-size-sm); - font-style: italic; - text-align: center; -} - -.chat__system--error { - color: var(--color-error); -} - -.chat__system--success { - color: var(--color-success); -} - -.chat__input-container { - display: flex; - gap: var(--spacing-sm); - margin-top: var(--spacing-sm); -} - -.chat__input { - flex: 1; -} - -.chat__controls { - display: flex; - gap: var(--spacing-sm); - margin-top: var(--spacing-sm); -} - -/* ============================================ - AVATAR WINDOW - ============================================ */ -.avatar-window { - width: 360px; -} - -.avatar__preview { - width: 120px; - height: 120px; - object-fit: cover; - border-radius: var(--radius-full); - border: 3px solid var(--color-text); - box-shadow: var(--shadow-md); - background: var(--color-surface); - align-self: center; -} - -.avatar__username { - font-size: var(--font-size-lg); - font-weight: 600; - text-align: center; - color: var(--color-text); - margin-top: var(--spacing-sm); -} - -.avatar__controls { - display: flex; - flex-direction: column; - gap: var(--spacing-sm); - align-items: center; -} - -.avatar__file-input { - display: none; -} - -/* ============================================ - EASTER EGG BUTTON - ============================================ */ -/* .easter-egg { - position: absolute; - top: 20%; - left: 50%; - transform: translateX(-50%); - z-index: 1; - background: var(--color-surface); - color: var(--color-text); - border: 1px solid var(--color-surface-light); - padding: var(--spacing-sm) var(--spacing-md); - cursor: pointer; - font-size: var(--font-size-md); - border-radius: var(--radius-md); - transition: all var(--transition-fast); -} - -.easter-egg:hover { - background: var(--color-error); - border-color: var(--color-error); -} */ - -/* ============================================ - UTILITIES - ============================================ */ -.hidden { - display: none !important; -} - -.visually-hidden { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - border: 0; -} - -.text-center { - text-align: center; -} - -.flex-center { - display: flex; - align-items: center; - justify-content: center; -} - -/* ============================================ - FRIENDS WINDOW - ============================================ */ -.friends-window { - width: 400px; - height: 450px; -} - -.friends__tabs { - display: flex; - gap: var(--spacing-xs); - margin-bottom: var(--spacing-sm); -} - -.friends__tab { - flex: 1; - padding: var(--spacing-sm); - background: var(--color-surface); - border: 1px solid var(--color-surface-light); - color: var(--color-text); - cursor: pointer; - font-size: var(--font-size-sm); - transition: all var(--transition-fast); -} - -.friends__tab:hover { - background: var(--color-surface-light); -} - -.friends__tab--active { - background: var(--color-primary); - border-color: var(--color-primary); -} - -.friends__content { - display: flex; - flex-direction: column; - flex: 1; - overflow: hidden; -} - -.friends__search { - display: flex; - gap: var(--spacing-sm); - margin-bottom: var(--spacing-sm); -} - -.friends__search .input { - flex: 1; -} - -.friends__list { - flex: 1; - overflow-y: auto; - display: flex; - flex-direction: column; - gap: var(--spacing-sm); -} - -.friends__item { - display: flex; - align-items: center; - gap: var(--spacing-sm); - padding: var(--spacing-sm); - background: var(--color-surface); - border-radius: var(--radius-md); -} - -.friends__avatar { - width: 40px; - height: 40px; - border-radius: var(--radius-full); - object-fit: cover; - border: 2px solid var(--color-surface-light); -} - -.friends__name { - flex: 1; - font-size: var(--font-size-md); - font-weight: 500; -} - -.friends__actions { - display: flex; - gap: var(--spacing-xs); -} - -.friends__actions .btn { - padding: var(--spacing-xs) var(--spacing-sm); - font-size: var(--font-size-sm); -} - -.friends__empty { - text-align: center; - color: var(--color-text-muted); - padding: var(--spacing-lg); -} diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/index.html b/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/index.html deleted file mode 100755 index ee45125..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/index.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - Transcendence.io - - - - - - -

Transcendence.io

- - - - - - - - diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/login.js b/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/login.js deleted file mode 100755 index f83dfec..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/login.js +++ /dev/null @@ -1,239 +0,0 @@ -import { Window } from './windows.js'; -import { API, STORAGE_KEYS, CSS } from './config.js'; -import { eventBus, Events } from './events.js'; - -/** - * Login and registration window - * Emits events instead of directly importing other windows - */ -export class LoginWindow extends Window { - constructor() { - super({ - name: 'login', - title: 'Login', - cssClasses: ['login'] - }); - - this.buildUI(); - this.bindEvents(); - this.checkIfAlreadyLoggedIn(); - } - - /** - * Builds the user interface - */ - buildUI() { - // Main form - this.form = this.createElement('div', 'login__form'); - - // Username field - this.usernameInput = this.createElement('input', CSS.INPUT, { - type: 'text', - placeholder: 'Username' - }); - - // Password field - this.passwordInput = this.createElement('input', CSS.INPUT, { - type: 'password', - placeholder: 'Password' - }); - - // Action buttons - this.actions = this.createElement('div', 'login__actions'); - - this.loginBtn = this.createElement('button', [CSS.BTN, CSS.BTN_PRIMARY], { - text: 'Sign in' - }); - - this.registerBtn = this.createElement('button', [CSS.BTN, CSS.BTN_SECONDARY], { - text: 'Register' - }); - - this.actions.append(this.loginBtn, this.registerBtn); - - // Feedback message - this.message = this.createElement('div', CSS.MESSAGE); - - // Divider - this.divider = this.createElement('div', 'login__divider', { - text: 'or' - }); - - // GitHub button - this.githubBtn = this.createElement('button', [CSS.BTN, CSS.BTN_GITHUB], { - text: 'Sign in with GitHub' - }); - - // Assembly - this.form.append( - this.usernameInput, - this.passwordInput, - this.actions, - this.message, - this.divider, - this.githubBtn - ); - - this.body.appendChild(this.form); - } - - /** - * Attaches event handlers - */ - bindEvents() { - this.loginBtn.addEventListener('click', () => this.handleLogin()); - this.registerBtn.addEventListener('click', () => this.handleRegister()); - this.githubBtn.addEventListener('click', () => this.handleGitHubLogin()); - - // Login with Enter - this.passwordInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter') { - this.handleLogin(); - } - }); - } - - /** - * Checks if user is already logged in - */ - checkIfAlreadyLoggedIn() { - const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); - if (token) { - this.showMessage('You are already logged in!', 'success'); - } - } - - /** - * Handles login - */ - async handleLogin() { - const username = this.usernameInput.value.trim(); - const password = this.passwordInput.value; - - if (!username || !password) { - this.showMessage('Please fill in all fields', 'error'); - return; - } - - this.showMessage('Signing in...', 'info'); - - try { - const response = await fetch(API.AUTH.LOGIN, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ username, password }) - }); - - const data = await response.json(); - - if (response.ok && data.token) { - console.log('Login successful, storing token'); - localStorage.setItem(STORAGE_KEYS.AUTH_TOKEN, data.token); - this.showMessage('Login successful! Welcome.', 'success'); - - console.log('Emitting USER_LOGGED_IN event'); - // Emit login event - eventBus.emit(Events.USER_LOGGED_IN, { username, token: data.token }); - - console.log('Token stored:', !!localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN)); - - // Close window after delay - setTimeout(() => this.hide(), 1500); - } else { - const errorMsg = data?.message || 'Login failed'; - this.showMessage(errorMsg, 'error'); - } - } catch (error) { - console.error('Login error:', error); - this.showMessage('Server connection error', 'error'); - } - } - - /** - * Handles registration - */ - async handleRegister() { - const username = this.usernameInput.value.trim(); - const password = this.passwordInput.value; - - if (!username || !password) { - this.showMessage('Please fill in all fields', 'error'); - return; - } - - this.showMessage('Registering...', 'info'); - - try { - const response = await fetch(API.AUTH.REGISTER, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ username, password }) - }); - - const data = await response.json(); - - if (response.ok) { - this.showMessage('Registration successful! You can now sign in.', 'success'); - eventBus.emit(Events.USER_REGISTERED, { username }); - } else { - const errorMsg = data?.message || 'Registration failed'; - this.showMessage(errorMsg, 'error'); - } - } catch (error) { - console.error('Registration error:', error); - this.showMessage('Server connection error', 'error'); - } - } - - /** - * Handles GitHub OAuth login - */ - handleGitHubLogin() { - const width = 600; - const height = 700; - const left = (screen.width - width) / 2; - const top = (screen.height - height) / 2; - - const popup = window.open( - API.AUTH.GITHUB, - 'githubOAuth', - `width=${width},height=${height},left=${left},top=${top}` - ); - - const handleMessage = (event) => { - if (event.data?.token) { - localStorage.setItem(STORAGE_KEYS.AUTH_TOKEN, event.data.token); - this.showMessage('GitHub login successful! Welcome.', 'success'); - - // Emit login event - eventBus.emit(Events.USER_LOGGED_IN, { - provider: 'github', - token: event.data.token - }); - - window.removeEventListener('message', handleMessage); - if (popup) popup.close(); - } - }; - - window.addEventListener('message', handleMessage, { once: true }); - } - - /** - * Displays a feedback message - * @param {string} text - Message text - * @param {'success'|'error'|'info'} type - Message type - */ - showMessage(text, type = 'info') { - this.message.textContent = text; - this.message.className = CSS.MESSAGE; - - if (type === 'success') { - this.message.classList.add(CSS.MESSAGE_SUCCESS); - } else if (type === 'error') { - this.message.classList.add(CSS.MESSAGE_ERROR); - } else { - this.message.classList.add(CSS.MESSAGE_INFO); - } - } -} diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/pieces.js b/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/pieces.js deleted file mode 100755 index 9af9cad..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/pieces.js +++ /dev/null @@ -1,99 +0,0 @@ -// ───────────────────────────────────────────── -// PIÈCES -// ───────────────────────────────────────────── - -class Piece { - constructor(startX, startY) { - this.position = { x: startX, y: startY }; - this.currentRotation = 0; - this.rotations = this.defineRotations(); - this.shape = this.rotations[0]; - this.color = this.getColor(); - } - defineRotations() { return [[[1]]]; } - getColor() { return 1; } - getPosition() { return { ...this.position }; } - getShape() { return this.shape; } - moveDown() { this.position.y++; } - moveLeft() { this.position.x--; } - moveRight() { this.position.x++; } - rotateLeft() { - this.currentRotation = (this.currentRotation - 1 + this.rotations.length) % this.rotations.length; - this.shape = this.rotations[this.currentRotation]; - } - rotateRight() { - this.currentRotation = (this.currentRotation + 1) % this.rotations.length; - this.shape = this.rotations[this.currentRotation]; - } -} - -class PieceT extends Piece { - defineRotations() { - return [ - [[0,1,0],[1,1,1],[0,0,0]], - [[0,1,0],[0,1,1],[0,1,0]], - [[0,0,0],[1,1,1],[0,1,0]], - [[0,1,0],[1,1,0],[0,1,0]] - ]; - } - getColor() { return 1; } -} - -class PieceL extends Piece { - defineRotations() { - return [ - [[0,0,1],[1,1,1],[0,0,0]], - [[0,1,0],[0,1,0],[0,1,1]], - [[0,0,0],[1,1,1],[1,0,0]], - [[1,1,0],[0,1,0],[0,1,0]] - ]; - } - getColor() { return 2; } -} - -class PieceReverseL extends Piece { - defineRotations() { - return [ - [[1,0,0],[1,1,1],[0,0,0]], - [[0,1,1],[0,1,0],[0,1,0]], - [[0,0,0],[1,1,1],[0,0,1]], - [[0,1,0],[0,1,0],[1,1,0]] - ]; - } - getColor() { return 3; } -} - -class PieceI extends Piece { - defineRotations() { - return [ - [[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]], - [[0,0,1,0],[0,0,1,0],[0,0,1,0],[0,0,1,0]] - ]; - } - getColor() { return 4; } -} - -class PieceZ extends Piece { - defineRotations() { - return [ - [[1,1,0],[0,1,1],[0,0,0]], - [[0,0,1],[0,1,1],[0,1,0]] - ]; - } - getColor() { return 5; } -} - -class PieceReverseZ extends Piece { - defineRotations() { - return [ - [[0,1,1],[1,1,0],[0,0,0]], - [[0,1,0],[0,1,1],[0,0,1]] - ]; - } - getColor() { return 6; } -} - -class PieceO extends Piece { - defineRotations() { return [[[1,1],[1,1]]]; } - getColor() { return 7; } -} diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/renderer.js b/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/renderer.js deleted file mode 100755 index f024216..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/renderer.js +++ /dev/null @@ -1,126 +0,0 @@ -// ───────────────────────────────────────────── -// RENDU -// ───────────────────────────────────────────── - -const CELL = 30; -const COLORS = ['#070712','#a855f7','#f97316','#3b82f6','#06b6d4','#ef4444','#22c55e','#eab308','#555577']; - -const ctxMain = document.getElementById('canvas-main').getContext('2d'); -const ctxNext = document.getElementById('canvas-next').getContext('2d'); -const ctxHold = document.getElementById('canvas-hold').getContext('2d'); -const ctxOpponent = document.getElementById('canvas-opponent').getContext('2d'); - -function drawCell(ctx, x, y, colorIndex, size) { - const p = 1; - ctx.fillStyle = COLORS[colorIndex]; - ctx.fillRect(x * size + p, y * size + p, size - p * 2, size - p * 2); - // Highlight - ctx.fillStyle = 'rgba(255,255,255,0.25)'; - ctx.fillRect(x * size + p, y * size + p, size - p * 2, 3); - ctx.fillRect(x * size + p, y * size + p, 3, size - p * 2); - // Ombre - ctx.fillStyle = 'rgba(0,0,0,0.35)'; - ctx.fillRect(x * size + p, (y + 1) * size - p - 3, size - p * 2, 3); - ctx.fillRect((x + 1) * size - p - 3, y * size + p, 3, size - p * 2); -} - -function clearCanvas(ctx, w, h) { - ctx.fillStyle = '#070712'; - ctx.fillRect(0, 0, w, h); -} - -function drawGridLines(ctx, cols, rows, size) { - ctx.strokeStyle = 'rgba(255,255,255,0.04)'; - ctx.lineWidth = 1; - for (let x = 0; x <= cols; x++) { - ctx.beginPath(); ctx.moveTo(x * size, 0); ctx.lineTo(x * size, rows * size); ctx.stroke(); - } - for (let y = 0; y <= rows; y++) { - ctx.beginPath(); ctx.moveTo(0, y * size); ctx.lineTo(cols * size, y * size); ctx.stroke(); - } -} - -function drawGhost(ctx, piece, grid) { - if (!piece) return; - const ghost = { x: piece.getPosition().x, y: piece.getPosition().y }; - const shape = piece.getShape(); - - while (true) { - ghost.y++; - let valid = true; - for (let row = 0; row < shape.length && valid; row++) - for (let col = 0; col < shape[row].length && valid; col++) - if (shape[row][col] !== 0) { - const ny = ghost.y + row; - const nx = ghost.x + col; - if (ny >= grid.length || grid[ny][nx] !== 0) valid = false; - } - if (!valid) { ghost.y--; break; } - } - - if (ghost.y === piece.getPosition().y) return; - - ctx.strokeStyle = 'rgba(255,255,255,0.15)'; - ctx.lineWidth = 1; - for (let row = 0; row < shape.length; row++) - for (let col = 0; col < shape[row].length; col++) - if (shape[row][col] !== 0) - ctx.strokeRect( - (ghost.x + col) * CELL + 2, - (ghost.y + row) * CELL + 2, - CELL - 4, CELL - 4 - ); -} - -function drawMiniPiece(ctx, piece, canvasW, canvasH) { - clearCanvas(ctx, canvasW, canvasH); - if (!piece) return; - const shape = piece.getShape(); - const color = piece.getColor(); - const s = 20; - const offsetX = Math.floor((canvasW / s - shape[0].length) / 2); - const offsetY = Math.floor((canvasH / s - shape.length) / 2); - for (let row = 0; row < shape.length; row++) - for (let col = 0; col < shape[row].length; col++) - if (shape[row][col] !== 0) - drawCell(ctx, offsetX + col, offsetY + row, color, s); -} - -function render() { - // Grille principale - clearCanvas(ctxMain, 300, 600); - drawGridLines(ctxMain, 10, 20, CELL); - - for (let y = 0; y < game.grid.length; y++) - for (let x = 0; x < game.grid[y].length; x++) - if (game.grid[y][x] !== 0) - drawCell(ctxMain, x, y, game.grid[y][x], CELL); - - // Ghost + pièce courante - if (game.currentPiece) { - drawGhost(ctxMain, game.currentPiece, game.grid); - const { x, y } = game.currentPiece.getPosition(); - const shape = game.currentPiece.getShape(); - const color = game.currentPiece.getColor(); - for (let row = 0; row < shape.length; row++) - for (let col = 0; col < shape[row].length; col++) - if (shape[row][col] !== 0) - drawCell(ctxMain, x + col, y + row, color, CELL); - } - - // Panneaux miniatures - drawMiniPiece(ctxNext, game.nextPiece, 100, 80); - drawMiniPiece(ctxHold, game.storedPiece, 100, 80); - - // Score - document.getElementById('score-display').textContent = game.score; -} - -function renderOpponent(opponentGrid) { - clearCanvas(ctxOpponent, 300, 600); - drawGridLines(ctxOpponent, 10, 20, CELL); - for (let y = 0; y < opponentGrid.length; y++) - for (let x = 0; x < opponentGrid[y].length; x++) - if (opponentGrid[y][x] !== 0) - drawCell(ctxOpponent, x, y, opponentGrid[y][x], CELL); -} diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/tetris.css b/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/tetris.css deleted file mode 100755 index 9d29537..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/tetris.css +++ /dev/null @@ -1,355 +0,0 @@ -:root { - --bg: #070712; - --panel: #0d0d1f; - --border: #1a1a3e; - --accent: #00ffe7; - --accent2:#ff00aa; - --dim: #3a3a6a; - --text: #c0c0e0; -} - -* { margin: 0; padding: 0; box-sizing: border-box; } - -body { - background: var(--bg); - font-family: 'Share Tech Mono', monospace; - color: var(--text); - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - overflow: hidden; -} - -body::before { - content: ''; - position: fixed; - inset: 0; - background-image: - linear-gradient(rgba(0,255,231,0.03) 1px, transparent 1px), - linear-gradient(90deg, rgba(0,255,231,0.03) 1px, transparent 1px); - background-size: 40px 40px; - pointer-events: none; - z-index: 0; -} - -h1 { - font-family: 'Orbitron', monospace; - font-weight: 900; - font-size: 2.2rem; - letter-spacing: 0.4em; - color: var(--accent); - text-shadow: 0 0 20px var(--accent), 0 0 40px var(--accent); - margin-bottom: 20px; - position: relative; - z-index: 1; -} - -/* ── Zone de jeu globale ── */ -#game-area { - display: flex; - gap: 32px; - align-items: flex-start; - position: relative; - z-index: 1; -} - -/* ── Section locale (légèrement décalée à gauche par le flex naturel) ── */ -#local-section { - display: flex; - flex-direction: column; - align-items: center; -} - -#app { - display: flex; - gap: 16px; - align-items: flex-start; -} - -/* ── Section adversaire ── */ -#opponent-section { - display: none; /* masqué jusqu'à connexion duel */ - gap: 16px; - align-items: flex-start; -} -#opponent-section.visible { - display: flex; -} - -.opponent-info-panel { - width: 130px; -} - -/* ── Panneaux ── */ -.panel { - background: var(--panel); - border: 1px solid var(--border); - border-radius: 6px; - padding: 14px; - width: 130px; - box-shadow: 0 0 20px rgba(0,255,231,0.05); -} - -.panel-title { - font-family: 'Orbitron', monospace; - font-size: 0.6rem; - letter-spacing: 0.2em; - color: var(--accent); - text-transform: uppercase; - margin-bottom: 10px; - text-align: center; -} - -canvas { display: block; border-radius: 4px; } - -#canvas-main { - border: 1px solid var(--border); - box-shadow: 0 0 30px rgba(0,255,231,0.08), inset 0 0 30px rgba(0,0,0,0.5); -} - -#canvas-next, #canvas-hold { - border: 1px solid var(--border); - margin: 0 auto; -} - -/* ── Canvas adversaire ── */ -#canvas-opponent { - border: 1px solid var(--accent2); - box-shadow: 0 0 30px rgba(255,0,170,0.08), inset 0 0 30px rgba(0,0,0,0.5); -} - -/* ── Score ── */ -.score-block { - margin-top: 14px; - text-align: center; -} - -.score-label { - font-size: 0.55rem; - letter-spacing: 0.2em; - color: var(--dim); - text-transform: uppercase; - margin-bottom: 4px; -} - -.score-value { - font-family: 'Orbitron', monospace; - font-size: 1.4rem; - font-weight: 700; - color: var(--accent); - text-shadow: 0 0 10px var(--accent); -} - -/* ── Boutons ── */ -.btn-group { - display: flex; - flex-direction: column; - gap: 8px; - margin-top: 14px; -} - -button { - font-family: 'Orbitron', monospace; - font-size: 0.55rem; - letter-spacing: 0.15em; - font-weight: 700; - text-transform: uppercase; - padding: 10px 8px; - border: 1px solid; - border-radius: 4px; - cursor: pointer; - transition: all 0.2s; - background: transparent; - width: 100%; -} - -#btn-start { color: var(--accent); border-color: var(--accent); } -#btn-start:hover:not(:disabled) { background: var(--accent); color: var(--bg); box-shadow: 0 0 15px var(--accent); } - -#btn-pause { color: var(--accent2); border-color: var(--accent2); } -#btn-pause:hover:not(:disabled) { background: var(--accent2); color: var(--bg); box-shadow: 0 0 15px var(--accent2); } - -#btn-stop { color: #ef4444; border-color: #ef4444; } -#btn-stop:hover:not(:disabled) { background: #ef4444; color: var(--bg); box-shadow: 0 0 15px #ef4444; } - -button:disabled { opacity: 0.3; cursor: not-allowed; } - -/* ── Contrôles ── */ -.controls-list { - margin-top: 14px; - font-size: 0.6rem; - line-height: 2; - color: var(--dim); -} -.controls-list span { color: var(--text); } - -/* ── Overlays ── */ -#main-wrapper, -#opponent-wrapper { position: relative; } - -#overlay, -#overlay-opponent { - display: none; - position: absolute; - top: 0; left: 0; - width: 300px; - height: 600px; - background: rgba(7,7,18,0.88); - border-radius: 4px; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 12px; - z-index: 10; - pointer-events: none; -} -#overlay.visible, -#overlay-opponent.visible { display: flex; } - -#overlay-title { - font-family: 'Orbitron', monospace; - font-size: 1.4rem; - font-weight: 900; - letter-spacing: 0.2em; - color: var(--accent2); - text-shadow: 0 0 20px var(--accent2); -} - -#overlay-score { - font-family: 'Orbitron', monospace; - font-size: 0.9rem; - color: var(--accent); -} - -#overlay-opponent-title { - font-family: 'Orbitron', monospace; - font-size: 1.4rem; - font-weight: 900; - letter-spacing: 0.2em; - color: var(--accent); - text-shadow: 0 0 20px var(--accent); -} - -#overlay-opponent-score { - font-family: 'Orbitron', monospace; - font-size: 0.9rem; - color: var(--accent2); -} - -/* ── Panneau duel ── */ -#duel-panel { - background: var(--panel); - border: 1px solid var(--border); - border-radius: 6px; - padding: 12px 20px; - margin-bottom: 14px; - position: relative; - z-index: 1; - display: flex; - align-items: center; - gap: 14px; - box-shadow: 0 0 20px rgba(255,0,170,0.04); -} - -.duel-row { - display: flex; - gap: 8px; - align-items: center; -} - -#input-room-code { - background: var(--bg); - border: 1px solid var(--border); - border-radius: 4px; - color: var(--accent2); - font-family: 'Orbitron', monospace; - font-size: 0.7rem; - letter-spacing: 0.15em; - padding: 6px 10px; - width: 120px; - text-transform: uppercase; - outline: none; - transition: border-color 0.2s; -} -#input-room-code:focus { - border-color: var(--accent2); - box-shadow: 0 0 8px rgba(255,0,170,0.2); -} - -#btn-join-duel { color: var(--accent2); border-color: var(--accent2); width: auto; padding: 6px 14px; } -#btn-join-duel:hover:not(:disabled) { background: var(--accent2); color: var(--bg); box-shadow: 0 0 12px var(--accent2); } - -#btn-leave-duel { color: #ef4444; border-color: #ef4444; width: auto; padding: 6px 14px; } -#btn-leave-duel:hover:not(:disabled) { background: #ef4444; color: var(--bg); box-shadow: 0 0 12px #ef4444; } - -#duel-status { - font-size: 0.6rem; - letter-spacing: 0.1em; - color: var(--dim); - min-width: 120px; -} -#duel-status.waiting { color: #f97316; } -#duel-status.ready { color: var(--accent); } - -/* ── Settings Panel ── */ -#settings-panel { - background: var(--panel); - border: 1px solid var(--border); - border-radius: 6px; - padding: 14px 20px; - margin-top: -250px; - margin-left: -600px; - box-shadow: 0 0 20px rgba(0,255,231,0.05); - position: relative; - z-index: 1; - display: flex; - flex-direction: column; - gap: 10px; - width: fit-content; -} - -.settings-title { - font-family: 'Orbitron', monospace; - font-size: 0.6rem; - letter-spacing: 0.2em; - color: var(--accent); - text-transform: uppercase; - text-align: center; - margin-bottom: 4px; -} - -.settings-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - font-size: 0.6rem; - color: var(--dim); - letter-spacing: 0.05em; -} - -#settings-panel input[type="number"] { - background: var(--bg); - border: 1px solid var(--border); - border-radius: 4px; - color: var(--accent); - font-family: 'Orbitron', monospace; - font-size: 0.65rem; - padding: 4px 8px; - width: 80px; - text-align: right; - outline: none; - transition: border-color 0.2s; -} - -#settings-panel input[type="number"]:focus { - border-color: var(--accent); - box-shadow: 0 0 8px rgba(0,255,231,0.2); -} - -#settings-panel input[type="number"]:disabled { - opacity: 0.3; - cursor: not-allowed; -} diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/tetris.html b/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/tetris.html deleted file mode 100755 index ea0a17c..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/tetris.html +++ /dev/null @@ -1,121 +0,0 @@ - - - - - - TETRIS - - - - - -

TETRIS

- - -
- Duel -
- - - -
-
-
- -
- - -
-
- - -
-
Hold
- - -
-
Score
-
0
-
- -
- - - -
-
- - -
- -
-
GAME OVER
-
-
-
- - -
-
Next
- - -
-
← → Déplacer
-
Descendre
-
Q Rot. gauche
-
W Rot. droite
-
Espace Drop
-
C Hold
-
-
- -
-
- - -
-
-
Adversaire
-
-
Score
-
-
-
- -
- -
-
-
-
-
-
- -
- - -
-
Paramètres
-
- - -
-
- - -
-
- - -
-
- - - - - - - - - - diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/tetris.js b/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/tetris.js deleted file mode 100755 index 6032065..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/tetris.js +++ /dev/null @@ -1,366 +0,0 @@ -// ───────────────────────────────────────────── -// LOGIQUE TETRIS -// ───────────────────────────────────────────── - -class Tetris { - constructor(onRender, onGameOver, onBlockPlaced = null, onLinesCleared = null) { - this.onRender = onRender; - this.onGameOver = onGameOver; - this.onBlockPlaced = onBlockPlaced; - this.onLinesCleared = onLinesCleared; - - this.grid = this._createGrid(10, 20); - this.bufferGrid = this._createGrid(10, 5); - this.currentPiece = null; - this.storedPiece = null; - this.nextPiece = null; - - this.score = 0; - this.initialTimeToDown = 1000; - this.timeToDown = 1000; - this.hardening = 1000; - this.count = 0; - this.decrementTTD = 100; - - this.lastLandingCol = 4; - - this.isRunning = false; - this.isPaused = false; - this.canStore = true; - - this.animationFrameId = null; - this.lastTime = 0; - this.accumulator = 0; - - this._keyHandler = this._handleKey.bind(this); - } - - configure({ timeToDown, hardening, decrementTTD }) { - if (timeToDown !== undefined) this.initialTimeToDown = this.timeToDown = timeToDown; - if (hardening !== undefined) this.hardening = hardening; - if (decrementTTD !== undefined) this.decrementTTD = decrementTTD; - } - - _createGrid(w, h) { - return Array.from({ length: h }, () => Array(w).fill(0)); - } - - start() { - if (this.isRunning) return; - this.isRunning = true; - this.isPaused = false; - this.grid = this._createGrid(10, 20); - this.score = 0; - this.count = 0; - this.timeToDown = this.initialTimeToDown; - this.storedPiece = null; - this.canStore = true; - this._spawnNewPiece(); - document.addEventListener('keydown', this._keyHandler); - this._startGameLoop(); - } - - stop() { - this.isRunning = false; - this.isPaused = false; - if (this.animationFrameId !== null) { - cancelAnimationFrame(this.animationFrameId); - this.animationFrameId = null; - } - this.accumulator = 0; - this.lastTime = 0; - document.removeEventListener('keydown', this._keyHandler); - } - - pause() { - if (!this.isRunning) return; - this.isPaused = !this.isPaused; - if (!this.isPaused) { - this.lastTime = 0; - this._startGameLoop(); - } - } - - _startGameLoop() { - this.lastTime = 0; - this.accumulator = 0; - - const gameLoop = (currentTime) => { - if (!this.isRunning) return; - - if (this.isPaused) { - this.animationFrameId = requestAnimationFrame(gameLoop); - return; - } - - if (this.lastTime === 0) { - this.lastTime = currentTime; - this.animationFrameId = requestAnimationFrame(gameLoop); - return; - } - - const deltaTime = currentTime - this.lastTime; - this.lastTime = currentTime; - this.accumulator += deltaTime; - - while (this.isRunning && this.accumulator >= this.timeToDown) { - this._tick(); - this.accumulator -= this.timeToDown; - if (this.accumulator > this.timeToDown * 3) { - this.accumulator = 0; - break; - } - } - - this.onRender(); - this.animationFrameId = requestAnimationFrame(gameLoop); - }; - - this.animationFrameId = requestAnimationFrame(gameLoop); - } - - _tick() { - if (!this.currentPiece) return; - if (this._canMoveDown()) { - this.currentPiece.moveDown(); - } else { - this._lockPiece(); - this.verifierLignes(); - this._makeHarder(); - this._spawnNewPiece(); - this.canStore = true; - if (!this._canSpawn()) this._gameOver(); - } - } - - _handleKey(e) { - if (!this.isRunning || !this.currentPiece) return; - - switch (e.key) { - case 'ArrowLeft': - e.preventDefault(); - if (!this.isPaused && this._canMoveLeft()) this.currentPiece.moveLeft(); - break; - case 'ArrowRight': - e.preventDefault(); - if (!this.isPaused && this._canMoveRight()) this.currentPiece.moveRight(); - break; - case 'ArrowDown': - e.preventDefault(); - if (!this.isPaused && this._canMoveDown()) { - this.currentPiece.moveDown(); - this.score += 1; - this.accumulator = 0; - } - break; - case ' ': - e.preventDefault(); - if (!this.isPaused) this._hardDrop(); - break; - case 'q': case 'Q': - e.preventDefault(); - if (!this.isPaused) this._rotatePiece(-1); - break; - case 'w': case 'W': - e.preventDefault(); - if (!this.isPaused) this._rotatePiece(1); - break; - case 'c': case 'C': - e.preventDefault(); - if (!this.isPaused) this._storePiece(); - break; - } - - this.onRender(); - } - - _hardDrop() { - if (!this.currentPiece) return; - let dist = 0; - while (this._canMoveDown()) { this.currentPiece.moveDown(); dist++; } - this.score += dist * 2; - this._lockPiece(); - this.verifierLignes(); - this._makeHarder(); - this._spawnNewPiece(); - this.canStore = true; - this.accumulator = 0; - if (!this._canSpawn()) this._gameOver(); - } - - _rotatePiece(direction) { - if (!this.currentPiece) return; - const originalPos = { ...this.currentPiece.getPosition() }; - - if (direction === -1) this.currentPiece.rotateLeft(); - else this.currentPiece.rotateRight(); - - if (!this._isValidPosition()) { - this.currentPiece.moveRight(); - if (this._isValidPosition()) return; - - this.currentPiece.moveLeft(); - this.currentPiece.moveLeft(); - if (this._isValidPosition()) return; - - this.currentPiece.moveLeft(); - if (this._isValidPosition()) return; - - this.currentPiece.moveRight(); - this.currentPiece.moveRight(); - this.currentPiece.position.y--; - if (this._isValidPosition()) return; - - this.currentPiece.position.y = originalPos.y; - this.currentPiece.position.x = originalPos.x; - if (direction === -1) this.currentPiece.rotateRight(); - else this.currentPiece.rotateLeft(); - } - } - - _storePiece() { - if (!this.canStore || !this.currentPiece) return; - - if (this.storedPiece === null) { - this.storedPiece = this.currentPiece; - this._spawnNewPiece(); - } else { - const temp = this.storedPiece; - this.storedPiece = this.currentPiece; - this.currentPiece = temp; - this.currentPiece.position.x = 3; - this.currentPiece.position.y = 0; - } - this.canStore = false; - this.accumulator = 0; - } - - _spawnNewPiece() { - this.currentPiece = this.nextPiece || this._createRandomPiece(); - this.nextPiece = this._createRandomPiece(); - this._updateBufferGrid(); - } - - _createRandomPiece() { - const types = [PieceT, PieceL, PieceReverseL, PieceI, PieceZ, PieceReverseZ, PieceO]; - return new types[Math.floor(Math.random() * types.length)](3, 0); - } - - _updateBufferGrid() { - this.bufferGrid = this._createGrid(10, 5); - if (!this.nextPiece) return; - const shape = this.nextPiece.getShape(); - const offsetX = Math.floor((10 - shape[0].length) / 2); - for (let y = 0; y < shape.length; y++) - for (let x = 0; x < shape[y].length; x++) - if (shape[y][x] !== 0) - this.bufferGrid[y + 1][x + offsetX] = this.nextPiece.getColor(); - } - - verifierLignes() { - let cleared = 0; - for (let y = this.grid.length - 1; y >= 0; y--) { - if (this.grid[y].every(c => c !== 0)) { - this.grid.splice(y, 1); - this.grid.unshift(Array(10).fill(0)); - cleared++; - y++; - } - } - const points = [0, 100, 300, 500, 800]; - this.score += points[cleared]; - this.count += points[cleared]; - if (this.onLinesCleared && cleared > 0) - this.onLinesCleared(cleared, this.lastLandingCol); - } - - _makeHarder() { - if (this.count >= this.hardening) { - this.count = 0; - this.timeToDown = Math.max(100, this.timeToDown - this.decrementTTD); - } - } - - _canMoveDown() { - if (!this.currentPiece) return false; - const { x, y } = this.currentPiece.getPosition(); - const shape = this.currentPiece.getShape(); - for (let row = 0; row < shape.length; row++) - for (let col = 0; col < shape[row].length; col++) - if (shape[row][col] !== 0) { - const ny = y + row + 1; - const nx = x + col; - if (ny >= this.grid.length || this.grid[ny][nx] !== 0) return false; - } - return true; - } - - _canMoveLeft() { - if (!this.currentPiece) return false; - const { x, y } = this.currentPiece.getPosition(); - const shape = this.currentPiece.getShape(); - for (let row = 0; row < shape.length; row++) - for (let col = 0; col < shape[row].length; col++) - if (shape[row][col] !== 0) { - const nx = x + col - 1; - if (nx < 0 || this.grid[y + row][nx] !== 0) return false; - } - return true; - } - - _canMoveRight() { - if (!this.currentPiece) return false; - const { x, y } = this.currentPiece.getPosition(); - const shape = this.currentPiece.getShape(); - for (let row = 0; row < shape.length; row++) - for (let col = 0; col < shape[row].length; col++) - if (shape[row][col] !== 0) { - const nx = x + col + 1; - if (nx >= this.grid[0].length || this.grid[y + row][nx] !== 0) return false; - } - return true; - } - - _isValidPosition() { - if (!this.currentPiece) return false; - const { x, y } = this.currentPiece.getPosition(); - const shape = this.currentPiece.getShape(); - for (let row = 0; row < shape.length; row++) - for (let col = 0; col < shape[row].length; col++) - if (shape[row][col] !== 0) { - const gx = x + col; - const gy = y + row; - if (gx < 0 || gx >= this.grid[0].length || - gy < 0 || gy >= this.grid.length || - this.grid[gy][gx] !== 0) return false; - } - return true; - } - - _canSpawn() { return this._isValidPosition(); } - - _lockPiece() { - if (!this.currentPiece) return; - const { x, y } = this.currentPiece.getPosition(); - const shape = this.currentPiece.getShape(); - const color = this.currentPiece.getColor(); - for (let row = 0; row < shape.length; row++) - for (let col = 0; col < shape[row].length; col++) - if (shape[row][col] !== 0) - this.grid[y + row][x + col] = color; - this.lastLandingCol = x + Math.floor(shape[0].length / 2); - if (this.onBlockPlaced) this.onBlockPlaced(this.grid.map(r => [...r])); - } - - addGarbageLines(lines) { - if (!this.isRunning || !lines.length) return; - this.grid.splice(0, lines.length); - for (const line of lines) this.grid.push([...line]); - if (!this._isValidPosition()) this._gameOver(); - } - - _gameOver() { - this.stop(); - this.onGameOver(this.score); - } -} diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/ui.js b/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/ui.js deleted file mode 100755 index d738e54..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/ui.js +++ /dev/null @@ -1,162 +0,0 @@ -// ───────────────────────────────────────────── -// UI -// ───────────────────────────────────────────── - -const btnStart = document.getElementById('btn-start'); -const btnPause = document.getElementById('btn-pause'); -const btnStop = document.getElementById('btn-stop'); -const overlay = document.getElementById('overlay'); -const inputTTD = document.getElementById('input-ttd'); -const inputHardening = document.getElementById('input-hardening'); -const inputDecrement = document.getElementById('input-decrement'); - -// Duel UI -const btnJoinDuel = document.getElementById('btn-join-duel'); -const btnLeaveDuel = document.getElementById('btn-leave-duel'); -const inputRoomCode = document.getElementById('input-room-code'); -const duelStatusEl = document.getElementById('duel-status'); -const opponentSection = document.getElementById('opponent-section'); - -function updateButtons() { - btnStart.disabled = game.isRunning; - btnPause.disabled = !game.isRunning; - btnStop.disabled = !game.isRunning; - btnPause.textContent = game.isPaused ? 'Resume' : 'Pause'; - inputTTD.disabled = game.isRunning; - inputHardening.disabled = game.isRunning; - inputDecrement.disabled = game.isRunning; -} - -function showOverlay(title, score) { - document.getElementById('overlay-title').textContent = title; - document.getElementById('overlay-score').textContent = score !== undefined ? `Score : ${score}` : ''; - overlay.classList.add('visible'); -} - -function hideOverlay() { - overlay.classList.remove('visible'); -} - -// ───────────────────────────────────────────── -// SOCKET + DUEL -// ───────────────────────────────────────────── - -const socket = io({ - auth: { token: localStorage.getItem('auth_token') }, - reconnection: true, - reconnectionAttempts: 5, - reconnectionDelay: 1000, - transports: ['websocket', 'polling'] -}); - -let duel = null; - -function updateDuelStatus(status, opponentName) { - duelStatusEl.className = ''; - if (status === 'waiting') { - duelStatusEl.textContent = 'En attente d\'un adversaire…'; - duelStatusEl.classList.add('waiting'); - opponentSection.classList.remove('visible'); - } else if (status === 'ready') { - duelStatusEl.textContent = `Prêt — ${opponentName}`; - duelStatusEl.classList.add('ready'); - opponentSection.classList.add('visible'); - if (duel) duel.hideOpponentOverlay(); - renderOpponent(duel ? duel.opponentGrid : Array.from({length:20}, () => Array(10).fill(0))); - } else { - duelStatusEl.textContent = '—'; - opponentSection.classList.remove('visible'); - } -} - -function startLocalGame() { - hideOverlay(); - game.start(); - updateButtons(); - render(); -} - -btnJoinDuel.addEventListener('click', () => { - const code = inputRoomCode.value.trim().toUpperCase(); - if (!code) return; - if (duel) { duel.leave(); } - duel = new Duel(socket, game, updateDuelStatus, startLocalGame); - duel.join(code); - btnJoinDuel.disabled = true; - btnLeaveDuel.disabled = false; - inputRoomCode.disabled = true; - updateDuelStatus('waiting', null); -}); - -btnLeaveDuel.addEventListener('click', () => { - if (duel) { duel.leave(); duel = null; } - btnJoinDuel.disabled = false; - btnLeaveDuel.disabled = true; - inputRoomCode.disabled = false; - updateDuelStatus(null, null); -}); - -// ───────────────────────────────────────────── -// INIT -// ───────────────────────────────────────────── - -const game = new Tetris( - // onRender - () => { - if (duel) duel.synchronize_game(); - render(); - updateButtons(); - }, - // onGameOver - (score) => { - if (duel) duel.onLocalGameOver(score); - render(); - updateButtons(); - showOverlay('GAME OVER', score); - }, - // onBlockPlaced — relay duel - (grid) => { - if (duel) duel.onLocalBlockPlaced(grid, game.score); - }, - // onLinesCleared — relay duel - (count, holeCol) => { - if (duel) duel.onLocalLinesCleared(count, holeCol); - } -); - -btnStart.addEventListener('click', () => { - if (duel && duel.isReady) { - duel.startDuel(); // déclenche les deux parties via le serveur - } else { - startLocalGame(); // solo - } -}); - -btnPause.addEventListener('click', () => { - game.pause(); - updateButtons(); - if (game.isPaused) showOverlay('PAUSE'); - else hideOverlay(); -}); - -btnStop.addEventListener('click', () => { - game.stop(); - updateButtons(); - render(); - showOverlay('STOPPED'); -}); - -function applySettings() { - game.configure({ - timeToDown: parseInt(inputTTD.value, 10), - hardening: parseInt(inputHardening.value, 10), - decrementTTD: parseInt(inputDecrement.value, 10), - }); -} - -inputTTD.addEventListener('change', applySettings); -inputHardening.addEventListener('change', applySettings); -inputDecrement.addEventListener('change', applySettings); - -render(); -updateButtons(); diff --git a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/windows.js b/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/windows.js deleted file mode 100755 index 229a229..0000000 --- a/Transcendence/Transcendance-Test/Transcendence/srcs/frontend/src/windows.js +++ /dev/null @@ -1,234 +0,0 @@ -import { CSS } from './config.js'; -import { eventBus, Events } from './events.js'; - -/** - * Centralized window registry - * Manages window visibility and positioning - */ -class WindowRegistry { - constructor() { - this.windows = new Map(); - } - - /** - * Registers a window in the registry - * @param {string} name - Unique window name - * @param {Window} window - Window instance - */ - register(name, window) { - this.windows.set(name, window); - } - - /** - * Gets a window by its name - * @param {string} name - Window name - * @returns {Window|undefined} - */ - get(name) { - return this.windows.get(name); - } - - /** - * Returns all visible windows - * @returns {Window[]} - */ - getVisible() { - return Array.from(this.windows.values()).filter(w => w.isVisible()); - } - - /** - * Reorganizes visible windows - */ - reorganize() { - const visible = this.getVisible(); - - if (visible.length === 0) return; - - if (visible.length === 1) { - visible[0].setPosition('center'); - } else if (visible.length === 2) { - visible[0].setPosition('left'); - visible[1].setPosition('right'); - } else { - visible.forEach(w => w.setPosition('center')); - } - } - - /** - * Shows a window and reorganizes - * @param {string} name - Window name - */ - show(name) { - const window = this.get(name); - if (window) { - window.show(); - } - } - - /** - * Hides a window and reorganizes - * @param {string} name - Window name - */ - hide(name) { - const window = this.get(name); - if (window) { - window.hide(); - } - } - - /** - * Toggles window visibility - * @param {string} name - Window name - */ - toggle(name) { - const window = this.get(name); - if (window) { - window.toggle(); - } - } -} - -// Singleton registry instance -export const windowRegistry = new WindowRegistry(); - -/** - * Base class for all windows - * Uses CSS classes instead of inline styles - */ -export class Window { - /** - * @param {Object} options - Configuration options - * @param {string} options.name - Unique name for the registry - * @param {string} options.title - Title displayed in the header - * @param {string[]} [options.cssClasses] - Additional CSS classes - */ - constructor({ name, title, cssClasses = [] }) { - this.name = name; - - // Create main container - this.element = document.createElement('div'); - this.element.className = [CSS.WINDOW, ...cssClasses].join(' '); - - // Header - this.header = document.createElement('div'); - this.header.className = CSS.WINDOW_HEADER; - - this.titleElement = document.createElement('span'); - this.titleElement.className = CSS.WINDOW_TITLE; - this.titleElement.textContent = title; - - this.closeBtn = document.createElement('button'); - this.closeBtn.className = CSS.WINDOW_CLOSE; - this.closeBtn.textContent = '✖'; - this.closeBtn.setAttribute('aria-label', 'Close'); - this.closeBtn.addEventListener('click', () => this.hide()); - - this.header.append(this.titleElement, this.closeBtn); - - // Body - this.body = document.createElement('div'); - this.body.className = CSS.WINDOW_BODY; - - // Assembly - this.element.append(this.header, this.body); - document.body.appendChild(this.element); - - // Register in the registry - windowRegistry.register(name, this); - } - - /** - * Checks if the window is visible - * @returns {boolean} - */ - isVisible() { - return this.element.classList.contains(CSS.WINDOW_VISIBLE); - } - - /** - * Sets the window position - * @param {'center'|'left'|'right'} position - */ - setPosition(position) { - this.element.classList.remove('window--left', 'window--right'); - - if (position === 'left') { - this.element.classList.add('window--left'); - } else if (position === 'right') { - this.element.classList.add('window--right'); - } - } - - /** - * Shows the window - */ - show() { - const wasHidden = !this.isVisible(); - this.element.classList.add(CSS.WINDOW_VISIBLE); - - if (wasHidden) { - windowRegistry.reorganize(); - eventBus.emit(Events.WINDOW_OPENED, { name: this.name }); - } - } - - /** - * Hides the window - */ - hide() { - const wasVisible = this.isVisible(); - this.element.classList.remove(CSS.WINDOW_VISIBLE); - - if (wasVisible) { - windowRegistry.reorganize(); - eventBus.emit(Events.WINDOW_CLOSED, { name: this.name }); - } - } - - /** - * Toggles visibility - */ - toggle() { - if (this.isVisible()) { - this.hide(); - } else { - this.show(); - } - } - - /** - * Updates the window title - * @param {string} title - */ - setTitle(title) { - this.titleElement.textContent = title; - } - - /** - * Creates an element with CSS classes - * @param {string} tag - HTML tag - * @param {string|string[]} classes - CSS classes - * @param {Object} [attrs] - Additional attributes - * @returns {HTMLElement} - */ - createElement(tag, classes, attrs = {}) { - const element = document.createElement(tag); - const classList = Array.isArray(classes) ? classes : [classes]; - element.className = classList.filter(Boolean).join(' '); - - Object.entries(attrs).forEach(([key, value]) => { - if (key === 'text') { - element.textContent = value; - } else if (key === 'html') { - element.innerHTML = value; - } else { - element.setAttribute(key, value); - } - }); - - return element; - } -} - -// Export old class name for compatibility (alias) -export { Window as fenetre };