@veristone/nuxt-v-app 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +42 -0
  2. package/app/app.vue +7 -0
  3. package/app/assets/css/v-app.css +313 -0
  4. package/app/components/V/A/Badge.vue +75 -0
  5. package/app/components/V/A/Btn/Add.vue +17 -0
  6. package/app/components/V/A/Btn/Back.vue +25 -0
  7. package/app/components/V/A/Btn/ConfirmDelete.vue +45 -0
  8. package/app/components/V/A/Btn/Edit.vue +35 -0
  9. package/app/components/V/A/Btn/Export.vue +28 -0
  10. package/app/components/V/A/Btn/Refresh.vue +21 -0
  11. package/app/components/V/A/Btn/Submit.vue +45 -0
  12. package/app/components/V/A/Btn/View.vue +23 -0
  13. package/app/components/V/A/Card.legacy.vue +291 -0
  14. package/app/components/V/A/Card.vue +108 -0
  15. package/app/components/V/A/CompanyMenu.vue +83 -0
  16. package/app/components/V/A/Data/KeyValue.vue +98 -0
  17. package/app/components/V/A/Data/StatusBadge.vue +44 -0
  18. package/app/components/V/A/DataField.vue +140 -0
  19. package/app/components/V/A/DataGrid.vue +43 -0
  20. package/app/components/V/A/DataTable.vue +144 -0
  21. package/app/components/V/A/EmptyState.vue +154 -0
  22. package/app/components/V/A/Fmt/Currency.vue +36 -0
  23. package/app/components/V/A/Fmt/DateTime.vue +34 -0
  24. package/app/components/V/A/Fmt/Percent.vue +47 -0
  25. package/app/components/V/A/LoadingState.vue +140 -0
  26. package/app/components/V/A/MetricCard.vue +129 -0
  27. package/app/components/V/A/Modal/Base.vue +195 -0
  28. package/app/components/V/A/Modal/Confirm.vue +92 -0
  29. package/app/components/V/A/Modal/Form.vue +105 -0
  30. package/app/components/V/A/Navigation.vue +110 -0
  31. package/app/components/V/A/QuickActions.vue +169 -0
  32. package/app/components/V/A/Slide.vue +109 -0
  33. package/app/components/V/A/Slideover.vue +259 -0
  34. package/app/components/V/A/State/Empty.vue +20 -0
  35. package/app/components/V/A/State/Error.vue +34 -0
  36. package/app/components/V/A/State/Loading.vue +33 -0
  37. package/app/components/V/A/StatsCard.vue +215 -0
  38. package/app/components/V/A/StatusBadge.vue +215 -0
  39. package/app/components/V/A/Table.vue +674 -0
  40. package/app/components/V/A/UserMenu.vue +127 -0
  41. package/app/components/V/A/WelcomeHeader.vue +96 -0
  42. package/app/components/V/Modal.vue +36 -0
  43. package/app/components/Va/Blocks/VaBlockGridCharts.vue +32 -0
  44. package/app/components/Va/Blocks/VaBlockGridKPI.vue +32 -0
  45. package/app/components/Va/Blocks/VaBlockGridTables.vue +23 -0
  46. package/app/components/Va/Blocks/VaBlockKpiGrid.vue +8 -0
  47. package/app/components/Va/Blocks/VaBlockSessionFilterBar.vue +8 -0
  48. package/app/components/Va/Cards/VaCardDonutChart.vue +59 -0
  49. package/app/components/Va/Cards/VaCardHeader.vue +10 -0
  50. package/app/components/Va/Cards/VaCardKpi.vue +17 -0
  51. package/app/components/Va/Cards/VaCardKpi2.vue +55 -0
  52. package/app/components/Va/Cards/VaCardLatestOrders.vue +82 -0
  53. package/app/components/Va/Cards/VaCardPopularProducts.vue +88 -0
  54. package/app/components/Va/Cards/VaCardRevenueBarChart.vue +49 -0
  55. package/app/components/Va/Cards/VaCardSubtitle.vue +5 -0
  56. package/app/components/Va/Cards/VaCardTitle.vue +5 -0
  57. package/app/components/Va/Cards/VaCardWithActiveUsers.vue +41 -0
  58. package/app/components/Va/Cards/VaCardWithChart.vue +135 -0
  59. package/app/components/Va/Cards/VaCardWithChartBlock.vue +26 -0
  60. package/app/components/Va/Cards/VaCardWithIndicator.vue +39 -0
  61. package/app/components/Va/Cards/VaCardWithProgressCircle.vue +34 -0
  62. package/app/components/Va/Cards/types.ts +11 -0
  63. package/app/components/Va/Charts/VaChartAppPerformanceBar.vue +118 -0
  64. package/app/components/Va/Charts/VaChartAppPerformanceBarChart.vue +118 -0
  65. package/app/components/Va/Charts/VaChartAreaMini.vue +127 -0
  66. package/app/components/Va/Charts/VaChartBarMini.vue +68 -0
  67. package/app/components/Va/Charts/VaChartCardinalMulti.vue +108 -0
  68. package/app/components/Va/Charts/VaChartColorBarChart.vue +78 -0
  69. package/app/components/Va/Charts/VaChartDonutHalf.vue +35 -0
  70. package/app/components/Va/Charts/VaChartDonutMini.vue +77 -0
  71. package/app/components/Va/Charts/VaChartExpensesBar.vue +58 -0
  72. package/app/components/Va/Charts/VaChartFinanceSummary.vue +96 -0
  73. package/app/components/Va/Charts/VaChartGoogleSearchConsole.vue +90 -0
  74. package/app/components/Va/Charts/VaChartIncomeBar.vue +82 -0
  75. package/app/components/Va/Charts/VaChartLegend.vue +25 -0
  76. package/app/components/Va/Charts/VaChartLineMini.vue +205 -0
  77. package/app/components/Va/Charts/VaChartRealtimeTraffic.vue +182 -0
  78. package/app/components/Va/Charts/VaChartRevenue.vue +43 -0
  79. package/app/components/Va/Charts/VaChartRevenueLine.vue +42 -0
  80. package/app/components/Va/Charts/VaChartRevenuevsCost.vue +84 -0
  81. package/app/components/Va/Charts/VaChartSearchIntent.vue +179 -0
  82. package/app/components/Va/Charts/VaChartSpendingTrend.vue +127 -0
  83. package/app/components/Va/Charts/VaChartStackedHorizontal.vue +64 -0
  84. package/app/components/Va/Charts/VaChartStepMinimal.vue +109 -0
  85. package/app/components/Va/Charts/VaChartStockComparisonLine.vue +86 -0
  86. package/app/components/Va/Charts/VaChartStocksPortfolioLine.vue +161 -0
  87. package/app/components/Va/Charts/VaChartStocksSectorLine.vue +223 -0
  88. package/app/components/Va/Charts/VaChartTasksCategories.vue +96 -0
  89. package/app/components/Va/Charts/VaChartTasksProgress.vue +130 -0
  90. package/app/components/Va/Charts/VaChartTrafficOverview.vue +112 -0
  91. package/app/components/Va/Charts/VaChartWebPerformanceLineChart.vue +114 -0
  92. package/app/components/Va/Charts/VaChartWinLostBar.vue +110 -0
  93. package/app/components/Va/Charts/VaChartWinLostDonut.vue +107 -0
  94. package/app/components/Va/Charts/VaChartWinLostLine.vue +111 -0
  95. package/app/components/Va/Charts/types.ts +10 -0
  96. package/app/components/Va/Dashboard/Navigation/types.ts +8 -0
  97. package/app/components/Va/Dashboard/VaDashboardKPICard.vue +31 -0
  98. package/app/components/Va/Dashboard/VaDashboardNavigation.vue +50 -0
  99. package/app/components/Va/Dashboard/VaDashboardPricePlan.vue +102 -0
  100. package/app/components/Va/Dashboard/VaDashboardUsageChart.vue +84 -0
  101. package/app/components/Va/Dashboard/VaDashboardUsageRequestChart.vue +46 -0
  102. package/app/components/Va/Layout/NotificationsSlideover.vue +169 -0
  103. package/app/components/Va/Layout/SideNav/types.ts +5 -0
  104. package/app/components/Va/Layout/SideNav.vue +108 -0
  105. package/app/components/Va/Layout/TeamsMenu.vue +57 -0
  106. package/app/components/Va/Layout/UserMenu.vue +57 -0
  107. package/app/composables/useDashboard.ts +25 -0
  108. package/app/composables/useVAAnimation.ts +324 -0
  109. package/app/composables/useVAUtils.ts +118 -0
  110. package/app/composables/useVCrud.ts +647 -0
  111. package/app/composables/useVFetch.ts +46 -0
  112. package/app/composables/useVFileUpload.ts +45 -0
  113. package/app/composables/useVToast.ts +73 -0
  114. package/app/composables/useXATableColumns.ts +456 -0
  115. package/app/data/BillingStats.ts +65 -0
  116. package/app/data/SearchData.ts +58 -0
  117. package/app/data/TasksData.ts +101 -0
  118. package/app/data/dashboardData.ts +113 -0
  119. package/app/layouts/default.vue +171 -0
  120. package/app/layouts/legacy.vue +61 -0
  121. package/app/pages/playground/base.vue +498 -0
  122. package/app/pages/playground/blocks.vue +108 -0
  123. package/app/pages/playground/buttons.vue +237 -0
  124. package/app/pages/playground/cards.vue +326 -0
  125. package/app/pages/playground/charts.vue +338 -0
  126. package/app/pages/playground/dashboard.vue +315 -0
  127. package/app/pages/playground/formatters.vue +329 -0
  128. package/app/pages/playground/index.vue +109 -0
  129. package/app/pages/playground/layout.vue +159 -0
  130. package/app/pages/playground/modals.vue +606 -0
  131. package/app/pages/playground/states.vue +282 -0
  132. package/app/pages/playground/tables.vue +618 -0
  133. package/app/pages/test-layout.vue +10 -0
  134. package/nuxt.config.ts +12 -0
  135. package/package.json +71 -0
  136. package/tsconfig.json +18 -0
