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;