credits mejorado, ahora se acepta feedback en la app

This commit is contained in:
2025-08-28 20:08:20 -06:00
parent c97778fdb1
commit 8a937e5e24
5 changed files with 395 additions and 1 deletions

View File

@@ -27,18 +27,158 @@
<p class="note"> <p class="note">
Gracias por jugar y contribuir a la comunidad. Si querés colaborar, difundir o proponer mejoras, ¡escribinos! Gracias por jugar y contribuir a la comunidad. Si querés colaborar, difundir o proponer mejoras, ¡escribinos!
</p> </p>
<!-- Sección de mensajes -->
<div class="messages-section">
<h2>Mensajes de la comunidad</h2>
<div class="message-input-group">
<textarea
v-model="newMessage"
placeholder="Deja tu mensaje, sugerencia o comentario..."
class="message-input"
maxlength="500"
rows="3"
></textarea>
<div class="input-actions">
<span class="char-count">{{ newMessage.length }}/500</span>
<button
@click="submitMessage"
:disabled="!newMessage.trim() || isSubmitting"
class="btn-submit"
>
{{ isSubmitting ? 'Enviando...' : 'Enviar mensaje' }}
</button>
</div>
</div>
<div v-if="messages.length > 0" class="messages-list">
<div class="messages-header">
<h3>Mensajes recibidos ({{ messages.length }})</h3>
<button @click="downloadMessages" class="btn-download">
📥 Descargar .md
</button>
</div>
<div class="messages-container">
<div
v-for="(message, index) in messages"
:key="message.id"
class="message-item"
>
<div class="message-meta">
<span class="message-number">#{{ messages.length - index }}</span>
<span class="message-date">{{ formatDate(message.timestamp) }}</span>
</div>
<div class="message-content">{{ message.content }}</div>
</div>
</div>
</div>
<div v-else class="no-messages">
No hay mensajes todavía. ¡ el primero en dejar uno!
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
interface Message {
id: string;
content: string;
timestamp: number;
}
const router = useRouter(); const router = useRouter();
const newMessage = ref('');
const messages = ref<Message[]>([]);
const isSubmitting = ref(false);
function goBack() { function goBack() {
if (window.history.length > 1) router.back(); if (window.history.length > 1) router.back();
else router.push('/'); else router.push('/');
} }
async function submitMessage() {
if (!newMessage.value.trim() || isSubmitting.value) return;
isSubmitting.value = true;
try {
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3000/api'}/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: newMessage.value.trim() })
});
if (response.ok) {
newMessage.value = '';
await loadMessages(); // Recargar mensajes
} else {
console.error('Failed to submit message');
}
} catch (error) {
console.error('Error submitting message:', error);
} finally {
isSubmitting.value = false;
}
}
async function loadMessages() {
try {
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3000/api'}/messages`);
if (response.ok) {
const data = await response.json();
messages.value = data.messages || [];
}
} catch (error) {
console.error('Error loading messages:', error);
messages.value = [];
}
}
function formatDate(timestamp: number): string {
return new Date(timestamp).toLocaleString('es-AR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
function downloadMessages() {
if (messages.value.length === 0) return;
let markdown = '# Mensajes de la comunidad - SnatchGame\n\n';
markdown += `Generado el: ${new Date().toLocaleString('es-AR')}\n`;
markdown += `Total de mensajes: ${messages.value.length}\n\n---\n\n`;
// Ordenar mensajes por timestamp (más recientes primero)
const sortedMessages = [...messages.value].sort((a, b) => b.timestamp - a.timestamp);
sortedMessages.forEach((message, index) => {
markdown += `## Mensaje #${sortedMessages.length - index}\n\n`;
markdown += `**Fecha:** ${formatDate(message.timestamp)}\n\n`;
markdown += `${message.content}\n\n---\n\n`;
});
// Crear y descargar el archivo
const blob = new Blob([markdown], { type: 'text/markdown;charset=utf-8' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `snatchgame-mensajes-${new Date().toISOString().split('T')[0]}.md`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
onMounted(() => {
loadMessages();
});
</script> </script>
<style scoped> <style scoped>
@@ -69,9 +209,170 @@ function goBack() {
.list li { margin: 6px 0; } .list li { margin: 6px 0; }
.note { margin-top: 12px; font-size: 14px; color: #475569; } .note { margin-top: 12px; font-size: 14px; color: #475569; }
/* Messages section styles */
.messages-section {
margin-top: 32px;
border-top: 1px solid rgba(0,0,0,0.1);
padding-top: 24px;
}
.messages-section h2 {
color: #334155;
margin-bottom: 16px;
font-size: 18px;
}
.message-input-group {
margin-bottom: 24px;
}
.message-input {
width: 100%;
padding: 12px;
border: 2px solid rgba(0,0,0,0.1);
border-radius: 8px;
resize: vertical;
font-family: inherit;
font-size: 14px;
line-height: 1.4;
background: rgba(255,255,255,0.8);
}
.message-input:focus {
outline: none;
border-color: #667eea;
}
.input-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8px;
}
.char-count {
font-size: 12px;
color: #64748b;
}
.btn-submit {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-submit:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-submit:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.messages-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.messages-header h3 {
margin: 0;
color: #334155;
font-size: 16px;
}
.btn-download {
background: rgba(255,255,255,0.6);
border: 1px solid rgba(0,0,0,0.1);
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
color: #334155;
transition: all 0.2s;
}
.btn-download:hover {
background: rgba(255,255,255,0.8);
transform: translateY(-1px);
}
.messages-container {
max-height: 400px;
overflow-y: auto;
border: 1px solid rgba(0,0,0,0.1);
border-radius: 8px;
background: rgba(255,255,255,0.4);
}
.message-item {
padding: 12px;
border-bottom: 1px solid rgba(0,0,0,0.05);
}
.message-item:last-child {
border-bottom: none;
}
.message-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
font-size: 12px;
color: #64748b;
}
.message-number {
font-weight: 600;
color: #667eea;
}
.message-content {
color: #334155;
line-height: 1.4;
white-space: pre-wrap;
}
.no-messages {
text-align: center;
color: #64748b;
font-style: italic;
padding: 24px;
background: rgba(255,255,255,0.3);
border-radius: 8px;
border: 1px dashed rgba(0,0,0,0.1);
}
@media (max-width: 640px) { @media (max-width: 640px) {
.card { padding: 18px; } .card { padding: 18px; }
.header h1 { font-size: 18px; } .header h1 { font-size: 18px; }
.messages-header {
flex-direction: column;
gap: 8px;
align-items: stretch;
}
.input-actions {
flex-direction: column;
gap: 8px;
align-items: stretch;
}
.btn-submit {
width: 100%;
}
} }
</style> </style>

View File

@@ -82,6 +82,9 @@
<div class="icon"></div> <div class="icon"></div>
<div class="title">Juego en pausa</div> <div class="title">Juego en pausa</div>
<div class="hint">Esperando a que ambos jugadores estén conectados</div> <div class="hint">Esperando a que ambos jugadores estén conectados</div>
<div class="actions">
<button class="btn btn-leave" @click="leaveGame">Salir del Juego</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -489,4 +492,5 @@ async function leaveGame() {
.pause-box .icon { font-size: 48px; margin-bottom: 8px; } .pause-box .icon { font-size: 48px; margin-bottom: 8px; }
.pause-box .title { font-weight: 800; font-size: 20px; } .pause-box .title { font-weight: 800; font-size: 20px; }
.pause-box .hint { margin-top: 6px; color:#666; font-size: 14px; } .pause-box .hint { margin-top: 6px; color:#666; font-size: 14px; }
.pause-box .actions { margin-top: 16px; }
</style> </style>

View File

@@ -47,6 +47,9 @@
<span class="pause-icon"></span> <span class="pause-icon"></span>
<h2>Game Paused</h2> <h2>Game Paused</h2>
<p>Waiting for players to reconnect...</p> <p>Waiting for players to reconnect...</p>
<div class="actions">
<button @click="leaveGame" class="btn btn-leave">Leave Game</button>
</div>
</div> </div>
</div> </div>

View File

@@ -31,6 +31,16 @@
</h1> </h1>
<div class="subtitle">Arena de intercambio social</div> <div class="subtitle">Arena de intercambio social</div>
<!-- Error notification for reconnection issues -->
<div v-if="reconnectionError" class="error-notification">
<div class="error-icon"></div>
<div class="error-content">
<div class="error-title">No se pudo reconectar a la partida</div>
<div class="error-message">{{ reconnectionErrorMessage }}</div>
</div>
<button @click="dismissError" class="btn-dismiss-error"></button>
</div>
<div class="player-section"> <div class="player-section">
<div class="name-input-group" v-if="!nameConfirmed || editingName"> <div class="name-input-group" v-if="!nameConfirmed || editingName">
<input <input
@@ -107,6 +117,10 @@ const inputName = ref('');
const isJoining = ref(false); const isJoining = ref(false);
const colorInput = ref('#667eea'); const colorInput = ref('#667eea');
const qrCanvas = ref<HTMLCanvasElement>(); const qrCanvas = ref<HTMLCanvasElement>();
// Reconnection error state
const reconnectionError = ref(false);
const reconnectionErrorMessage = ref('');
const showInstallBanner = ref(false); const showInstallBanner = ref(false);
const canPromptInstall = ref(false); const canPromptInstall = ref(false);
let deferredPrompt: any = null; let deferredPrompt: any = null;
@@ -204,8 +218,18 @@ onMounted(async () => {
colyseusService.lobbyRoom.value = null; colyseusService.lobbyRoom.value = null;
} }
await router.push(`/${routeUuid.value}/demo`); await router.push(`/${routeUuid.value}/demo`);
} catch (error) { } catch (error: any) {
console.error('Reconnection failed:', error); console.error('Reconnection failed:', error);
// Check if it's a reconnection token error
const errorMessage = error?.message || String(error);
if (errorMessage.includes('reconnection token invalid') || errorMessage.includes('expired')) {
reconnectionError.value = true;
reconnectionErrorMessage.value = 'Otro jugador ya está usando este UUID. Si eres tú en otro dispositivo, cierra esa sesión primero.';
} else {
reconnectionError.value = true;
reconnectionErrorMessage.value = 'Error al reconectar: ' + errorMessage;
}
} }
}); });
@@ -447,6 +471,11 @@ function goToSelector() {
router.push('/'); router.push('/');
} }
function dismissError() {
reconnectionError.value = false;
reconnectionErrorMessage.value = '';
}
async function triggerInstall() { async function triggerInstall() {
try { try {
if (!deferredPrompt) return; if (!deferredPrompt) return;
@@ -591,6 +620,56 @@ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
font-size: 1.2rem; font-size: 1.2rem;
} }
/* Error notification styles */
.error-notification {
display: flex;
align-items: center;
gap: 12px;
margin: 20px 0;
padding: 16px;
background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
border: 1px solid #fca5a5;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.15);
}
.error-icon {
font-size: 24px;
flex-shrink: 0;
}
.error-content {
flex: 1;
}
.error-title {
font-weight: 700;
color: #991b1b;
margin-bottom: 4px;
}
.error-message {
color: #dc2626;
font-size: 14px;
line-height: 1.4;
}
.btn-dismiss-error {
background: none;
border: none;
font-size: 18px;
color: #dc2626;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: background-color 0.2s;
flex-shrink: 0;
}
.btn-dismiss-error:hover {
background: rgba(220, 38, 38, 0.1);
}
.player-section { .player-section {
margin: 30px 0; margin: 30px 0;
padding: 20px; padding: 20px;

View File

@@ -37,6 +37,9 @@
<button @click="goToLeaderboard" class="qa-btn"> <button @click="goToLeaderboard" class="qa-btn">
📈 Estadísticas 📈 Estadísticas
</button> </button>
<button @click="goToCredits" class="qa-btn">
👨💻 Créditos
</button>
</div> </div>
<div class="search-container"> <div class="search-container">
@@ -353,6 +356,10 @@ function goToLeaderboard() {
router.push('/leaderboard'); router.push('/leaderboard');
} }
function goToCredits() {
router.push('/credits');
}
// Context menu functions // Context menu functions
function showContextMenu(event: MouseEvent, uuidInfo: UuidInfo) { function showContextMenu(event: MouseEvent, uuidInfo: UuidInfo) {
contextMenu.value = { contextMenu.value = {