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:
Resistencia Dev
2025-12-22 16:51:35 +01:00
parent be15983455
commit 53a5e3886e
9 changed files with 497 additions and 4 deletions

View 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>
)}
</>
);
}

View File

@@ -5,14 +5,16 @@ import { GameState, GamePhase, Player, GAME_CONFIG, Faction } from '../../../sha
import MissionReveal from './MissionReveal';
import MissionResult from './MissionResult';
import VictoryScreen from './VictoryScreen';
import ExitGameButton from './ExitGameButton';
interface GameBoardProps {
gameState: GameState;
currentPlayerId: string;
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[]>([]);
// Hooks para FASE REVEAL ROLE
@@ -298,6 +300,14 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
return (
<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 */}
<div className="absolute inset-0 z-0 opacity-40">
<Image

View 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>
);
}