credits mejorado, ahora se acepta feedback en la app
This commit is contained in:
@@ -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. ¡Sé 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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
Reference in New Issue
Block a user