@@ -0,0 +1,90 @@
1
+ <script lang="ts" setup>
2
+ import { DetailedSearchQueryData } from '~/data/SearchData'
3
+
4
+ interface DateAggregateData {
5
+ date: string
6
+ impressions: number
7
+ clicks: number
8
+ ctr: number
9
+ averagePosition: number
10
+ }
11
+
12
+ const chartData = computed<DateAggregateData[]>(() => {
13
+ return DetailedSearchQueryData.map((dateEntry) => {
14
+ let totalImpressions = 0
15
+ let totalClicks = 0
16
+
17
+ let totalWeightedPosition = 0
18
+
19
+ dateEntry.queries.forEach((query) => {
20
+ totalImpressions += query.impressions
21
+ totalClicks += query.clicks
22
+
23
+ totalWeightedPosition += query.averagePosition * query.impressions
24
+ })
25
+
26
+ const aggregateCTR
27
+ = totalImpressions > 0 ? (totalClicks / totalImpressions) * 100 : 0
28
+
29
+ const aggregateAveragePosition
30
+ = totalImpressions > 0 ? totalWeightedPosition / totalImpressions : 0
31
+
32
+ return {
33
+ date: dateEntry.date,
34
+ impressions: totalImpressions,
35
+ clicks: totalClicks,
36
+ ctr: parseFloat(aggregateCTR.toFixed(2)),
37
+ averagePosition: parseFloat(aggregateAveragePosition.toFixed(1))
38
+ }
39
+ })
40
+ })
41
+
42
+ const categories = {
43
+ impressions: {
44
+ name: 'Impressions',
45
+ color: 'var(--color-amber-400)'
46
+ },
47
+ clicks: {
48
+ name: 'Clicks',
49
+ color: 'var(--ui-primary)'
50
+ },
51
+ ctr: {
52
+ name: 'CTR',
53
+ color: 'var(--color-indigo-400)'
54
+ },
55
+ averagePosition: {
56
+ name: 'Average Position',
57
+ color: 'var(--color-orange-400)'
58
+ }
59
+ }
60
+
61
+ const xFormatter = (tick: number, _i?: number, _ticks?: number[]): string => {
62
+ return String(chartData.value[tick]?.date ?? '')
63
+ }
64
+ </script>
65
+
66
+ <template>
67
+ <UCard>
68
+ <div class="mb-8 space-y-2">
69
+ <p class="font-semibold text-lg text-highlighted">
70
+ Search Performance
71
+ </p>
72
+ </div>
73
+
74
+ <BarChart
75
+ :data="chartData"
76
+ :height="275"
77
+ :x-num-ticks="4"
78
+ :y-num-ticks="4"
79
+ :x-formatter="xFormatter"
80
+ :categories="categories"
81
+ :y-grid-line="true"
82
+ :x-grid-line="true"
83
+ :group-padding="0.2"
84
+ :bar-padding="0.5"
85
+ :y-axis="['clicks', 'impressions', 'averagePosition', 'ctr']"
86
+ :curve-type="CurveType.Linear"
87
+ :legend-position="LegendPosition.BottomCenter"
88
+ />
89
+ </UCard>
90
+ </template>
@@ -0,0 +1,82 @@
1
+ <script setup lang="ts">
2
+ interface DailyData {
3
+ day: string
4
+ Income: number
5
+ Expense: number
6
+ }
7
+
8
+ const chartData = ref<DailyData[]>([
9
+ {
10
+ day: 'May.',
11
+ Income: 8500,
12
+ Expense: 4200
13
+ },
14
+ {
15
+ day: 'Jun.',
16
+ Income: 9100,
17
+ Expense: 3900
18
+ },
19
+ {
20
+ day: 'Jul.',
21
+ Income: 9800,
22
+ Expense: 4100
23
+ },
24
+ {
25
+ day: 'Aug.',
26
+ Income: 10200,
27
+ Expense: 4300
28
+ },
29
+ {
30
+ day: 'Sep.',
31
+ Income: 9700,
32
+ Expense: 4000
33
+ },
34
+ {
35
+ day: 'Oct.',
36
+ Income: 10500,
37
+ Expense: 4500
38
+ }
39
+ ])
40
+
41
+ const chartCategories: Record<string, BulletLegendItemInterface> = {
42
+ Income: {
43
+ name: 'Income',
44
+ color: 'var(--ui-primary)'
45
+ },
46
+ Expense: {
47
+ name: 'Expense',
48
+ color: 'var(--ui-warning)'
49
+ }
50
+ }
51
+
52
+ const xAxisFormatter = (index: number): string => {
53
+ if (index >= 0 && index < chartData.value.length) {
54
+ return chartData.value[index]?.day || ''
55
+ }
56
+ return ''
57
+ }
58
+ </script>
59
+
60
+ <template>
61
+ <BarChart
62
+ :data="chartData"
63
+ :categories="chartCategories"
64
+ :y-axis="['Income', 'Expense']"
65
+ :x-formatter="xAxisFormatter"
66
+ :height="200"
67
+ :stacked="false"
68
+ :radius="3"
69
+ :group-padding="0.25"
70
+ :bar-padding="0.1"
71
+ :hide-legend="true"
72
+ :legend-position="LegendPosition.BottomCenter"
73
+ :x-domain-line="false"
74
+ :y-domain-line="false"
75
+ :x-tick-line="false"
76
+ :y-tick-line="false"
77
+ :x-grid-line="false"
78
+ :y-grid-line="true"
79
+ :x-num-ticks="5"
80
+ :y-num-ticks="3"
81
+ />
82
+ </template>
@@ -0,0 +1,25 @@
1
+ <script lang="ts" setup>
2
+ import type { ChartCategories } from '~/types'
3
+
4
+ defineProps<{
5
+ categories: ChartCategories
6
+ }>()
7
+ </script>
8
+
9
+ <template>
10
+ <div class="flex flex-wrap items-center justify-end gap-x-6 gap-y-2">
11
+ <div
12
+ v-for="(category, categoryKey) in categories"
13
+ :key="categoryKey"
14
+ class="flex items-center gap-2"
15
+ >
16
+ <span
17
+ class="h-2 w-2 rounded-full"
18
+ :style="{ backgroundColor: category.color }"
19
+ />
20
+ <span class="text-xs text-(--vis-legend-label-color)">
21
+ {{ category.name }}
22
+ </span>
23
+ </div>
24
+ </div>
25
+ </template>
@@ -0,0 +1,205 @@
1
+ <script setup lang="ts">
2
+ const colorMode = useColorMode()
3
+
4
+ interface BudgetDataItem {
5
+ month: number
6
+ actual: number
7
+ target: number
8
+ }
9
+
10
+ const baseData: BudgetDataItem[] = [
11
+ {
12
+ month: 1,
13
+ actual: 2500,
14
+ target: 2600
15
+ },
16
+ {
17
+ month: 2,
18
+ actual: 1500,
19
+ target: 2700
20
+ },
21
+ {
22
+ month: 3,
23
+ actual: 3000,
24
+ target: 2900
25
+ },
26
+ {
27
+ month: 4,
28
+ actual: 4000,
29
+ target: 3200
30
+ },
31
+ {
32
+ month: 5,
33
+ actual: 4500,
34
+ target: 3500
35
+ },
36
+ {
37
+ month: 6,
38
+ actual: 2800,
39
+ target: 3600
40
+ },
41
+ {
42
+ month: 7,
43
+ actual: 3500,
44
+ target: 3800
45
+ }
46
+ ]
47
+
48
+ const range = ref<'Full year' | 'Last 6 months'>('Full year')
49
+
50
+ const curveChoice = ref<'Smooth' | 'Linear' | 'Step' | 'Basis'>('Smooth')
51
+
52
+ const curveMap = computed(() => ({
53
+ Smooth: CurveType.MonotoneX,
54
+ Linear: CurveType.Linear,
55
+ Step: CurveType.StepAfter,
56
+ Basis: CurveType.Basis
57
+ }))
58
+
59
+ const displayedData = computed<BudgetDataItem[]>(() => {
60
+ if (range.value === 'Last 6 months') {
61
+ return baseData.filter(d => d.month >= 7)
62
+ }
63
+ return baseData
64
+ })
65
+
66
+ const Categories = computed<Record<string, BulletLegendItemInterface>>(() => ({
67
+ actual: {
68
+ name: 'Actual',
69
+ color: colorMode.value === 'dark' ? '#22c55e' : '#16a34a'
70
+ },
71
+ target: {
72
+ name: 'Target',
73
+ color: colorMode.value === 'dark' ? '#64748b' : '#475569'
74
+ }
75
+ }))
76
+ const actualColor = computed(() => Categories.value.actual?.color ?? '#22c55e')
77
+ const targetColor = computed(() => Categories.value.target?.color ?? '#64748b')
78
+
79
+ type SeriesMarkerConfigItem = {
80
+ type?: 'circle' | 'square' | 'triangle' | 'diamond'
81
+ size?: number
82
+ strokeWidth?: number
83
+ color: string
84
+ strokeColor: string
85
+ }
86
+
87
+ type SeriesMarkerConfig = Record<'actual' | 'target', SeriesMarkerConfigItem>
88
+
89
+ const markerConfig: SeriesMarkerConfig = {
90
+ actual: {
91
+ type: 'circle',
92
+ size: 7,
93
+ color: actualColor.value,
94
+ strokeColor: actualColor.value,
95
+ strokeWidth: 2
96
+ },
97
+ target: {
98
+ type: 'triangle',
99
+ size: 8,
100
+ color: targetColor.value,
101
+ strokeColor: targetColor.value,
102
+ strokeWidth: 2
103
+ }
104
+ }
105
+
106
+ const lineDashArray = computed<number[][]>(() => [
107
+ [0, 0],
108
+ [5, 4]
109
+ ])
110
+
111
+ const monthLabel = (i: number): string =>
112
+ new Date(`2025-${displayedData.value[i]?.month}-02`).toLocaleDateString(
113
+ 'en-US',
114
+ { month: 'short' }
115
+ )
116
+
117
+ const formatCurrency = (value: number) =>
118
+ new Intl.NumberFormat('en-US', {
119
+ style: 'currency',
120
+ currency: 'USD',
121
+ minimumFractionDigits: 0,
122
+ maximumFractionDigits: 0
123
+ }).format(value)
124
+ </script>
125
+
126
+ <template>
127
+ <div class="mx-auto max-w-xl">
128
+ <LineChart
129
+ :key="colorMode.value + range + curveChoice"
130
+ :data="displayedData"
131
+ :height="100"
132
+ :categories="Categories"
133
+ :x-formatter="monthLabel"
134
+ :y-formatter="formatCurrency"
135
+ :y-grid-line="true"
136
+ :x-grid-line="true"
137
+ :hide-y-axis="true"
138
+ :curve-type="curveMap[curveChoice]"
139
+ :legend-position="LegendPosition.TopRight"
140
+ :hide-legend="false"
141
+ :marker-config="markerConfig"
142
+ :line-dash-array="lineDashArray"
143
+ >
144
+ <template #tooltip="{ values }">
145
+ <div
146
+ class="bg-default text-default border-default min-w-32 rounded-lg border p-3 shadow-lg"
147
+ >
148
+ <div class="text-dimmed mb-2 text-xs">
149
+ {{
150
+ new Date(`2025-${values?.month}-02`).toLocaleDateString("en-US", {
151
+ month: "long"
152
+ })
153
+ }}
154
+ </div>
155
+
156
+ <div class="space-y-1">
157
+ <div class="flex items-center justify-between text-sm">
158
+ <div class="flex items-center gap-2">
159
+ <div
160
+ class="h-2 w-2 rounded-full"
161
+ :style="{ backgroundColor: actualColor }"
162
+ />
163
+ <span class="w-24">Actual: </span>
164
+ </div>
165
+ <span class="font-medium">{{
166
+ formatCurrency(values?.actual ?? 0)
167
+ }}</span>
168
+ </div>
169
+
170
+ <div class="flex items-center justify-between text-sm">
171
+ <div class="flex items-center gap-2">
172
+ <div
173
+ class="h-2 w-2 rounded-full"
174
+ :style="{ backgroundColor: targetColor }"
175
+ />
176
+ <span>Target</span>
177
+ </div>
178
+ <span class="font-medium">{{
179
+ formatCurrency(values?.target ?? 0)
180
+ }}</span>
181
+ </div>
182
+
183
+ <hr class="border-default my-2">
184
+
185
+ <div class="flex justify-between text-sm">
186
+ <span>Variance</span>
187
+ <span
188
+ class="font-semibold"
189
+ :class="
190
+ (values?.actual ?? 0) - (values?.target ?? 0) >= 0
191
+ ? 'text-success'
192
+ : 'text-error'
193
+ "
194
+ >
195
+ {{
196
+ formatCurrency((values?.actual ?? 0) - (values?.target ?? 0))
197
+ }}
198
+ </span>
199
+ </div>
200
+ </div>
201
+ </div>
202
+ </template>
203
+ </LineChart>
204
+ </div>
205
+ </template>
@@ -0,0 +1,182 @@
1
+ <script setup lang="ts">
2
+ interface CountryStat {
3
+ country: string
4
+ code?: string
5
+ users: number
6
+ }
7
+
8
+ const props = withDefaults(
9
+ defineProps<{
10
+ minutes?: number
11
+ updateInterval?: number
12
+ seedCountries?: CountryStat[] | null
13
+ simulate?: boolean
14
+ }>(),
15
+ {
16
+ minutes: 30,
17
+ updateInterval: 4000,
18
+ simulate: true,
19
+ seedCountries: null
20
+ }
21
+ )
22
+
23
+ const perMinute = ref<number[]>(
24
+ Array.from(
25
+ { length: props.minutes },
26
+ () => 50 + Math.round(Math.random() * 120)
27
+ )
28
+ )
29
+
30
+ const countries = ref<CountryStat[]>(
31
+ props.seedCountries || [
32
+ {
33
+ country: 'Netherlands',
34
+ users: 87
35
+ },
36
+ {
37
+ country: 'Belgium',
38
+ users: 25
39
+ },
40
+ {
41
+ country: 'Germany',
42
+ users: 4
43
+ },
44
+ {
45
+ country: 'United States',
46
+ users: 2
47
+ },
48
+ {
49
+ country: 'France',
50
+ users: 1
51
+ }
52
+ ]
53
+ )
54
+
55
+ const totalActive = computed(() =>
56
+ countries.value.reduce((a, c) => a + c.users, 0)
57
+ )
58
+
59
+ const maxBucket = computed(() => Math.max(...perMinute.value, 1))
60
+
61
+ const maxCountryUsers = computed(() =>
62
+ Math.max(...countries.value.map(c => c.users), 1)
63
+ )
64
+
65
+ let timer: ReturnType<typeof setInterval> | null = null
66
+
67
+ function simulateTick() {
68
+ perMinute.value.shift()
69
+
70
+ const last = perMinute.value[perMinute.value.length - 1] || 100
71
+ const delta = (Math.random() - 0.5) * 40
72
+ const next = Math.max(10, Math.round(last + delta))
73
+ perMinute.value.push(next)
74
+
75
+ countries.value = countries.value
76
+ .map((c, idx) => {
77
+ const varianceFactor = Math.max(1, countries.value.length - idx)
78
+ const change = Math.round(((Math.random() - 0.5) * 120) / varianceFactor)
79
+ return {
80
+ ...c,
81
+ users: Math.max(5, c.users + change)
82
+ }
83
+ })
84
+ .sort((a, b) => b.users - a.users)
85
+ }
86
+
87
+ onMounted(() => {
88
+ if (props.simulate) {
89
+ timer = setInterval(simulateTick, props.updateInterval)
90
+ }
91
+ })
92
+
93
+ onBeforeUnmount(() => {
94
+ if (timer) clearInterval(timer)
95
+ })
96
+
97
+ defineExpose({
98
+ perMinute,
99
+ countries,
100
+ totalActive
101
+ })
102
+ </script>
103
+
104
+ <template>
105
+ <UCard :ui="{ footer: 'flex justify-end' }">
106
+ <div class="flex items-start justify-between gap-4">
107
+ <div class="space-y-2">
108
+ <p class="text-xs uppercase tracking-wider text-muted font-medium">
109
+ Active users in last 30 minutes
110
+ </p>
111
+ <div class="font-semibold text-3xl text-highlighted tabular-nums">
112
+ {{ totalActive.toLocaleString() }}
113
+ </div>
114
+ </div>
115
+ <UButton
116
+ color="success"
117
+ variant="soft"
118
+ size="sm"
119
+ icon="i-lucide-circle-check"
120
+ aria-label="Status OK"
121
+ class="shrink-0"
122
+ />
123
+ </div>
124
+
125
+ <div
126
+ class="mt-4 h-16 grid grid-cols-30 gap-[2px] items-end"
127
+ role="list"
128
+ aria-label="Active users per minute chart"
129
+ >
130
+ <div
131
+ v-for="(v, i) in perMinute"
132
+ :key="i"
133
+ class="relative group flex items-end justify-center h-full"
134
+ >
135
+ <div
136
+ class="w-full rounded-xs bg-orange-400 group-hover:bg-orange-500 transition-colors"
137
+ :style="{ height: `${((v / maxBucket) * 100).toFixed(2)}%` }"
138
+ :aria-label="`Minute ${i + 1} value ${v}`"
139
+ />
140
+ </div>
141
+ </div>
142
+
143
+ <div class="mt-6">
144
+ <p class="text-xs uppercase tracking-wider text-muted font-medium mb-3">
145
+ Top Countries
146
+ </p>
147
+ <ul class="space-y-2">
148
+ <li
149
+ v-for="c in countries"
150
+ :key="c.country"
151
+ class="flex items-center gap-3"
152
+ >
153
+ <div class="w-40 truncate text-sm">
154
+ {{ c.country }}
155
+ </div>
156
+ <div class="flex-1 h-2 bg-elevated rounded-sm overflow-hidden">
157
+ <div
158
+ class="h-full bg-blue-500"
159
+ :style="{
160
+ width: ((c.users / maxCountryUsers) * 100).toFixed(2) + '%'
161
+ }"
162
+ />
163
+ </div>
164
+ <div class="w-16 text-right text-sm tabular-nums">
165
+ {{
166
+ c.users.toLocaleString(undefined, {
167
+ notation: c.users > 999 ? "compact" : "standard"
168
+ })
169
+ }}
170
+ </div>
171
+ </li>
172
+ </ul>
173
+ </div>
174
+
175
+ <template #footer>
176
+ <UButton
177
+ label="View Real Time"
178
+ trailing-icon="i-lucide-chevron-right"
179
+ />
180
+ </template>
181
+ </UCard>
182
+ </template>
@@ -0,0 +1,43 @@
1
+ <script setup lang="ts">
2
+ type RevenueDataItem = {
3
+ month: string;
4
+ desktop: number;
5
+ mobile: number;
6
+ };
7
+
8
+ const RevenueData: RevenueDataItem[] = [
9
+ { month: "January", desktop: 186, mobile: 80 },
10
+ { month: "February", desktop: 305, mobile: 200 },
11
+ { month: "March", desktop: 237, mobile: 120 },
12
+ { month: "April", desktop: 73, mobile: 190 },
13
+ { month: "May", desktop: 209, mobile: 130 },
14
+ { month: "June", desktop: 214, mobile: 140 },
15
+ ];
16
+
17
+ const RevenueCategoriesMultple = {
18
+ desktop: { name: "Desktop", color: "var(--color-primary-400)" },
19
+ mobile: { name: "Mobile", color: "var(--chart-secondary)" },
20
+ };
21
+
22
+ const xFormatter = (i: number): string => `${RevenueData[i]?.month}`;
23
+ const yFormatter = (tick: number, _i?: number, _ticks?: number[]) =>
24
+ tick.toString() + "K";
25
+
26
+ </script>
27
+ <template>
28
+ <BarChart
29
+ :data="RevenueData"
30
+ :height="200"
31
+ :categories="RevenueCategoriesMultple"
32
+ :y-axis="['desktop', 'mobile']"
33
+ :group-padding="0"
34
+ :bar-padding="0.2"
35
+ :radius="4"
36
+ :y-num-ticks="4"
37
+ :x-formatter="xFormatter"
38
+ :y-formatter="yFormatter"
39
+ :legend-position="LegendPosition.Top"
40
+ :hide-legend="true"
41
+ :y-grid-line="true"
42
+ />
43
+ </template>
@@ -0,0 +1,42 @@
1
+ <script setup lang="ts">
2
+ const chartData = ref([
3
+ { date: "1", current: 1100, last_year: 1050 },
4
+ { date: "2", current: 1150, last_year: 1080 },
5
+ { date: "3", current: 1120, last_year: 1100 },
6
+ { date: "4", current: 1180, last_year: 1120 },
7
+ { date: "5", current: 1200, last_year: 1150 },
8
+ { date: "6", current: 1170, last_year: 1130 },
9
+ { date: "7", current: 1210, last_year: 1160 },
10
+ { date: "8", current: 1230, last_year: 1180 },
11
+ { date: "9", current: 1250, last_year: 1200 },
12
+ { date: "10", current: 1220, last_year: 1190 },
13
+ { date: "11", current: 1280, last_year: 1210 },
14
+ { date: "12", current: 1300, last_year: 1230 },
15
+ { date: "13", current: 1270, last_year: 1220 },
16
+ { date: "14", current: 1320, last_year: 1250 },
17
+ ]);
18
+
19
+ const categories: Record<string, BulletLegendItemInterface> = {
20
+ current: { name: "June 2025", color: "var(--ui-primary)" },
21
+ last_year: { name: "June 2024", color: "var(--color-warning-500)" },
22
+ };
23
+
24
+ const xFormatter = (i: number): string => `${chartData.value[i]?.date}`;
25
+ const yFormatter = (value: number): string => `$${(value / 1000).toFixed(1)}k`;
26
+ </script>
27
+ <template>
28
+ <LineChart
29
+ :data="chartData"
30
+ :height="180"
31
+ :x-num-ticks="4"
32
+ :y-num-ticks="3.5"
33
+ :categories="categories"
34
+ :x-formatter="xFormatter"
35
+ :y-formatter="yFormatter"
36
+ :curve-type="CurveType.Linear"
37
+ :legend-position="LegendPosition.Top"
38
+ :y-grid-line="true"
39
+ :hide-x-axis="true"
40
+ :hide-legend="true"
41
+ />
42
+ </template>