Files
snatchgame/client/src/views/UuidSelector.vue

915 lines
20 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="uuid-selector-container">
<div class="selector-card">
<div class="header">
<h1 class="title">🎮 Snatch Game</h1>
<p class="subtitle">Selecciona tu UUID para continuar</p>
</div>
<div class="search-container">
<input
v-model="searchQuery"
type="text"
placeholder="Buscar UUID o nombre..."
class="search-input"
@input="filterUuids"
/>
<span class="search-icon">🔍</span>
</div>
<!-- Loading State -->
<div v-if="loading" class="loading">
<div class="spinner"></div>
<p>Cargando UUIDs...</p>
</div>
<!-- Loaded Content -->
<div v-else>
<div class="uuids-grid">
<div
v-for="(uuidInfo, index) in filteredUuids"
:key="uuidInfo.uuid"
class="uuid-card"
:class="{ 'has-name': uuidInfo.hasName }"
@click="selectUuid(uuidInfo.uuid)"
@contextmenu.prevent="showContextMenu($event, uuidInfo)"
:style="{ animationDelay: `${index * 0.02}s` }"
>
<div class="uuid-number">{{ getUuidIndex(uuidInfo.uuid) }}</div>
<div v-if="uuidInfo.name" class="player-name">{{ uuidInfo.name }}</div>
<div class="uuid-text">{{ formatUuid(uuidInfo.uuid) }}</div>
</div>
</div>
<div class="quick-actions">
<button @click="selectRandom" class="btn-random">
🎲 Seleccionar Aleatorio
</button>
<button @click="goToDashboard" class="btn-dashboard">
🎛 Dashboard Admin
</button>
</div>
</div>
<!-- Context Menu -->
<div
v-if="contextMenu.visible"
class="context-menu"
:style="{ top: contextMenu.y + 'px', left: contextMenu.x + 'px' }"
@click="contextMenu.visible = false"
>
<div class="context-menu-item" @click="printQR(contextMenu.uuid)">
🖨 Imprimir QR
</div>
</div>
<!-- QR Print Modal -->
<div v-if="printModal.visible" class="print-modal-overlay" @click="closePrintModal">
<div class="print-modal-content" @click.stop>
<button class="close-button" @click="closePrintModal"></button>
<div class="print-container" ref="printContainer">
<div class="qr-print-page">
<div class="qr-header">
<h2>🎮 Snatch Game</h2>
<p class="player-info">{{ printModal.name || 'Jugador' }}</p>
</div>
<div class="qr-code-container">
<canvas ref="qrCanvas"></canvas>
</div>
<div class="qr-footer">
<p class="uuid-display">UUID: {{ printModal.uuidShort }}</p>
<p class="url-display">{{ printModal.url }}</p>
<div class="instructions">
<p>📱 Escanea este código QR</p>
<p>para acceder al juego</p>
</div>
</div>
</div>
</div>
<div class="modal-buttons">
<button class="btn-download" @click="downloadPNG">📥 Descargar PNG</button>
<button class="btn-print" @click="executePrint">🖨 Imprimir</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, nextTick } from 'vue';
import { useRouter } from 'vue-router';
import QRCode from 'qrcode';
import html2canvas from 'html2canvas';
interface UuidInfo {
uuid: string;
name: string | null;
hasName: boolean;
}
const router = useRouter();
const loading = ref(true);
const allUuids = ref<UuidInfo[]>([]);
const searchQuery = ref('');
const filteredUuids = ref<UuidInfo[]>([]);
const qrCanvas = ref<HTMLCanvasElement>();
const printContainer = ref<HTMLElement>();
// Context menu state
const contextMenu = ref({
visible: false,
x: 0,
y: 0,
uuid: null as UuidInfo | null
});
// Print modal state
const printModal = ref({
visible: false,
uuid: '',
name: '',
uuidShort: '',
url: ''
});
onMounted(async () => {
try {
console.log('Loading UUIDs with names...');
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3000/api'}/admin/uuids-with-names`);
if (!response.ok) {
console.warn('Failed to fetch uuids-with-names, trying fallback...');
// Fallback to regular UUIDs endpoint
const fallbackResponse = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3000/api'}/admin/uuids`);
const fallbackData = await fallbackResponse.json();
allUuids.value = (fallbackData.uuids || []).map((uuid: string) => ({
uuid,
name: null,
hasName: false
}));
} else {
const data = await response.json();
allUuids.value = data.uuids || [];
}
filteredUuids.value = allUuids.value;
console.log(`Loaded ${allUuids.value.length} UUIDs`);
} catch (error) {
console.error('Failed to load UUIDs:', error);
allUuids.value = [];
} finally {
loading.value = false;
}
});
function filterUuids() {
const query = searchQuery.value.toLowerCase();
if (!query) {
filteredUuids.value = allUuids.value; // Show all when no search
return;
}
filteredUuids.value = allUuids.value.filter(uuidInfo =>
uuidInfo.uuid.toLowerCase().includes(query) ||
(uuidInfo.name && uuidInfo.name.toLowerCase().includes(query)) ||
getUuidIndex(uuidInfo.uuid).toString().includes(query)
);
}
function getUuidIndex(uuid: string): number {
return allUuids.value.findIndex(u => u.uuid === uuid) + 1;
}
function formatUuid(uuid: string): string {
// Show first 8 chars for better readability
return uuid.substring(0, 8) + '...';
}
function selectUuid(uuid: string) {
router.push(`/${uuid}`);
}
function selectRandom() {
if (allUuids.value.length > 0) {
const randomUuidInfo = allUuids.value[Math.floor(Math.random() * allUuids.value.length)];
selectUuid(randomUuidInfo.uuid);
}
}
function goToDashboard() {
router.push('/dashboard');
}
// Context menu functions
function showContextMenu(event: MouseEvent, uuidInfo: UuidInfo) {
contextMenu.value = {
visible: true,
x: event.clientX,
y: event.clientY,
uuid: uuidInfo
};
}
// Hide context menu when clicking elsewhere
document.addEventListener('click', () => {
contextMenu.value.visible = false;
});
// QR Print functions
async function printQR(uuidInfo: UuidInfo | null) {
if (!uuidInfo) return;
contextMenu.value.visible = false;
// Get the base URL (production or development)
const baseUrl = window.location.origin;
const fullUrl = `${baseUrl}/${uuidInfo.uuid}`;
printModal.value = {
visible: true,
uuid: uuidInfo.uuid,
name: uuidInfo.name || '',
uuidShort: uuidInfo.uuid.substring(0, 8),
url: fullUrl
};
// Wait for modal to render
await nextTick();
// Generate QR code
if (qrCanvas.value) {
await QRCode.toCanvas(qrCanvas.value, fullUrl, {
width: 256,
margin: 2,
color: {
dark: '#000000',
light: '#FFFFFF'
}
});
}
}
function closePrintModal() {
printModal.value.visible = false;
}
function executePrint() {
if (!printContainer.value) return;
const printContent = printContainer.value.innerHTML;
const printWindow = window.open('', '_blank', 'width=400,height=600');
if (!printWindow) return;
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>QR Code - ${printModal.value.name || printModal.value.uuidShort}</title>
<style>
@page {
size: portrait;
margin: 0;
}
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.qr-print-page {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 40px 20px;
box-sizing: border-box;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.qr-header {
text-align: center;
}
.qr-header h2 {
font-size: 36px;
margin: 0 0 10px 0;
}
.player-info {
font-size: 24px;
font-weight: bold;
margin: 0;
}
.qr-code-container {
background: white;
padding: 20px;
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}
.qr-code-container canvas {
display: block;
}
.qr-footer {
text-align: center;
}
.uuid-display {
font-family: monospace;
font-size: 14px;
margin: 10px 0;
opacity: 0.9;
}
.url-display {
font-size: 12px;
opacity: 0.7;
margin: 5px 0;
}
.instructions {
margin-top: 20px;
font-size: 18px;
}
.instructions p {
margin: 5px 0;
}
</style>
</head>
<body>
${printContent}
</body>
</html>
`);
printWindow.document.close();
printWindow.focus();
// Add canvas to print window
const printCanvas = printWindow.document.querySelector('canvas');
if (printCanvas && qrCanvas.value) {
const ctx = printCanvas.getContext('2d');
if (ctx) {
printCanvas.width = qrCanvas.value.width;
printCanvas.height = qrCanvas.value.height;
ctx.drawImage(qrCanvas.value, 0, 0);
}
}
setTimeout(() => {
printWindow.print();
printWindow.close();
}, 250);
}
async function downloadPNG() {
if (!printContainer.value || !qrCanvas.value) return;
try {
// Create a dedicated container for PNG generation
const pngContainer = document.createElement('div');
pngContainer.style.position = 'absolute';
pngContainer.style.left = '-9999px';
pngContainer.style.top = '0';
pngContainer.style.width = '400px';
pngContainer.style.height = '600px';
// Create the QR card HTML with inline styles
const qrDataURL = qrCanvas.value.toDataURL('image/png');
pngContainer.innerHTML = `
<div style="
width: 400px;
height: 600px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 40px 20px;
box-sizing: border-box;
color: white;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
">
<div style="text-align: center;">
<h2 style="
font-size: 36px;
margin: 0 0 10px 0;
font-weight: bold;
">🎮 Snatch Game</h2>
<p style="
font-size: 24px;
font-weight: bold;
margin: 0;
">${printModal.value.name || 'Jugador'}</p>
</div>
<div style="
background: white;
padding: 20px;
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
">
<img src="${qrDataURL}" style="
display: block;
width: 256px;
height: 256px;
" />
</div>
<div style="text-align: center;">
<p style="
font-family: monospace;
font-size: 14px;
margin: 10px 0;
opacity: 0.9;
">UUID: ${printModal.value.uuidShort}</p>
<p style="
font-size: 12px;
opacity: 0.7;
margin: 5px 0;
">${printModal.value.url}</p>
<div style="margin-top: 20px;">
<p style="
margin: 5px 0;
font-size: 18px;
">📱 Escanea este código QR</p>
<p style="
margin: 5px 0;
font-size: 18px;
">para acceder al juego</p>
</div>
</div>
</div>
`;
document.body.appendChild(pngContainer);
// Capture with html2canvas
const canvas = await html2canvas(pngContainer, {
width: 400,
height: 600,
scale: 2,
backgroundColor: null,
useCORS: true,
allowTaint: false,
logging: false,
removeContainer: false
});
// Remove the temporary container
document.body.removeChild(pngContainer);
// Create download link
const link = document.createElement('a');
link.download = `qr-code-${printModal.value.name || printModal.value.uuidShort}.png`;
link.href = canvas.toDataURL('image/png', 1.0);
// Trigger download
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} catch (error) {
console.error('Error generating PNG:', error);
alert('Error al generar la imagen PNG');
}
}
</script>
<style scoped>
.uuid-selector-container {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.selector-card {
background: white;
border-radius: 20px;
padding: 40px;
max-width: 1200px;
width: 100%;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
animation: slideUp 0.5s ease-out;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.header {
text-align: center;
margin-bottom: 30px;
}
.title {
font-size: 3rem;
margin: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: bold;
}
.subtitle {
color: #666;
font-size: 1.2rem;
margin-top: 10px;
}
.search-container {
position: relative;
margin-bottom: 30px;
}
.search-input {
width: 100%;
padding: 15px 50px 15px 20px;
font-size: 16px;
border: 2px solid #e0e0e0;
border-radius: 12px;
transition: all 0.3s;
outline: none;
}
.search-input:focus {
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.search-icon {
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
font-size: 20px;
opacity: 0.6;
}
.uuids-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 30px;
max-height: 500px;
overflow-y: auto;
padding: 10px;
}
.uuids-grid::-webkit-scrollbar {
width: 8px;
}
.uuids-grid::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.uuids-grid::-webkit-scrollbar-thumb {
background: #888;
border-radius: 10px;
}
.uuids-grid::-webkit-scrollbar-thumb:hover {
background: #555;
}
.uuid-card {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 20px;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s;
animation: fadeIn 0.5s ease-out backwards;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
.uuid-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.15);
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.uuid-card:hover .uuid-number,
.uuid-card:hover .uuid-text,
.uuid-card:hover .player-name {
color: white;
}
.uuid-card.has-name {
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
border: 2px solid rgba(102, 126, 234, 0.3);
}
.uuid-card.has-name:hover {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-color: transparent;
}
.uuid-number {
font-size: 24px;
font-weight: bold;
color: #667eea;
transition: color 0.3s;
}
.player-name {
font-size: 16px;
font-weight: 600;
color: #333;
transition: color 0.3s;
padding: 4px 8px;
background: rgba(255, 255, 255, 0.5);
border-radius: 8px;
margin: 4px 0;
}
.uuid-text {
font-family: 'Monaco', 'Menlo', monospace;
font-size: 12px;
color: #666;
transition: color 0.3s;
}
.loading {
text-align: center;
padding: 60px;
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.quick-actions {
display: flex;
gap: 15px;
justify-content: center;
}
.btn-random,
.btn-dashboard {
padding: 12px 30px;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-random {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
}
.btn-random:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(240, 147, 251, 0.4);
}
.btn-dashboard {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
.btn-dashboard:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(79, 172, 254, 0.4);
}
/* Context Menu */
.context-menu {
position: fixed;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 8px 0;
z-index: 1000;
min-width: 150px;
}
.context-menu-item {
padding: 10px 16px;
cursor: pointer;
transition: background 0.2s;
color: #333;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
}
.context-menu-item:hover {
background: #f5f5f5;
}
/* Print Modal */
.print-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
animation: fadeIn 0.3s ease-out;
}
.print-modal-content {
background: white;
border-radius: 20px;
padding: 30px;
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
position: relative;
animation: slideUp 0.3s ease-out;
}
.close-button {
position: absolute;
top: 15px;
right: 15px;
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
transition: color 0.2s;
}
.close-button:hover {
color: #333;
}
.print-container {
margin: 20px 0;
}
.qr-print-page {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 30px;
border-radius: 15px;
color: white;
text-align: center;
}
.qr-header h2 {
margin: 0 0 10px 0;
font-size: 28px;
}
.player-info {
font-size: 20px;
font-weight: bold;
margin: 0 0 20px 0;
}
.qr-code-container {
background: white;
padding: 20px;
border-radius: 15px;
display: inline-block;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
margin: 20px 0;
}
.qr-code-container canvas {
display: block;
}
.qr-footer {
margin-top: 20px;
}
.uuid-display {
font-family: monospace;
font-size: 12px;
opacity: 0.9;
margin: 10px 0;
}
.url-display {
font-size: 11px;
opacity: 0.7;
margin: 5px 0;
}
.instructions {
margin-top: 15px;
}
.instructions p {
margin: 5px 0;
font-size: 14px;
}
.modal-buttons {
display: flex;
gap: 10px;
margin-top: 20px;
}
.btn-download,
.btn-print {
flex: 1;
padding: 12px;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-download {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
.btn-download:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(79, 172, 254, 0.4);
}
.btn-print {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-print:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
/* Responsive */
@media (max-width: 768px) {
.selector-card {
padding: 20px;
}
.title {
font-size: 2rem;
}
.uuids-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
.quick-actions {
flex-direction: column;
}
.btn-random,
.btn-dashboard {
width: 100%;
}
.print-modal-content {
width: 95%;
padding: 20px;
}
.modal-buttons {
flex-direction: column;
}
}
</style>