From 4f4dd2d2f37b97155bc5c317787c31acec0aa1f4 Mon Sep 17 00:00:00 2001 From: josedario87 Date: Thu, 28 Aug 2025 04:05:59 -0600 Subject: [PATCH] mejora filtro por salas --- client/src/composables/useEventFilters.ts | 2 +- client/src/views/Leaderboard.vue | 169 +++++++++++++++++----- 2 files changed, 136 insertions(+), 35 deletions(-) diff --git a/client/src/composables/useEventFilters.ts b/client/src/composables/useEventFilters.ts index 8818450..2242750 100644 --- a/client/src/composables/useEventFilters.ts +++ b/client/src/composables/useEventFilters.ts @@ -97,7 +97,7 @@ export function useEventFilters() { const parts = []; if (roundFilter.value.length) parts.push(`Round ${roundFilter.value.join(',')}`); if (gameFilter.value.length) parts.push(`Game ${gameFilter.value.join(',')}`); - if (roomFilter.value.length) parts.push(`Rooms ${roomFilter.value.map(r => r.slice(0,8)).join(',')}`); + if (roomFilter.value.length) parts.push(`Rooms ${roomFilter.value.length}`); return parts.length > 0 ? parts.join(' + ') : 'Sin filtros'; }); diff --git a/client/src/views/Leaderboard.vue b/client/src/views/Leaderboard.vue index 833d1bf..57f85bd 100644 --- a/client/src/views/Leaderboard.vue +++ b/client/src/views/Leaderboard.vue @@ -72,31 +72,30 @@ - -
-
- - @@ -481,6 +480,46 @@ const roomsPage = computed(() => { const selectedUuids = ref([]); const selectedRoomIds = ref([]); +const roomSliceStart = ref(0); +const roomSliceEnd = ref(0); +const maxIndex = computed(() => Math.max(0, availableRooms.value.length - 1)); +const sliceInitialized = ref(false); +const roomSliceIds = computed(() => { + const a = Math.max(0, Math.min(roomSliceStart.value | 0, maxIndex.value)); + const b = Math.max(0, Math.min(roomSliceEnd.value | 0, maxIndex.value)); + const start = Math.min(a, b); + const end = Math.max(a, b); + return availableRooms.value.slice(start, end + 1).map(r => r.roomId); +}); +const sliceStartLabel = computed(() => roomSliceIds.value.length ? Math.min(roomSliceStart.value, roomSliceEnd.value) + 1 : 0); +const sliceEndLabel = computed(() => roomSliceIds.value.length ? Math.max(roomSliceStart.value, roomSliceEnd.value) + 1 : 0); +const startPct = computed(() => maxIndex.value > 0 ? (Math.min(roomSliceStart.value, roomSliceEnd.value) / maxIndex.value) * 100 : 0); +const endPct = computed(() => maxIndex.value > 0 ? (Math.max(roomSliceStart.value, roomSliceEnd.value) / maxIndex.value) * 100 : 0); +function onSliceStart() { + if (roomSliceStart.value > roomSliceEnd.value) roomSliceStart.value = roomSliceEnd.value; +} +function onSliceEnd() { + if (roomSliceEnd.value < roomSliceStart.value) roomSliceEnd.value = roomSliceStart.value; +} + +function selectRecent(count: number) { + const m = maxIndex.value; + if (m < 0) return; + const start = Math.max(0, m - (count - 1)); + roomSliceStart.value = start; + roomSliceEnd.value = m; +} + +function addRecent(count: number) { + const m = maxIndex.value; + if (m < 0) return; + // Current selection length anchored at end + const currentLen = roomSliceIds.value.length; + const newLen = Math.min(m + 1, Math.max(0, currentLen) + Math.max(1, count)); + const start = Math.max(0, m - (newLen - 1)); + roomSliceStart.value = start; + roomSliceEnd.value = m; +} const playerLoading = ref(false); const playerEventCounts = ref>((Object.fromEntries(EVENTS.map(k => [k, 0])) as Record)); const playersActionsByUuid = ref>>({}); @@ -521,11 +560,33 @@ function toggleRoom(roomId: string) { } function clearRooms() { + suppressSliceSync = true; selectedRoomIds.value = []; eventFilters.roomFilter.value = []; eventFilters.applyFilters(EVENTS); + // allow user to re-enable by moving sliders + requestAnimationFrame(() => { suppressSliceSync = false; }); } +// Keep event filters in sync with slice selection +let suppressSliceSync = false; +// Initialize full-range selection as soon as rooms are available (first time) +watch(maxIndex, (m) => { + if (m >= 0 && !sliceInitialized.value) { + roomSliceStart.value = 0; + roomSliceEnd.value = m; + selectedRoomIds.value = [...roomSliceIds.value]; + eventFilters.roomFilter.value = [...selectedRoomIds.value]; + sliceInitialized.value = true; + } +}); +watch([roomSliceStart, roomSliceEnd, () => availableRooms.value.length], () => { + if (suppressSliceSync) return; + selectedRoomIds.value = [...roomSliceIds.value]; + eventFilters.roomFilter.value = [...selectedRoomIds.value]; + eventFilters.applyFilters(EVENTS); +}); + function goHome() { router.push('/'); } @@ -687,20 +748,31 @@ function setupStreams() { playersActionsByUuid.value = byUuid; eventFilters.updateAggregatedData(allDetailedEvents, aggregatedCounts); - // Extract unique room IDs from aggregated events - this is our primary source for rooms + // Extract unique room IDs from aggregated events and track "newness" by last seen index const roomIds = new Set(); - allDetailedEvents.forEach(event => { - if (event.roomId && event.roomId.trim()) { - roomIds.add(event.roomId); - } + const lastSeenIndex: Record = {}; + allDetailedEvents.forEach((event, idx) => { + const rid = (event.roomId || '').trim(); + if (!rid) return; + roomIds.add(rid); + lastSeenIndex[rid] = idx; // increasing idx means newer }); - - // Build available rooms list from aggregated events - availableRooms.value = Array.from(roomIds).map(roomId => ({ - roomId, - name: `Sala ${roomId.slice(0, 8)}`, - playerCount: allDetailedEvents.filter(e => e.roomId === roomId).length - })); + + // Build available rooms list sorted by lastSeenIndex ASC (older first, newest get the highest index) + availableRooms.value = Array.from(roomIds) + .map(roomId => ({ + roomId, + name: `Sala ${roomId.slice(0, 8)}`, + playerCount: allDetailedEvents.filter(e => e.roomId === roomId).length, + _lastSeen: lastSeenIndex[roomId] ?? -1 + })) + .sort((a, b) => (a._lastSeen - b._lastSeen)) + .map(({ _lastSeen, ...rest }) => rest); + // Initialize slice window: full range by default, preserve user selection if present + if (availableRooms.value.length > 0 && selectedRoomIds.value.length === 0) { + roomSliceStart.value = 0; + roomSliceEnd.value = availableRooms.value.length - 1; + } // Compute metrics from score history computeMetricsFromScores(); @@ -938,6 +1010,35 @@ function downloadJSON() { border-top: 1px solid rgba(203, 213, 225, 0.5); } +/* Room slice controls */ +.room-slice { margin-top: 10px; padding-top: 12px; border-top: 1px solid rgba(203,213,225,0.5); display: flex; flex-direction: column; gap: 10px; } +.slice-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; } +.slice-info { display: flex; align-items: center; gap: 10px; color: #334155; font-weight: 600; } +.slice-summary { color: #475569; font-weight: 700; } + +/* Dual range slider */ +.dual-slider { position: relative; height: 36px; padding: 16px 8px; } +.dual-slider .track { position: absolute; left: 8px; right: 8px; top: 50%; height: 8px; transform: translateY(-50%); border-radius: 999px; background: linear-gradient(135deg, #e2e8f0 0%, #cbd5e1 100%); box-shadow: inset 0 1px 2px rgba(0,0,0,0.06); } +.dual-slider .highlight { position: absolute; top: 50%; height: 8px; transform: translateY(-50%); border-radius: 999px; background: linear-gradient(90deg, #06b6d4, #8b5cf6); box-shadow: 0 2px 8px rgba(139,92,246,0.25); } +.dual-slider .range { -webkit-appearance: none; appearance: none; position: absolute; left: 0; right: 0; top: 0; bottom: 0; background: transparent; pointer-events: none; } +.dual-slider .range::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 22px; height: 22px; background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); border: 2px solid #8b5cf6; border-radius: 50%; box-shadow: 0 4px 12px rgba(139,92,246,0.3); pointer-events: auto; cursor: pointer; } +.dual-slider .range::-moz-range-thumb { width: 22px; height: 22px; background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); border: 2px solid #8b5cf6; border-radius: 50%; box-shadow: 0 4px 12px rgba(139,92,246,0.3); pointer-events: auto; cursor: pointer; } +.dual-slider .range::-ms-thumb { width: 22px; height: 22px; background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); border: 2px solid #8b5cf6; border-radius: 50%; box-shadow: 0 4px 12px rgba(139,92,246,0.3); pointer-events: auto; cursor: pointer; } +.dual-slider .range.start { z-index: 2; } +.dual-slider .range.end { z-index: 3; } + +/* Quick select buttons: super compact and subtle */ +.quick-select { display: flex; justify-content: flex-end; gap: 6px; margin-top: 4px; } +.qs-btn { padding: 2px 6px; border-radius: 999px; border: 1px solid rgba(148,163,184,0.35); background: rgba(255,255,255,0.6); color: #475569; font-size: 11px; font-weight: 800; cursor: pointer; opacity: 0.85; transition: all 0.2s ease; } +.qs-btn:hover { opacity: 1; box-shadow: 0 2px 6px rgba(0,0,0,0.08); transform: translateY(-1px); } +.qs-btn:active { transform: translateY(0); } + +@media (max-width: 640px) { + .dual-slider { height: 32px; padding: 14px 6px; } + .dual-slider .track, .dual-slider .highlight { height: 6px; } + .dual-slider .range::-webkit-slider-thumb, .dual-slider .range::-moz-range-thumb { width: 18px; height: 18px; } +} + .search-controls { display: flex; align-items: center;