Files
snatchgame/nuxt-snatchgame/app/pages/admin.vue
josedario87 62226ab5d4
Some checks failed
build-and-deploy / filter (push) Successful in 3s
build-and-deploy / build (push) Failing after 6s
build-and-deploy / deploy (push) Has been skipped
feat: Migración completa de SnatchGame a Nuxt 4
- Creado nuevo proyecto Nuxt 4 con estructura app/
- Servidor Colyseus separado para evitar problemas con decoradores
- Migrado GameRoom y toda la lógica del juego
- Implementado cliente con composables useGameClient
- Panel de administración funcional
- Componentes Vue migrados (HomeScreen, GameScreen, PlayerCard, etc)
- Configuración para ejecutar ambos servidores (npm run dev:all)
2025-08-05 16:05:51 -06:00

395 lines
9.4 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="admin-dashboard">
<div class="dashboard-header">
<h1>Panel de Administración - Snatch Game</h1>
<div class="connection-status" :class="{ connected: isConnected }">
{{ isConnected ? '🟢 Conectado' : '🔴 Desconectado' }}
</div>
</div>
<div class="dashboard-content">
<!-- Statistics Cards -->
<div class="stats-grid">
<div class="stat-card">
<h3>Jugadores Conectados</h3>
<div class="stat-value">{{ stats.connectedPlayers }}</div>
</div>
<div class="stat-card">
<h3>Juegos Activos</h3>
<div class="stat-value">{{ stats.activeGames }}</div>
</div>
<div class="stat-card">
<h3>Ronda Actual</h3>
<div class="stat-value">{{ stats.currentRound }}</div>
</div>
<div class="stat-card">
<h3>Estado del Juego</h3>
<div class="stat-value">{{ gameStateDisplay }}</div>
</div>
</div>
<!-- Control Panel -->
<div class="control-panel">
<h2>Controles del Juego</h2>
<div class="controls-grid">
<button @click="pauseGame" class="control-btn pause">
Pausar Juego
</button>
<button @click="resumeGame" class="control-btn resume">
Reanudar Juego
</button>
<button @click="advanceRound" class="control-btn advance">
Avanzar Ronda
</button>
<button @click="previousRound" class="control-btn previous">
Retroceder Ronda
</button>
<button @click="kickAllPlayers" class="control-btn danger">
🚫 Expulsar a Todos
</button>
</div>
</div>
<!-- Players List -->
<div class="players-section">
<h2>Jugadores Activos ({{ stats.players.length }})</h2>
<div v-if="stats.players.length === 0" class="no-players">
No hay jugadores conectados
</div>
<div v-else class="players-grid">
<div v-for="player in stats.players" :key="player.id" class="player-admin-card">
<div class="player-info">
<h4>{{ player.name }}</h4>
<div class="player-details">
<span>Sala: {{ player.roomId?.substring(0, 8) || 'N/A' }}</span>
<span>Rol: {{ player.producerRole }}</span>
<span>Puntos: {{ player.points }}</span>
</div>
<div class="player-tokens">
<span>🦃 {{ player.tokens.turkeys }}</span>
<span> {{ player.tokens.coffee }}</span>
<span>🌽 {{ player.tokens.corn }}</span>
</div>
</div>
<button @click="kickPlayer(player.id)" class="kick-btn">
Expulsar
</button>
</div>
</div>
</div>
<!-- Rooms List -->
<div class="rooms-section">
<h2>Salas Activas ({{ stats.rooms.length }})</h2>
<div v-if="stats.rooms.length === 0" class="no-rooms">
No hay salas activas
</div>
<div v-else class="rooms-grid">
<div v-for="room in stats.rooms" :key="room.roomId" class="room-card">
<h4>Sala: {{ room.roomId.substring(0, 8) }}</h4>
<div class="room-details">
<span>Jugadores: {{ room.players }}/{{ room.maxPlayers }}</span>
<span>Estado: {{ room.locked ? 'Cerrada' : 'Abierta' }}</span>
<span>Fase: {{ room.metadata?.gamePhase || 'N/A' }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, computed } from 'vue'
import { useAdminService } from '~/composables/useAdminService'
const adminService = useAdminService()
const stats = computed(() => adminService.stats.value)
const isConnected = computed(() => adminService.isConnected.value)
const gameStateDisplay = computed(() => {
const state = stats.value.gameState
switch (state) {
case 'waiting_for_players': return 'Esperando jugadores'
case 'in_progress': return 'En progreso'
case 'paused': return 'Pausado'
default: return state
}
})
onMounted(() => {
adminService.connect()
})
const pauseGame = async () => {
try {
const result = await adminService.pauseGame()
console.log('Game paused:', result)
} catch (error) {
console.error('Failed to pause game:', error)
alert('Error al pausar el juego')
}
}
const resumeGame = async () => {
try {
const result = await adminService.resumeGame()
console.log('Game resumed:', result)
} catch (error) {
console.error('Failed to resume game:', error)
alert('Error al reanudar el juego')
}
}
const advanceRound = async () => {
try {
const result = await adminService.advanceRound()
console.log('Round advanced:', result)
} catch (error) {
console.error('Failed to advance round:', error)
alert('Error al avanzar la ronda')
}
}
const previousRound = async () => {
try {
const result = await adminService.previousRound()
console.log('Round went back:', result)
} catch (error) {
console.error('Failed to go to previous round:', error)
alert('Error al retroceder la ronda')
}
}
const kickPlayer = async (playerId: string) => {
if (!confirm(`¿Estás seguro de expulsar al jugador ${playerId}?`)) return
try {
const result = await adminService.kickPlayer(playerId)
console.log('Player kicked:', result)
} catch (error) {
console.error('Failed to kick player:', error)
alert('Error al expulsar al jugador')
}
}
const kickAllPlayers = async () => {
if (!confirm('¿Estás seguro de expulsar a TODOS los jugadores?')) return
try {
const result = await adminService.kickAllPlayers()
console.log('All players kicked:', result)
} catch (error) {
console.error('Failed to kick all players:', error)
alert('Error al expulsar a todos los jugadores')
}
}
</script>
<style scoped>
.admin-dashboard {
min-height: 100vh;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
color: white;
padding: 2rem;
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 2px solid rgba(255, 255, 255, 0.2);
}
.dashboard-header h1 {
margin: 0;
font-size: 2rem;
}
.connection-status {
padding: 0.5rem 1rem;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
font-weight: 600;
}
.connection-status.connected {
background: rgba(76, 175, 80, 0.2);
}
.dashboard-content {
max-width: 1400px;
margin: 0 auto;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 1.5rem;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.stat-card h3 {
margin: 0 0 1rem 0;
font-size: 1rem;
opacity: 0.9;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: #ffd700;
}
.control-panel {
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 2rem;
margin-bottom: 2rem;
backdrop-filter: blur(10px);
}
.control-panel h2 {
margin: 0 0 1.5rem 0;
}
.controls-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.control-btn {
padding: 1rem;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
color: white;
}
.control-btn.pause {
background: rgba(255, 193, 7, 0.8);
}
.control-btn.resume {
background: rgba(76, 175, 80, 0.8);
}
.control-btn.advance, .control-btn.previous {
background: rgba(33, 150, 243, 0.8);
}
.control-btn.danger {
background: rgba(244, 67, 54, 0.8);
}
.control-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
filter: brightness(1.1);
}
.players-section, .rooms-section {
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 2rem;
margin-bottom: 2rem;
backdrop-filter: blur(10px);
}
.players-section h2, .rooms-section h2 {
margin: 0 0 1.5rem 0;
}
.no-players, .no-rooms {
text-align: center;
padding: 2rem;
opacity: 0.7;
}
.players-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
}
.player-admin-card {
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.player-info h4 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
}
.player-details {
display: flex;
gap: 1rem;
font-size: 0.9rem;
opacity: 0.9;
margin-bottom: 0.5rem;
}
.player-tokens {
display: flex;
gap: 0.75rem;
font-size: 1rem;
}
.kick-btn {
padding: 0.5rem 1rem;
background: rgba(244, 67, 54, 0.8);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
}
.kick-btn:hover {
background: rgba(244, 67, 54, 1);
transform: translateY(-1px);
}
.rooms-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem;
}
.room-card {
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 1rem;
}
.room-card h4 {
margin: 0 0 0.5rem 0;
}
.room-details {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.9rem;
opacity: 0.9;
}
</style>