feat: Implementar sesiones persistentes y botones de salida
- Añadido sistema de sesiones con localStorage - Nuevo hook useSessionStorage para manejar sesiones - Botón de salir de la partida (ExitGameButton) en todas las pantallas del juego - Botón de logout (LogoutButton) solo en el lobby - Evento leave_game en servidor para cerrar partida cuando alguien sale - Evento reconnect_session para reconectar jugadores después de recargar - Actualizado GameBoard para incluir botón de salida - Actualizado page.tsx para manejar sesiones y logout
This commit is contained in:
62
.agent/workflows/sesiones-y-botones.md
Normal file
62
.agent/workflows/sesiones-y-botones.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
description: Implementación de sesiones persistentes y botones de salida
|
||||||
|
---
|
||||||
|
|
||||||
|
# Implementación de Sesiones y Botones de Salida
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
Implementar tres mejoras principales:
|
||||||
|
1. Sistema de sesiones persistentes
|
||||||
|
2. Botón de salir de la partida (en todas las pantallas de juego)
|
||||||
|
3. Botón de salir del juego (solo en lobby)
|
||||||
|
|
||||||
|
## Tareas
|
||||||
|
|
||||||
|
### 1. Sistema de Sesiones Persistentes
|
||||||
|
|
||||||
|
**Cliente:**
|
||||||
|
- Crear hook `useSessionStorage` para manejar sesiones
|
||||||
|
- Guardar en localStorage:
|
||||||
|
- `playerName` y `fullPlayerName`
|
||||||
|
- `currentView` (login/lobby/game)
|
||||||
|
- `roomId` si está en una partida
|
||||||
|
- Al cargar la app, verificar si hay sesión activa
|
||||||
|
- Si hay sesión, reconectar al servidor y recuperar estado
|
||||||
|
|
||||||
|
**Servidor:**
|
||||||
|
- Implementar evento `reconnect_session` para validar y recuperar estado
|
||||||
|
- Mantener mapping de `socketId` a `playerId` persistente
|
||||||
|
- Al reconectar, actualizar el socketId del jugador en la partida
|
||||||
|
|
||||||
|
### 2. Botón de Salir de la Partida
|
||||||
|
|
||||||
|
**Cliente:**
|
||||||
|
- Crear componente `ExitGameButton` con icono de flecha
|
||||||
|
- Posicionar arriba a la izquierda
|
||||||
|
- Mostrar en todas las fases del juego (no en lobby)
|
||||||
|
- Al hacer clic, emitir evento `leave_game`
|
||||||
|
|
||||||
|
**Servidor:**
|
||||||
|
- Implementar evento `leave_game`
|
||||||
|
- Notificar a todos los jugadores que alguien salió
|
||||||
|
- Eliminar la partida de la BD
|
||||||
|
- Devolver a todos al lobby
|
||||||
|
|
||||||
|
### 3. Botón de Salir del Juego
|
||||||
|
|
||||||
|
**Cliente:**
|
||||||
|
- Crear componente `LogoutButton` con icono de apagar
|
||||||
|
- Posicionar arriba a la izquierda solo en lobby
|
||||||
|
- Al hacer clic:
|
||||||
|
- Limpiar localStorage
|
||||||
|
- Volver a vista de login
|
||||||
|
- Desconectar socket si está en partida
|
||||||
|
|
||||||
|
## Orden de Implementación
|
||||||
|
|
||||||
|
1. Crear hooks y utilidades para sesiones
|
||||||
|
2. Implementar botón de salir del juego (logout)
|
||||||
|
3. Implementar botón de salir de la partida
|
||||||
|
4. Implementar lógica de reconexión en servidor
|
||||||
|
5. Integrar sistema de sesiones en el cliente
|
||||||
|
6. Pruebas
|
||||||
112
SESIONES-IMPLEMENTACION.md
Normal file
112
SESIONES-IMPLEMENTACION.md
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# Resumen de Implementación: Sesiones y Botones de Salida
|
||||||
|
|
||||||
|
## Cambios Realizados
|
||||||
|
|
||||||
|
### 1. Sistema de Sesiones Persistentes ✅
|
||||||
|
|
||||||
|
#### Cliente
|
||||||
|
- **Nuevo hook**: `client/src/hooks/useSessionStorage.ts`
|
||||||
|
- Maneja el almacenamiento y recuperación de sesiones en localStorage
|
||||||
|
- Guarda: playerName, fullPlayerName, currentView, roomId
|
||||||
|
|
||||||
|
- **Actualización de `page.tsx`**:
|
||||||
|
- Integrado hook `useSessionStorage`
|
||||||
|
- Al iniciar sesión, se guarda la sesión
|
||||||
|
- Al cargar la app, se restaura la sesión si existe
|
||||||
|
- Al cambiar de vista (lobby/game), se actualiza la sesión
|
||||||
|
- Función `handleLogout` para limpiar sesión
|
||||||
|
|
||||||
|
#### Servidor
|
||||||
|
- **Nuevo evento**: `reconnect_session`
|
||||||
|
- Permite a un jugador reconectarse a una partida existente
|
||||||
|
- Actualiza el socketId del jugador en la partida
|
||||||
|
- Envía el estado actualizado al jugador reconectado
|
||||||
|
|
||||||
|
### 2. Botón de Salir de la Partida ✅
|
||||||
|
|
||||||
|
#### Cliente
|
||||||
|
- **Nuevo componente**: `client/src/components/ExitGameButton.tsx`
|
||||||
|
- Botón con icono de flecha
|
||||||
|
- Posicionado arriba a la izquierda (fixed top-4 left-4)
|
||||||
|
- Modal de confirmación antes de salir
|
||||||
|
- Se muestra en todas las fases del juego excepto en pantallas de victoria
|
||||||
|
|
||||||
|
- **Actualización de `GameBoard.tsx`**:
|
||||||
|
- Agregado prop `fullPlayerName`
|
||||||
|
- Integrado `ExitGameButton`
|
||||||
|
- Llama a `actions.leaveGame()` al confirmar
|
||||||
|
|
||||||
|
- **Actualización de `useSocket.ts`**:
|
||||||
|
- Nueva acción: `leaveGame()`
|
||||||
|
- Nuevo listener: `player_left_game`
|
||||||
|
|
||||||
|
#### Servidor
|
||||||
|
- **Nuevo evento**: `leave_game`
|
||||||
|
- Notifica a todos los jugadores que alguien abandonó
|
||||||
|
- Elimina la partida de la base de datos
|
||||||
|
- Limpia timers asociados
|
||||||
|
- Actualiza la lista de salas
|
||||||
|
- Desconecta a todos los jugadores de la sala
|
||||||
|
|
||||||
|
### 3. Botón de Salir del Juego (Logout) ✅
|
||||||
|
|
||||||
|
#### Cliente
|
||||||
|
- **Nuevo componente**: `client/src/components/LogoutButton.tsx`
|
||||||
|
- Botón con icono de apagar
|
||||||
|
- Posicionado arriba a la izquierda (fixed top-4 left-4)
|
||||||
|
- Solo visible en el lobby
|
||||||
|
|
||||||
|
- **Actualización de `page.tsx`**:
|
||||||
|
- Función `handleLogout()`:
|
||||||
|
- Limpia la sesión de localStorage
|
||||||
|
- Vuelve a la vista de login
|
||||||
|
- Si está en una partida, llama a `leaveGame()`
|
||||||
|
|
||||||
|
## Flujo de Uso
|
||||||
|
|
||||||
|
### Sesiones Persistentes
|
||||||
|
1. Usuario se loguea → sesión guardada en localStorage
|
||||||
|
2. Usuario cierra navegador
|
||||||
|
3. Usuario vuelve a abrir → sesión restaurada automáticamente
|
||||||
|
4. Si estaba en una partida, intenta reconectar
|
||||||
|
|
||||||
|
### Salir de la Partida
|
||||||
|
1. Usuario hace clic en botón de flecha (arriba izquierda)
|
||||||
|
2. Aparece modal de confirmación
|
||||||
|
3. Al confirmar:
|
||||||
|
- Servidor notifica a todos: "Jugador X ha abandonado"
|
||||||
|
- Partida eliminada de la BD
|
||||||
|
- Todos vuelven al lobby
|
||||||
|
|
||||||
|
### Salir del Juego (Logout)
|
||||||
|
1. Usuario hace clic en botón de apagar (arriba izquierda, solo en lobby)
|
||||||
|
2. Sesión eliminada de localStorage
|
||||||
|
3. Vuelve a pantalla de login
|
||||||
|
|
||||||
|
## Archivos Modificados
|
||||||
|
|
||||||
|
### Nuevos Archivos
|
||||||
|
- `client/src/hooks/useSessionStorage.ts`
|
||||||
|
- `client/src/components/LogoutButton.tsx`
|
||||||
|
- `client/src/components/ExitGameButton.tsx`
|
||||||
|
- `.agent/workflows/sesiones-y-botones.md`
|
||||||
|
|
||||||
|
### Archivos Modificados
|
||||||
|
- `client/src/app/page.tsx`
|
||||||
|
- `client/src/components/GameBoard.tsx`
|
||||||
|
- `client/src/hooks/useSocket.ts`
|
||||||
|
- `server/src/index.ts`
|
||||||
|
|
||||||
|
## Próximos Pasos
|
||||||
|
|
||||||
|
1. **Probar la aplicación**:
|
||||||
|
- Verificar que las sesiones persisten correctamente
|
||||||
|
- Probar el botón de salir de la partida
|
||||||
|
- Probar el botón de logout
|
||||||
|
- Verificar reconexión después de recargar
|
||||||
|
|
||||||
|
2. **Posibles mejoras**:
|
||||||
|
- Agregar notificación toast cuando alguien abandona
|
||||||
|
- Mejorar el manejo de errores en reconexión
|
||||||
|
- Agregar timeout de sesión (expiración automática)
|
||||||
|
- Guardar más información en la sesión (configuración, preferencias)
|
||||||
@@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useSocket } from '../hooks/useSocket';
|
import { useSocket } from '../hooks/useSocket';
|
||||||
|
import { useSessionStorage } from '../hooks/useSessionStorage';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import GameBoard from '../components/GameBoard';
|
import GameBoard from '../components/GameBoard';
|
||||||
|
import LogoutButton from '../components/LogoutButton';
|
||||||
import { GameRoom } from '../../../shared/types';
|
import { GameRoom } from '../../../shared/types';
|
||||||
|
|
||||||
// Constantes de apellidos
|
// Constantes de apellidos
|
||||||
@@ -21,6 +23,7 @@ type ViewState = 'login' | 'lobby' | 'game';
|
|||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { isConnected, gameState, roomsList, actions, socket } = useSocket();
|
const { isConnected, gameState, roomsList, actions, socket } = useSocket();
|
||||||
|
const { session, saveSession, updateSession, clearSession } = useSessionStorage();
|
||||||
|
|
||||||
// Estados locales de UI
|
// Estados locales de UI
|
||||||
const [view, setView] = useState<ViewState>('login');
|
const [view, setView] = useState<ViewState>('login');
|
||||||
@@ -35,13 +38,32 @@ export default function Home() {
|
|||||||
const [passwordPromptRoomId, setPasswordPromptRoomId] = useState<string | null>(null);
|
const [passwordPromptRoomId, setPasswordPromptRoomId] = useState<string | null>(null);
|
||||||
const [joinPassword, setJoinPassword] = useState('');
|
const [joinPassword, setJoinPassword] = useState('');
|
||||||
|
|
||||||
|
// Restaurar sesión al cargar
|
||||||
|
useEffect(() => {
|
||||||
|
if (session && isConnected) {
|
||||||
|
setPlayerName(session.playerName);
|
||||||
|
setFullPlayerName(session.fullPlayerName);
|
||||||
|
setView(session.currentView);
|
||||||
|
|
||||||
|
// Si había una partida activa, intentar reconectar
|
||||||
|
if (session.roomId && session.currentView === 'game') {
|
||||||
|
actions.reconnectSession({ playerName: session.fullPlayerName, roomId: session.roomId });
|
||||||
|
} else if (session.currentView === 'lobby') {
|
||||||
|
actions.refreshRooms();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [session, isConnected]);
|
||||||
|
|
||||||
// Efecto para cambiar a vista de juego cuando el servidor nos une
|
// Efecto para cambiar a vista de juego cuando el servidor nos une
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (gameState?.roomId) {
|
if (gameState?.roomId) {
|
||||||
setView('game');
|
setView('game');
|
||||||
|
// Guardar en sesión
|
||||||
|
updateSession({ currentView: 'game', roomId: gameState.roomId });
|
||||||
} else if (view === 'game' && !gameState) {
|
} else if (view === 'game' && !gameState) {
|
||||||
// Si estábamos en juego y volvemos a null, volver al lobby
|
// Si estábamos en juego y volvemos a null, volver al lobby
|
||||||
setView('lobby');
|
setView('lobby');
|
||||||
|
updateSession({ currentView: 'lobby', roomId: undefined });
|
||||||
}
|
}
|
||||||
}, [gameState]);
|
}, [gameState]);
|
||||||
|
|
||||||
@@ -50,13 +72,32 @@ export default function Home() {
|
|||||||
if (playerName) {
|
if (playerName) {
|
||||||
// Generar apellido aleatorio
|
// Generar apellido aleatorio
|
||||||
const randomSurname = SURNAMES[Math.floor(Math.random() * SURNAMES.length)];
|
const randomSurname = SURNAMES[Math.floor(Math.random() * SURNAMES.length)];
|
||||||
setFullPlayerName(`${playerName} ${randomSurname}`);
|
const fullName = `${playerName} ${randomSurname}`;
|
||||||
|
setFullPlayerName(fullName);
|
||||||
|
|
||||||
|
// Guardar sesión
|
||||||
|
saveSession({
|
||||||
|
playerName,
|
||||||
|
fullPlayerName: fullName,
|
||||||
|
currentView: 'lobby'
|
||||||
|
});
|
||||||
|
|
||||||
setView('lobby');
|
setView('lobby');
|
||||||
actions.refreshRooms();
|
actions.refreshRooms();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
clearSession();
|
||||||
|
setView('login');
|
||||||
|
setPlayerName('');
|
||||||
|
setFullPlayerName('');
|
||||||
|
// Si está en una partida, salir
|
||||||
|
if (gameState?.roomId) {
|
||||||
|
actions.leaveGame();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleCreateGame = (e: React.FormEvent) => {
|
const handleCreateGame = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
actions.createGame(fullPlayerName, createConfig.maxPlayers, createConfig.password);
|
actions.createGame(fullPlayerName, createConfig.maxPlayers, createConfig.password);
|
||||||
@@ -149,8 +190,9 @@ export default function Home() {
|
|||||||
return (
|
return (
|
||||||
<GameBoard
|
<GameBoard
|
||||||
gameState={gameState}
|
gameState={gameState}
|
||||||
currentPlayerId={socket.id}
|
currentPlayerId={socket.id || ''}
|
||||||
actions={actions}
|
actions={actions}
|
||||||
|
fullPlayerName={fullPlayerName}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -185,6 +227,9 @@ export default function Home() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Botón de Logout - solo en lobby */}
|
||||||
|
{view === 'lobby' && <LogoutButton onClick={handleLogout} />}
|
||||||
|
|
||||||
<div className="z-10 w-full flex-1 flex flex-col items-center justify-center p-4">
|
<div className="z-10 w-full flex-1 flex flex-col items-center justify-center p-4">
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
|
|
||||||
|
|||||||
80
client/src/components/ExitGameButton.tsx
Normal file
80
client/src/components/ExitGameButton.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
interface ExitGameButtonProps {
|
||||||
|
onExit: () => void;
|
||||||
|
playerName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ExitGameButton({ onExit, playerName }: ExitGameButtonProps) {
|
||||||
|
const [showConfirm, setShowConfirm] = useState(false);
|
||||||
|
|
||||||
|
const handleConfirmExit = () => {
|
||||||
|
setShowConfirm(false);
|
||||||
|
onExit();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<motion.button
|
||||||
|
onClick={() => setShowConfirm(true)}
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
className="fixed top-4 left-4 z-50 bg-yellow-900/80 hover:bg-yellow-800 text-white p-3 rounded-lg border border-yellow-700/50 backdrop-blur-sm shadow-lg transition-all group"
|
||||||
|
title="Abandonar partida"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="h-5 w-5"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className="text-xs font-bold uppercase tracking-wider hidden md:inline">Salir</span>
|
||||||
|
</div>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
{/* Modal de confirmación */}
|
||||||
|
{showConfirm && (
|
||||||
|
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0.9, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
className="bg-zinc-900 p-6 rounded-lg border border-red-700/50 w-full max-w-md mx-4 shadow-2xl"
|
||||||
|
>
|
||||||
|
<h3 className="text-xl font-bold text-red-400 mb-4 uppercase flex items-center gap-2">
|
||||||
|
⚠️ Abandonar Partida
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-300 mb-2">
|
||||||
|
¿Estás seguro de que quieres abandonar la partida?
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-400 mb-6">
|
||||||
|
La partida se cerrará para todos los jugadores y se perderá todo el progreso.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowConfirm(false)}
|
||||||
|
className="flex-1 py-3 bg-gray-700 hover:bg-gray-600 text-white rounded font-bold uppercase text-sm transition-colors"
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleConfirmExit}
|
||||||
|
className="flex-1 py-3 bg-red-900 hover:bg-red-800 text-white rounded font-bold uppercase text-sm transition-colors"
|
||||||
|
>
|
||||||
|
Salir
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,14 +5,16 @@ import { GameState, GamePhase, Player, GAME_CONFIG, Faction } from '../../../sha
|
|||||||
import MissionReveal from './MissionReveal';
|
import MissionReveal from './MissionReveal';
|
||||||
import MissionResult from './MissionResult';
|
import MissionResult from './MissionResult';
|
||||||
import VictoryScreen from './VictoryScreen';
|
import VictoryScreen from './VictoryScreen';
|
||||||
|
import ExitGameButton from './ExitGameButton';
|
||||||
|
|
||||||
interface GameBoardProps {
|
interface GameBoardProps {
|
||||||
gameState: GameState;
|
gameState: GameState;
|
||||||
currentPlayerId: string;
|
currentPlayerId: string;
|
||||||
actions: any;
|
actions: any;
|
||||||
|
fullPlayerName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function GameBoard({ gameState, currentPlayerId, actions }: GameBoardProps) {
|
export default function GameBoard({ gameState, currentPlayerId, actions, fullPlayerName }: GameBoardProps) {
|
||||||
const [selectedTeam, setSelectedTeam] = useState<string[]>([]);
|
const [selectedTeam, setSelectedTeam] = useState<string[]>([]);
|
||||||
|
|
||||||
// Hooks para FASE REVEAL ROLE
|
// Hooks para FASE REVEAL ROLE
|
||||||
@@ -298,6 +300,14 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full h-screen flex flex-col overflow-hidden">
|
<div className="relative w-full h-screen flex flex-col overflow-hidden">
|
||||||
|
{/* Botón de Salir de la Partida - No mostrar en pantallas de victoria */}
|
||||||
|
{gameState.phase !== GamePhase.ALLIED_WIN && gameState.phase !== GamePhase.NAZIS_WIN && (
|
||||||
|
<ExitGameButton
|
||||||
|
onExit={() => actions.leaveGame()}
|
||||||
|
playerName={fullPlayerName}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Fondo */}
|
{/* Fondo */}
|
||||||
<div className="absolute inset-0 z-0 opacity-40">
|
<div className="absolute inset-0 z-0 opacity-40">
|
||||||
<Image
|
<Image
|
||||||
|
|||||||
33
client/src/components/LogoutButton.tsx
Normal file
33
client/src/components/LogoutButton.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
interface LogoutButtonProps {
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LogoutButton({ onClick }: LogoutButtonProps) {
|
||||||
|
return (
|
||||||
|
<motion.button
|
||||||
|
onClick={onClick}
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
className="fixed top-4 left-4 z-50 bg-red-900/80 hover:bg-red-800 text-white p-3 rounded-lg border border-red-700/50 backdrop-blur-sm shadow-lg transition-all group"
|
||||||
|
title="Salir del juego"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="h-5 w-5"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8 7a1 1 0 00-1 1v4a1 1 0 001 1h4a1 1 0 001-1V8a1 1 0 00-1-1H8z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className="text-xs font-bold uppercase tracking-wider hidden md:inline">Salir</span>
|
||||||
|
</div>
|
||||||
|
</motion.button>
|
||||||
|
);
|
||||||
|
}
|
||||||
53
client/src/hooks/useSessionStorage.ts
Normal file
53
client/src/hooks/useSessionStorage.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
interface SessionData {
|
||||||
|
playerName: string;
|
||||||
|
fullPlayerName: string;
|
||||||
|
currentView: 'login' | 'lobby' | 'game';
|
||||||
|
roomId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSessionStorage() {
|
||||||
|
const [session, setSession] = useState<SessionData | null>(null);
|
||||||
|
|
||||||
|
// Cargar sesión al iniciar
|
||||||
|
useEffect(() => {
|
||||||
|
const savedSession = localStorage.getItem('resistencia_session');
|
||||||
|
if (savedSession) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(savedSession);
|
||||||
|
setSession(parsed);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error parsing session:', e);
|
||||||
|
localStorage.removeItem('resistencia_session');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Guardar sesión
|
||||||
|
const saveSession = (data: SessionData) => {
|
||||||
|
localStorage.setItem('resistencia_session', JSON.stringify(data));
|
||||||
|
setSession(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Actualizar sesión parcialmente
|
||||||
|
const updateSession = (partial: Partial<SessionData>) => {
|
||||||
|
if (session) {
|
||||||
|
const updated = { ...session, ...partial };
|
||||||
|
saveSession(updated);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Limpiar sesión
|
||||||
|
const clearSession = () => {
|
||||||
|
localStorage.removeItem('resistencia_session');
|
||||||
|
setSession(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
session,
|
||||||
|
saveSession,
|
||||||
|
updateSession,
|
||||||
|
clearSession
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -50,6 +50,12 @@ export const useSocket = () => {
|
|||||||
setGameState(null); // Resetear estado para volver al lobby
|
setGameState(null); // Resetear estado para volver al lobby
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Manejar cuando un jugador abandona la partida
|
||||||
|
socketInstance.on('player_left_game', ({ playerName }: { playerName: string }) => {
|
||||||
|
console.log(`${playerName} ha abandonado la partida`);
|
||||||
|
// El servidor ya habrá cerrado la partida, solo mostramos mensaje
|
||||||
|
});
|
||||||
|
|
||||||
setSocket(socketInstance);
|
setSocket(socketInstance);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -90,6 +96,14 @@ export const useSocket = () => {
|
|||||||
socket?.emit('assassin_kill', { roomId: gameState?.roomId, targetId });
|
socket?.emit('assassin_kill', { roomId: gameState?.roomId, targetId });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const leaveGame = () => {
|
||||||
|
socket?.emit('leave_game', { roomId: gameState?.roomId });
|
||||||
|
};
|
||||||
|
|
||||||
|
const reconnectSession = (sessionData: { playerName: string; roomId?: string }) => {
|
||||||
|
socket?.emit('reconnect_session', sessionData);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
socket,
|
socket,
|
||||||
isConnected,
|
isConnected,
|
||||||
@@ -105,6 +119,8 @@ export const useSocket = () => {
|
|||||||
voteMission,
|
voteMission,
|
||||||
voteLeader: (approve: boolean) => socket?.emit('vote_leader', { roomId: gameState?.roomId, approve }),
|
voteLeader: (approve: boolean) => socket?.emit('vote_leader', { roomId: gameState?.roomId, approve }),
|
||||||
assassinKill,
|
assassinKill,
|
||||||
|
leaveGame,
|
||||||
|
reconnectSession,
|
||||||
finishIntro: () => socket?.emit('finish_intro', { roomId: gameState?.roomId }),
|
finishIntro: () => socket?.emit('finish_intro', { roomId: gameState?.roomId }),
|
||||||
finishReveal: () => socket?.emit('finish_reveal', { roomId: gameState?.roomId }),
|
finishReveal: () => socket?.emit('finish_reveal', { roomId: gameState?.roomId }),
|
||||||
finishRollCall: () => socket?.emit('finish_roll_call', { roomId: gameState?.roomId }),
|
finishRollCall: () => socket?.emit('finish_roll_call', { roomId: gameState?.roomId }),
|
||||||
|
|||||||
@@ -301,7 +301,89 @@ io.on('connection', (socket) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 9. DESCONEXIÓN
|
// 8. ABANDONAR PARTIDA (cualquier jugador)
|
||||||
|
socket.on('leave_game', ({ roomId }) => {
|
||||||
|
const game = games[roomId];
|
||||||
|
if (game) {
|
||||||
|
// Encontrar el jugador que se va
|
||||||
|
const leavingPlayer = game.state.players.find(p => p.id === socket.id);
|
||||||
|
const playerName = leavingPlayer?.name || 'Un jugador';
|
||||||
|
|
||||||
|
// Notificar a todos los jugadores
|
||||||
|
io.to(roomId).emit('player_left_game', { playerName });
|
||||||
|
io.to(roomId).emit('game_finalized');
|
||||||
|
|
||||||
|
// Eliminar la partida de la base de datos
|
||||||
|
delete games[roomId];
|
||||||
|
|
||||||
|
// Limpiar timer si existe
|
||||||
|
if (voteTimers[roomId]) {
|
||||||
|
clearTimeout(voteTimers[roomId]);
|
||||||
|
delete voteTimers[roomId];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualizar lista de salas
|
||||||
|
io.emit('rooms_list', getRoomsList());
|
||||||
|
|
||||||
|
// Desconectar a todos de la sala
|
||||||
|
io.in(roomId).socketsLeave(roomId);
|
||||||
|
|
||||||
|
console.log(`[LEAVE_GAME] ${playerName} abandonó la partida ${roomId}. Partida eliminada.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 9. RECONECTAR SESIÓN
|
||||||
|
socket.on('reconnect_session', ({ playerName, roomId }) => {
|
||||||
|
console.log(`[RECONNECT_SESSION] Intento de reconexión: ${playerName} a sala ${roomId}`);
|
||||||
|
|
||||||
|
if (roomId) {
|
||||||
|
const game = games[roomId];
|
||||||
|
if (game) {
|
||||||
|
// Buscar si el jugador existe en la partida
|
||||||
|
const existingPlayer = game.state.players.find(p => p.name === playerName);
|
||||||
|
|
||||||
|
if (existingPlayer) {
|
||||||
|
// Actualizar el socket ID del jugador
|
||||||
|
existingPlayer.id = socket.id;
|
||||||
|
|
||||||
|
// Unir al socket a la sala
|
||||||
|
socket.join(roomId);
|
||||||
|
|
||||||
|
// Enviar estado actualizado
|
||||||
|
socket.emit('game_joined', { roomId, state: game.state });
|
||||||
|
io.to(roomId).emit('game_state', game.state);
|
||||||
|
|
||||||
|
console.log(`[RECONNECT_SESSION] ${playerName} reconectado exitosamente a ${roomId}`);
|
||||||
|
} else {
|
||||||
|
console.log(`[RECONNECT_SESSION] Jugador ${playerName} no encontrado en partida ${roomId}`);
|
||||||
|
socket.emit('error', 'No se pudo reconectar a la partida');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`[RECONNECT_SESSION] Partida ${roomId} no existe`);
|
||||||
|
socket.emit('error', 'La partida ya no existe');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 10. FINALIZAR Y EXPULSAR JUGADORES (solo host)
|
||||||
|
socket.on('finalize_game', ({ roomId }) => {
|
||||||
|
const game = games[roomId];
|
||||||
|
if (game && game.hostId === socket.id) {
|
||||||
|
// Notificar a todos los jugadores que la partida ha sido finalizada
|
||||||
|
io.to(roomId).emit('game_finalized');
|
||||||
|
|
||||||
|
// Eliminar la partida inmediatamente del registro
|
||||||
|
delete games[roomId];
|
||||||
|
|
||||||
|
// Actualizar lista de salas para todos los clientes
|
||||||
|
io.emit('rooms_list', getRoomsList());
|
||||||
|
|
||||||
|
// Desconectar a todos los jugadores de la sala
|
||||||
|
io.in(roomId).socketsLeave(roomId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 11. DESCONEXIÓN
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
// Buscar en qué partida estaba y sacarlo (opcional, por ahora solo notificamos)
|
// Buscar en qué partida estaba y sacarlo (opcional, por ahora solo notificamos)
|
||||||
console.log('Desconectado:', socket.id);
|
console.log('Desconectado:', socket.id);
|
||||||
|
|||||||
Reference in New Issue
Block a user