diff --git a/client/src/components/EventChart.vue b/client/src/components/EventChart.vue index 9c0b481..faa57f9 100644 --- a/client/src/components/EventChart.vue +++ b/client/src/components/EventChart.vue @@ -2,7 +2,9 @@

Eventos y comparación

Cargando datos…
-
+ + +
Basado en mensajes disponibles por sala. Click jugador para comparar.
+ + +
+
+
+
+
+ {{ eventStyles[action]?.icon || '📊' }} + {{ group.labels[actionIndex] }} + {{ group.values[actionIndex] }} ({{ Math.round(group.percentages[actionIndex]) }}%) +
+
+
+
+
+ {{ selectedPlayerUuid ? 'Ratios del jugador seleccionado' : 'Ratios globales' }}. + Los segmentos muestran la proporción relativa dentro de cada categoría. +
+
@@ -56,7 +99,7 @@ interface Props { playerEventCounts: Record; selectedPlayerUuid?: string; playerBarGradient: string; - showPercent: boolean; + viewMode: 'count' | 'percent' | 'ratio'; loading?: boolean; } @@ -67,6 +110,54 @@ const props = withDefaults(defineProps(), { const highlighted = ref(''); +// Define ratio groups for superposed view +const ratioGroups = [ + { + name: 'Ofertas', + actions: ['p1_propose', 'p1_no_offer'], + labels: ['Ofrecer', 'No Ofrecer'] + }, + { + name: 'Respuestas', + actions: ['p2_accept', 'p2_reject', 'p2_snatch'], + labels: ['Aceptar', 'Rechazar', 'Robar'] + }, + { + name: 'Fuerzas', + actions: ['p2_force', 'p2_no_force'], + labels: ['Forzar', 'No Forzar'] + }, + { + name: 'Vergüenzas', + actions: ['p1_shame', 'p1_no_shame'], + labels: ['Asignar', 'No Asignar'] + }, + { + name: 'Denuncias', + actions: ['p1_report', 'p1_no_report'], + labels: ['Denunciar', 'No Denunciar'] + } +]; + +// Compute ratio data for each group +const ratioData = computed(() => { + return ratioGroups.map(group => { + const counts = props.selectedPlayerUuid + ? props.playerEventCounts + : props.globalEventCounts; + + const values = group.actions.map(action => counts[action] || 0); + const total = values.reduce((sum, val) => sum + val, 0); + + return { + ...group, + values, + total, + percentages: total > 0 ? values.map(val => (val / total) * 100) : values.map(() => 0) + }; + }); +}); + // Global calculations const globalMax = computed(() => { const vals = props.eventTypes.map(k => props.globalEventCounts[k] || 0); @@ -80,12 +171,12 @@ const globalTotal = computed(() => function globalBarWidth(eventType: string) { const v = props.globalEventCounts[eventType] || 0; - return Math.round((v / (props.showPercent ? globalTotal.value : globalMax.value)) * 100); + return Math.round((v / (props.viewMode === 'percent' ? globalTotal.value : globalMax.value)) * 100); } function globalValueLabel(eventType: string) { const v = props.globalEventCounts[eventType] || 0; - return props.showPercent ? `${Math.round((v / globalTotal.value) * 100)}%` : String(v); + return props.viewMode === 'percent' ? `${Math.round((v / globalTotal.value) * 100)}%` : String(v); } // Player calculations @@ -101,12 +192,12 @@ const playerTotal = computed(() => function playerBarWidth(eventType: string) { const v = props.playerEventCounts[eventType] || 0; - return Math.round((v / (props.showPercent ? playerTotal.value : playerMax.value)) * 100); + return Math.round((v / (props.viewMode === 'percent' ? playerTotal.value : playerMax.value)) * 100); } function playerValueLabel(eventType: string) { const v = props.playerEventCounts[eventType] || 0; - return props.showPercent ? `${Math.round((v / playerTotal.value) * 100)}%` : String(v); + return props.viewMode === 'percent' ? `${Math.round((v / playerTotal.value) * 100)}%` : String(v); } // Styling helpers @@ -310,6 +401,101 @@ function friendlyEventName(eventType: string): string { color: #64748b; } +/* Ratio bars styles */ +.ratio-bars { + display: flex; + flex-direction: column; + gap: 16px; + flex: 1 1 auto; + min-height: 0; + padding: 8px 0; +} + +.ratio-group { + flex: 1 1 0; + min-height: 60px; + transition: transform .18s ease; +} + +.ratio-group.highlight { + transform: translateX(4px); +} + +.ratio-bar { + position: relative; + height: 100%; + background: linear-gradient(135deg, rgba(238,242,255,0.4) 0%, rgba(199,210,254,0.2) 100%); + border-radius: 12px; + overflow: hidden; + border: 1px solid rgba(199,210,254,0.3); + width: 100%; + display: flex; +} + +.ratio-segment { + height: 100%; + transition: all 0.6s cubic-bezier(.2,.7,.1,1); + backdrop-filter: blur(4px); + opacity: 0.8; +} + +.ratio-segment:first-child { + border-top-left-radius: 12px; + border-bottom-left-radius: 12px; +} + +.ratio-segment:last-child { + border-top-right-radius: 12px; + border-bottom-right-radius: 12px; +} + +.ratio-event-chip { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + display: flex; + align-items: center; + gap: 6px; + padding: 5px 10px; + border-radius: 999px; + border: 1px solid; + box-shadow: 0 4px 12px rgba(0,0,0,0.08), inset 0 1px 0 rgba(255,255,255,0.4); + backdrop-filter: blur(8px) saturate(120%); + -webkit-backdrop-filter: blur(8px) saturate(120%); + transition: all 0.3s ease; + white-space: nowrap; + max-width: 95%; +} + +.ratio-segment:hover .ratio-event-chip { + transform: translate(-50%, -50%) scale(1.05); + box-shadow: 0 6px 16px rgba(0,0,0,0.12), inset 0 1px 0 rgba(255,255,255,0.6); +} + +.ratio-icon { + font-size: 14px; +} + +.ratio-label { + color: #0f172a; + font-weight: 800; + font-size: 13px; + letter-spacing: .1px; +} + +.ratio-count { + color: #1f2937; + font-weight: 800; + background: rgba(255,255,255,0.7); + border: 1px solid rgba(229,231,235,0.5); + border-radius: 999px; + padding: 2px 6px; + font-size: 11px; + margin-left: 2px; + box-shadow: 0 1px 3px rgba(0,0,0,0.06); +} + /* Responsive chip sizing */ @media (min-width: 1200px) { .bar-chip { @@ -395,5 +581,83 @@ function friendlyEventName(eventType: string): string { font-size: 8px; margin-left: 1px; } + + /* Ratio responsive styles */ + .ratio-event-chip { + padding: 3px 6px; + gap: 3px; + } + + .ratio-icon { + font-size: 11px; + } + + .ratio-label { + font-size: 10px; + } + + .ratio-count { + font-size: 8px; + padding: 1px 3px; + } +} + +@media (max-width: 767px) { + .ratio-event-chip { + padding: 3px 7px; + gap: 4px; + } + + .ratio-icon { + font-size: 12px; + } + + .ratio-label { + font-size: 11px; + } + + .ratio-count { + font-size: 9px; + padding: 1px 4px; + } +} + +@media (min-width: 768px) and (max-width: 1199px) { + .ratio-event-chip { + padding: 4px 8px; + gap: 5px; + } + + .ratio-icon { + font-size: 13px; + } + + .ratio-label { + font-size: 12px; + } + + .ratio-count { + font-size: 10px; + } +} + +@media (min-width: 1200px) { + .ratio-event-chip { + padding: 6px 12px; + gap: 8px; + } + + .ratio-icon { + font-size: 16px; + } + + .ratio-label { + font-size: 14px; + } + + .ratio-count { + font-size: 12px; + padding: 3px 8px; + } } \ No newline at end of file diff --git a/client/src/views/Leaderboard.vue b/client/src/views/Leaderboard.vue index 1e81888..0697826 100644 --- a/client/src/views/Leaderboard.vue +++ b/client/src/views/Leaderboard.vue @@ -4,8 +4,8 @@

📈 Leaderboard

-
@@ -29,7 +29,14 @@ Jugador
- +
+ + +
-
@@ -60,7 +62,7 @@ :player-event-counts="playerEventCounts" :selected-player-uuid="selectedUuid" :player-bar-gradient="playerBarGradient" - :show-percent="showPercent" + :view-mode="viewMode" :loading="loading" /> @@ -78,6 +80,27 @@ interface RoomState { players?: any[]; systemMessages?: { kind: string }[] } const loading = ref(false); const eventFilters = useEventFilters(); + +// View mode cycling +type ViewMode = 'count' | 'percent' | 'ratio'; +const viewMode = ref('count'); + +const viewModeLabel = computed(() => { + switch (viewMode.value) { + case 'count': return 'Ver conteos'; + case 'percent': return 'Ver %'; + case 'ratio': return 'Ver superpuesto'; + default: return 'Ver conteos'; + } +}); + +function cycleViewMode() { + const modes: ViewMode[] = ['count', 'percent', 'ratio']; + const currentIndex = modes.indexOf(viewMode.value); + const nextIndex = (currentIndex + 1) % modes.length; + viewMode.value = modes[nextIndex]; +} + const EVENTS = [ 'p1_propose', 'p1_no_offer', 'p2_snatch', 'p2_accept', 'p2_force', 'p2_no_force', 'p2_reject', @@ -98,7 +121,6 @@ const EVENT_STYLES: Record { @@ -119,11 +141,25 @@ watch(search, () => { page.value = 1; }); const page = ref(1); -const pageSize = 20; -const pageCount = computed(() => Math.max(1, Math.ceil((playersFiltered.value.length || 0) / pageSize))); +const containerWidth = ref(1200); // Default width, will be updated dynamically +const dynamicPageSize = computed(() => { + // Estimate space needed per chip and controls + const chipWidth = 140; // Average chip width including gap + const clearBtnWidth = selectedUuid.value ? 150 : 0; // "Quitar selección" button + const searchWidth = 240; // Search input + const paginationWidth = pageCount.value > 1 ? 100 : 0; // Compact pagination + const margin = 40; // Container margins + + const availableWidth = containerWidth.value - searchWidth - paginationWidth - clearBtnWidth - margin; + const maxChips = Math.max(3, Math.floor(availableWidth / chipWidth)); // Minimum 3 chips + + return Math.min(maxChips, 15); // Maximum 15 chips per page +}); + +const pageCount = computed(() => Math.max(1, Math.ceil((playersFiltered.value.length || 0) / dynamicPageSize.value))); const playersPage = computed(() => { - const start = (page.value - 1) * pageSize; - return playersFiltered.value.slice(start, start + pageSize); + const start = (page.value - 1) * dynamicPageSize.value; + return playersFiltered.value.slice(start, start + dynamicPageSize.value); }); const selectedUuid = ref(''); @@ -368,12 +404,25 @@ async function loadPlayerHistory() { playerLoading.value = false; } +// Update container width on resize +function updateContainerWidth() { + containerWidth.value = window.innerWidth; +} + onMounted(() => { setupStreams(); // Initialize with aggregated data as default eventFilters.globalEventCounts.value = { ...eventFilters.globalEventCountsAggregated.value }; + + // Set initial container width and add resize listener + updateContainerWidth(); + window.addEventListener('resize', updateContainerWidth); +}); + +onUnmounted(() => { + closeStreams(); + window.removeEventListener('resize', updateContainerWidth); }); -onUnmounted(closeStreams); // Reset to first page when search changes or players list length shrinks below current page watch(() => search.value, () => { page.value = 1; }); @@ -406,10 +455,41 @@ const allPlayersActions = ref<{ uuid: string; name: string; total: number }[]>([ .key.player { background: linear-gradient(90deg, #a78bfa, #6366f1); box-shadow: 0 0 8px rgba(99,102,241,0.35); } .sep { opacity: 0.6; } -.player-chips { display:flex; align-items:flex-start; gap: 10px; flex-wrap: wrap; } -.search { padding:8px 10px; border:1px solid #cbd5e1; background:#fff; color:#0f172a; border-radius: 10px; min-width: 240px; outline:none; } -.search::placeholder { color:#64748b; } -.chips { display:flex; gap:10px; flex-wrap: wrap; } +.player-chips { + display: flex; + flex-direction: column; + gap: 12px; +} + +.search-controls { + display: flex; + align-items: center; + gap: 12px; + justify-content: space-between; +} + +.search { + padding: 8px 10px; + border: 1px solid #cbd5e1; + background: #fff; + color: #0f172a; + border-radius: 10px; + min-width: 240px; + outline: none; + flex: 1; + max-width: 300px; +} + +.search::placeholder { + color: #64748b; +} + +.chips { + display: flex; + gap: 10px; + flex-wrap: wrap; + align-items: center; +} .chip { display:flex; align-items:center; gap:8px; background: color-mix(in srgb, var(--primary) 6%, white); border:1px solid color-mix(in srgb, var(--primary) 24%, #e5e7eb); padding:8px 12px; border-radius: 999px; color:#111827; cursor:pointer; transition: transform .18s ease, background .18s ease, box-shadow .18s ease; } .chip:hover { transform: translateY(-1px); background: color-mix(in srgb, var(--primary) 10%, white); box-shadow: 0 6px 18px rgba(102,126,234,0.18); } .chip.active { background: color-mix(in srgb, var(--primary) 18%, white); border-color: color-mix(in srgb, var(--primary) 45%, #c7d2fe); box-shadow: 0 6px 22px rgba(99,102,241,0.22); } @@ -422,16 +502,21 @@ const allPlayersActions = ref<{ uuid: string; name: string; total: number }[]>([ .pagination { display: flex; align-items: center; - gap: 12px; - padding: 8px 12px; + gap: 8px; + padding: 6px 10px; background: rgba(255,255,255,0.82); border: 1px solid rgba(229,231,235,0.9); border-radius: 999px; - box-shadow: 0 6px 18px rgba(0,0,0,0.08), inset 0 1px 0 rgba(255,255,255,0.6); + box-shadow: 0 4px 12px rgba(0,0,0,0.08), inset 0 1px 0 rgba(255,255,255,0.6); backdrop-filter: blur(10px) saturate(120%); -webkit-backdrop-filter: blur(10px) saturate(120%); } +.pagination.compact { + gap: 6px; + padding: 4px 8px; +} + .pg-btn { width: 32px; height: 32px; @@ -449,11 +534,23 @@ const allPlayersActions = ref<{ uuid: string; name: string; total: number }[]>([ box-shadow: 0 4px 12px rgba(102,126,234,0.25); } +.pg-btn.compact { + width: 24px; + height: 24px; + font-size: 14px; + font-weight: 800; + box-shadow: 0 2px 8px rgba(102,126,234,0.2); +} + .pg-btn:hover:not(:disabled) { - transform: translateY(-2px); + transform: translateY(-1px); box-shadow: 0 6px 16px rgba(102,126,234,0.35); } +.pg-btn.compact:hover:not(:disabled) { + box-shadow: 0 4px 12px rgba(102,126,234,0.3); +} + .pg-btn:disabled { opacity: 0.5; cursor: not-allowed; @@ -463,23 +560,54 @@ const allPlayersActions = ref<{ uuid: string; name: string; total: number }[]>([ } .pg-ind { - font-weight: 700; + font-weight: 600; color: #334155; - font-size: 14px; - min-width: 60px; + font-size: 12px; + min-width: 35px; text-align: center; } -@media (max-width: 480px) { - .player-chips { +@media (max-width: 768px) { + .search-controls { flex-direction: column; + align-items: stretch; gap: 8px; } - .pagination { - width: 100%; + .search { + min-width: auto; + max-width: none; + } + + .pagination.compact { + align-self: center; + } +} + +@media (max-width: 480px) { + .player-chips { + gap: 8px; + } + + .chips { + gap: 8px; justify-content: center; } + + .chip { + padding: 6px 10px; + } + + .pg-btn.compact { + width: 20px; + height: 20px; + font-size: 12px; + } + + .pg-ind { + font-size: 11px; + min-width: 30px; + } }