feat(dashboard): comprehensive filter enhancement and UI layout fixes
- Add time range selector (1H/6H/24H/7D/All) with preference persistence - Implement smart X-axis label formatting for multi-day usage data - Standardize global component spacing and fix CSS @apply limitations - Add elegant empty state UI for charts when filtered data is absent - Update i18n translations for all new dashboard features
This commit is contained in:
@@ -144,9 +144,9 @@
|
||||
<!-- Usage Trend Chart -->
|
||||
<div class="view-card">
|
||||
<!-- Header with Stats and Filter -->
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-6">
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-6 mb-8">
|
||||
<div class="flex flex-wrap items-center gap-5">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
||||
class="w-4 h-4 text-neon-purple">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
@@ -157,40 +157,95 @@
|
||||
</div>
|
||||
|
||||
<!-- Usage Stats Pills -->
|
||||
<div class="flex flex-wrap gap-2 text-[10px] font-mono">
|
||||
<div class="px-2.5 py-1 rounded bg-space-800 border border-space-border/50 whitespace-nowrap">
|
||||
<div class="flex flex-wrap gap-2.5 text-[10px] font-mono">
|
||||
<div class="px-2.5 py-1 rounded bg-space-850 border border-space-border/60 whitespace-nowrap">
|
||||
<span class="text-gray-500" x-text="$store.global.t('totalColon')">Total:</span>
|
||||
<span class="text-white ml-1 font-bold" x-text="usageStats.total"></span>
|
||||
</div>
|
||||
<div class="px-2.5 py-1 rounded bg-space-800 border border-space-border/50 whitespace-nowrap">
|
||||
<div class="px-2.5 py-1 rounded bg-space-850 border border-space-border/60 whitespace-nowrap">
|
||||
<span class="text-gray-500" x-text="$store.global.t('todayColon')">Today:</span>
|
||||
<span class="text-neon-cyan ml-1 font-bold" x-text="usageStats.today"></span>
|
||||
</div>
|
||||
<div class="px-2.5 py-1 rounded bg-space-800 border border-space-border/50 whitespace-nowrap">
|
||||
<div class="px-2.5 py-1 rounded bg-space-850 border border-space-border/60 whitespace-nowrap">
|
||||
<span class="text-gray-500" x-text="$store.global.t('hour1Colon')">1H:</span>
|
||||
<span class="text-neon-green ml-1 font-bold" x-text="usageStats.thisHour"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 w-full sm:w-auto justify-end">
|
||||
<!-- Display Mode Toggle -->
|
||||
<div class="join">
|
||||
<button @click="setDisplayMode('family')"
|
||||
class="join-item btn btn-xs px-3 border-space-border/50 bg-space-800 text-gray-400 hover:text-white transition-all"
|
||||
:class="{'bg-neon-purple/20 text-neon-purple border-neon-purple/50': displayMode === 'family'}"
|
||||
x-text="$store.global.t('family')">
|
||||
<div class="flex items-center gap-3 w-full sm:w-auto justify-end flex-wrap">
|
||||
<!-- Time Range Dropdown -->
|
||||
<div class="relative">
|
||||
<button @click="showTimeRangeDropdown = !showTimeRangeDropdown; showDisplayModeDropdown = false; showModelFilter = false"
|
||||
class="flex items-center gap-2 px-3 py-1.5 text-[10px] font-mono text-gray-400 bg-space-800 border border-space-border/50 rounded hover:border-neon-cyan/50 transition-colors whitespace-nowrap">
|
||||
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span x-text="getTimeRangeLabel()"></span>
|
||||
<svg class="w-3 h-3 transition-transform" :class="{'rotate-180': showTimeRangeDropdown}" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<button @click="setDisplayMode('model')"
|
||||
class="join-item btn btn-xs px-3 border-space-border/50 bg-space-800 text-gray-400 hover:text-white transition-all"
|
||||
:class="{'bg-neon-purple/20 text-neon-purple border-neon-purple/50': displayMode === 'model'}"
|
||||
x-text="$store.global.t('model')">
|
||||
<div x-show="showTimeRangeDropdown" @click.outside="showTimeRangeDropdown = false"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95"
|
||||
class="absolute right-0 mt-1 w-36 bg-space-900 border border-space-border rounded-lg shadow-xl z-50 overflow-hidden py-1"
|
||||
style="display: none;">
|
||||
<button @click="setTimeRange('1h')" class="w-full px-3 py-1.5 text-left text-[10px] font-mono hover:bg-white/5 transition-colors"
|
||||
:class="timeRange === '1h' ? 'text-neon-cyan' : 'text-gray-400'"
|
||||
x-text="$store.global.t('last1Hour')"></button>
|
||||
<button @click="setTimeRange('6h')" class="w-full px-3 py-1.5 text-left text-[10px] font-mono hover:bg-white/5 transition-colors"
|
||||
:class="timeRange === '6h' ? 'text-neon-cyan' : 'text-gray-400'"
|
||||
x-text="$store.global.t('last6Hours')"></button>
|
||||
<button @click="setTimeRange('24h')" class="w-full px-3 py-1.5 text-left text-[10px] font-mono hover:bg-white/5 transition-colors"
|
||||
:class="timeRange === '24h' ? 'text-neon-cyan' : 'text-gray-400'"
|
||||
x-text="$store.global.t('last24Hours')"></button>
|
||||
<button @click="setTimeRange('7d')" class="w-full px-3 py-1.5 text-left text-[10px] font-mono hover:bg-white/5 transition-colors"
|
||||
:class="timeRange === '7d' ? 'text-neon-cyan' : 'text-gray-400'"
|
||||
x-text="$store.global.t('last7Days')"></button>
|
||||
<button @click="setTimeRange('all')" class="w-full px-3 py-1.5 text-left text-[10px] font-mono hover:bg-white/5 transition-colors"
|
||||
:class="timeRange === 'all' ? 'text-neon-cyan' : 'text-gray-400'"
|
||||
x-text="$store.global.t('allTime')"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Display Mode Dropdown -->
|
||||
<div class="relative">
|
||||
<button @click="showDisplayModeDropdown = !showDisplayModeDropdown; showTimeRangeDropdown = false; showModelFilter = false"
|
||||
class="flex items-center gap-2 px-3 py-1.5 text-[10px] font-mono text-gray-400 bg-space-800 border border-space-border/50 rounded hover:border-neon-purple/50 transition-colors whitespace-nowrap">
|
||||
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
<span x-text="displayMode === 'family' ? $store.global.t('family') : $store.global.t('model')"></span>
|
||||
<svg class="w-3 h-3 transition-transform" :class="{'rotate-180': showDisplayModeDropdown}" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<div x-show="showDisplayModeDropdown" @click.outside="showDisplayModeDropdown = false"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95"
|
||||
class="absolute right-0 mt-1 w-32 bg-space-900 border border-space-border rounded-lg shadow-xl z-50 overflow-hidden py-1"
|
||||
style="display: none;">
|
||||
<button @click="setDisplayMode('family')" class="w-full px-3 py-1.5 text-left text-[10px] font-mono hover:bg-white/5 transition-colors"
|
||||
:class="displayMode === 'family' ? 'text-neon-purple' : 'text-gray-400'"
|
||||
x-text="$store.global.t('family')"></button>
|
||||
<button @click="setDisplayMode('model')" class="w-full px-3 py-1.5 text-left text-[10px] font-mono hover:bg-white/5 transition-colors"
|
||||
:class="displayMode === 'model' ? 'text-neon-purple' : 'text-gray-400'"
|
||||
x-text="$store.global.t('model')"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Dropdown -->
|
||||
<div class="relative">
|
||||
<button @click="showModelFilter = !showModelFilter"
|
||||
<button @click="showModelFilter = !showModelFilter; showTimeRangeDropdown = false; showDisplayModeDropdown = false"
|
||||
class="flex items-center gap-2 px-3 py-1.5 text-[10px] font-mono text-gray-400 bg-space-800 border border-space-border/50 rounded hover:border-neon-purple/50 transition-colors whitespace-nowrap">
|
||||
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
@@ -293,7 +348,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Dynamic Legend -->
|
||||
<div class="flex flex-wrap gap-3 mb-3"
|
||||
<div class="flex flex-wrap gap-3 mb-5"
|
||||
x-show="displayMode === 'family' ? selectedFamilies.length > 0 : Object.values(selectedModels).flat().length > 0">
|
||||
<!-- Family Mode Legend -->
|
||||
<template x-if="displayMode === 'family'">
|
||||
@@ -322,7 +377,8 @@
|
||||
<!-- Chart -->
|
||||
<div class="h-48 w-full relative">
|
||||
<canvas id="usageTrendChart"></canvas>
|
||||
<!-- Loading/Empty State -->
|
||||
|
||||
<!-- Overall Loading State -->
|
||||
<div x-show="!stats.hasTrendData"
|
||||
class="absolute inset-0 flex items-center justify-center bg-space-900/50 backdrop-blur-sm z-10"
|
||||
style="display: none;">
|
||||
@@ -331,9 +387,29 @@
|
||||
<span x-text="$store.global.t('syncing')">SYNCING...</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- No Selection -->
|
||||
<div x-show="stats.hasTrendData && (displayMode === 'family' ? selectedFamilies.length === 0 : Object.values(selectedModels).flat().length === 0)"
|
||||
class="absolute inset-0 flex items-center justify-center bg-space-900/30 z-10">
|
||||
|
||||
<!-- Empty State (After Filtering) -->
|
||||
<div x-show="stats.hasTrendData && !hasFilteredTrendData"
|
||||
class="absolute inset-0 flex flex-col items-center justify-center bg-space-900/30 z-10"
|
||||
style="display: none;">
|
||||
<div class="flex flex-col items-center gap-4 animate-fade-in">
|
||||
<div class="w-12 h-12 rounded-full bg-space-850 flex items-center justify-center text-gray-600 border border-space-border/50">
|
||||
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-xs font-mono text-gray-500 text-center">
|
||||
<p x-text="$store.global.t('noDataTracked')">No data tracked yet</p>
|
||||
<p class="text-[10px] opacity-60 mt-1" x-text="'[' + getTimeRangeLabel() + ']'"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No Selection State -->
|
||||
<div x-show="stats.hasTrendData && hasFilteredTrendData && (displayMode === 'family' ? selectedFamilies.length === 0 : Object.values(selectedModels).flat().length === 0)"
|
||||
class="absolute inset-0 flex items-center justify-center bg-space-900/30 z-10"
|
||||
style="display: none;">
|
||||
<div class="text-xs font-mono text-gray-500"
|
||||
x-text="displayMode === 'family' ? $store.global.t('selectFamilies') : $store.global.t('selectModels')">
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user