@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.
- package/README.md +42 -0
- package/app/app.vue +7 -0
- package/app/assets/css/v-app.css +313 -0
- package/app/components/V/A/Badge.vue +75 -0
- package/app/components/V/A/Btn/Add.vue +17 -0
- package/app/components/V/A/Btn/Back.vue +25 -0
- package/app/components/V/A/Btn/ConfirmDelete.vue +45 -0
- package/app/components/V/A/Btn/Edit.vue +35 -0
- package/app/components/V/A/Btn/Export.vue +28 -0
- package/app/components/V/A/Btn/Refresh.vue +21 -0
- package/app/components/V/A/Btn/Submit.vue +45 -0
- package/app/components/V/A/Btn/View.vue +23 -0
- package/app/components/V/A/Card.legacy.vue +291 -0
- package/app/components/V/A/Card.vue +108 -0
- package/app/components/V/A/CompanyMenu.vue +83 -0
- package/app/components/V/A/Data/KeyValue.vue +98 -0
- package/app/components/V/A/Data/StatusBadge.vue +44 -0
- package/app/components/V/A/DataField.vue +140 -0
- package/app/components/V/A/DataGrid.vue +43 -0
- package/app/components/V/A/DataTable.vue +144 -0
- package/app/components/V/A/EmptyState.vue +154 -0
- package/app/components/V/A/Fmt/Currency.vue +36 -0
- package/app/components/V/A/Fmt/DateTime.vue +34 -0
- package/app/components/V/A/Fmt/Percent.vue +47 -0
- package/app/components/V/A/LoadingState.vue +140 -0
- package/app/components/V/A/MetricCard.vue +129 -0
- package/app/components/V/A/Modal/Base.vue +195 -0
- package/app/components/V/A/Modal/Confirm.vue +92 -0
- package/app/components/V/A/Modal/Form.vue +105 -0
- package/app/components/V/A/Navigation.vue +110 -0
- package/app/components/V/A/QuickActions.vue +169 -0
- package/app/components/V/A/Slide.vue +109 -0
- package/app/components/V/A/Slideover.vue +259 -0
- package/app/components/V/A/State/Empty.vue +20 -0
- package/app/components/V/A/State/Error.vue +34 -0
- package/app/components/V/A/State/Loading.vue +33 -0
- package/app/components/V/A/StatsCard.vue +215 -0
- package/app/components/V/A/StatusBadge.vue +215 -0
- package/app/components/V/A/Table.vue +674 -0
- package/app/components/V/A/UserMenu.vue +127 -0
- package/app/components/V/A/WelcomeHeader.vue +96 -0
- package/app/components/V/Modal.vue +36 -0
- package/app/components/Va/Blocks/VaBlockGridCharts.vue +32 -0
- package/app/components/Va/Blocks/VaBlockGridKPI.vue +32 -0
- package/app/components/Va/Blocks/VaBlockGridTables.vue +23 -0
- package/app/components/Va/Blocks/VaBlockKpiGrid.vue +8 -0
- package/app/components/Va/Blocks/VaBlockSessionFilterBar.vue +8 -0
- package/app/components/Va/Cards/VaCardDonutChart.vue +59 -0
- package/app/components/Va/Cards/VaCardHeader.vue +10 -0
- package/app/components/Va/Cards/VaCardKpi.vue +17 -0
- package/app/components/Va/Cards/VaCardKpi2.vue +55 -0
- package/app/components/Va/Cards/VaCardLatestOrders.vue +82 -0
- package/app/components/Va/Cards/VaCardPopularProducts.vue +88 -0
- package/app/components/Va/Cards/VaCardRevenueBarChart.vue +49 -0
- package/app/components/Va/Cards/VaCardSubtitle.vue +5 -0
- package/app/components/Va/Cards/VaCardTitle.vue +5 -0
- package/app/components/Va/Cards/VaCardWithActiveUsers.vue +41 -0
- package/app/components/Va/Cards/VaCardWithChart.vue +135 -0
- package/app/components/Va/Cards/VaCardWithChartBlock.vue +26 -0
- package/app/components/Va/Cards/VaCardWithIndicator.vue +39 -0
- package/app/components/Va/Cards/VaCardWithProgressCircle.vue +34 -0
- package/app/components/Va/Cards/types.ts +11 -0
- package/app/components/Va/Charts/VaChartAppPerformanceBar.vue +118 -0
- package/app/components/Va/Charts/VaChartAppPerformanceBarChart.vue +118 -0
- package/app/components/Va/Charts/VaChartAreaMini.vue +127 -0
- package/app/components/Va/Charts/VaChartBarMini.vue +68 -0
- package/app/components/Va/Charts/VaChartCardinalMulti.vue +108 -0
- package/app/components/Va/Charts/VaChartColorBarChart.vue +78 -0
- package/app/components/Va/Charts/VaChartDonutHalf.vue +35 -0
- package/app/components/Va/Charts/VaChartDonutMini.vue +77 -0
- package/app/components/Va/Charts/VaChartExpensesBar.vue +58 -0
- package/app/components/Va/Charts/VaChartFinanceSummary.vue +96 -0
- package/app/components/Va/Charts/VaChartGoogleSearchConsole.vue +90 -0
- package/app/components/Va/Charts/VaChartIncomeBar.vue +82 -0
- package/app/components/Va/Charts/VaChartLegend.vue +25 -0
- package/app/components/Va/Charts/VaChartLineMini.vue +205 -0
- package/app/components/Va/Charts/VaChartRealtimeTraffic.vue +182 -0
- package/app/components/Va/Charts/VaChartRevenue.vue +43 -0
- package/app/components/Va/Charts/VaChartRevenueLine.vue +42 -0
- package/app/components/Va/Charts/VaChartRevenuevsCost.vue +84 -0
- package/app/components/Va/Charts/VaChartSearchIntent.vue +179 -0
- package/app/components/Va/Charts/VaChartSpendingTrend.vue +127 -0
- package/app/components/Va/Charts/VaChartStackedHorizontal.vue +64 -0
- package/app/components/Va/Charts/VaChartStepMinimal.vue +109 -0
- package/app/components/Va/Charts/VaChartStockComparisonLine.vue +86 -0
- package/app/components/Va/Charts/VaChartStocksPortfolioLine.vue +161 -0
- package/app/components/Va/Charts/VaChartStocksSectorLine.vue +223 -0
- package/app/components/Va/Charts/VaChartTasksCategories.vue +96 -0
- package/app/components/Va/Charts/VaChartTasksProgress.vue +130 -0
- package/app/components/Va/Charts/VaChartTrafficOverview.vue +112 -0
- package/app/components/Va/Charts/VaChartWebPerformanceLineChart.vue +114 -0
- package/app/components/Va/Charts/VaChartWinLostBar.vue +110 -0
- package/app/components/Va/Charts/VaChartWinLostDonut.vue +107 -0
- package/app/components/Va/Charts/VaChartWinLostLine.vue +111 -0
- package/app/components/Va/Charts/types.ts +10 -0
- package/app/components/Va/Dashboard/Navigation/types.ts +8 -0
- package/app/components/Va/Dashboard/VaDashboardKPICard.vue +31 -0
- package/app/components/Va/Dashboard/VaDashboardNavigation.vue +50 -0
- package/app/components/Va/Dashboard/VaDashboardPricePlan.vue +102 -0
- package/app/components/Va/Dashboard/VaDashboardUsageChart.vue +84 -0
- package/app/components/Va/Dashboard/VaDashboardUsageRequestChart.vue +46 -0
- package/app/components/Va/Layout/NotificationsSlideover.vue +169 -0
- package/app/components/Va/Layout/SideNav/types.ts +5 -0
- package/app/components/Va/Layout/SideNav.vue +108 -0
- package/app/components/Va/Layout/TeamsMenu.vue +57 -0
- package/app/components/Va/Layout/UserMenu.vue +57 -0
- package/app/composables/useDashboard.ts +25 -0
- package/app/composables/useVAAnimation.ts +324 -0
- package/app/composables/useVAUtils.ts +118 -0
- package/app/composables/useVCrud.ts +647 -0
- package/app/composables/useVFetch.ts +46 -0
- package/app/composables/useVFileUpload.ts +45 -0
- package/app/composables/useVToast.ts +73 -0
- package/app/composables/useXATableColumns.ts +456 -0
- package/app/data/BillingStats.ts +65 -0
- package/app/data/SearchData.ts +58 -0
- package/app/data/TasksData.ts +101 -0
- package/app/data/dashboardData.ts +113 -0
- package/app/layouts/default.vue +171 -0
- package/app/layouts/legacy.vue +61 -0
- package/app/pages/playground/base.vue +498 -0
- package/app/pages/playground/blocks.vue +108 -0
- package/app/pages/playground/buttons.vue +237 -0
- package/app/pages/playground/cards.vue +326 -0
- package/app/pages/playground/charts.vue +338 -0
- package/app/pages/playground/dashboard.vue +315 -0
- package/app/pages/playground/formatters.vue +329 -0
- package/app/pages/playground/index.vue +109 -0
- package/app/pages/playground/layout.vue +159 -0
- package/app/pages/playground/modals.vue +606 -0
- package/app/pages/playground/states.vue +282 -0
- package/app/pages/playground/tables.vue +618 -0
- package/app/pages/test-layout.vue +10 -0
- package/nuxt.config.ts +12 -0
- package/package.json +71 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const props = withDefaults(defineProps<{
|
|
3
|
+
collapsed?: boolean
|
|
4
|
+
user?: {
|
|
5
|
+
name: string
|
|
6
|
+
avatar?: {
|
|
7
|
+
src: string
|
|
8
|
+
alt?: string
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
theme?: 'light' | 'dark'
|
|
12
|
+
}>(), {
|
|
13
|
+
collapsed: false,
|
|
14
|
+
user: () => ({
|
|
15
|
+
name: 'User',
|
|
16
|
+
avatar: {
|
|
17
|
+
src: 'https://api.dicebear.com/7.x/avataaars/svg?seed=default',
|
|
18
|
+
alt: 'User'
|
|
19
|
+
}
|
|
20
|
+
}),
|
|
21
|
+
theme: 'light'
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const buttonUi = computed(() => props.theme === 'dark'
|
|
25
|
+
? {
|
|
26
|
+
root: 'hover:bg-white/5',
|
|
27
|
+
label: 'text-slate-300 group-hover:text-white',
|
|
28
|
+
trailingIcon: 'text-slate-400 group-hover:text-white'
|
|
29
|
+
}
|
|
30
|
+
: { trailingIcon: 'text-dimmed' }
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
const emit = defineEmits<{
|
|
34
|
+
logout: []
|
|
35
|
+
profile: []
|
|
36
|
+
settings: []
|
|
37
|
+
}>()
|
|
38
|
+
|
|
39
|
+
// Simple menu state
|
|
40
|
+
const isOpen = ref(false)
|
|
41
|
+
const menuRef = ref<HTMLElement | null>(null)
|
|
42
|
+
|
|
43
|
+
const toggleMenu = () => {
|
|
44
|
+
isOpen.value = !isOpen.value
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const closeMenu = () => {
|
|
48
|
+
isOpen.value = false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const menuItems = [
|
|
52
|
+
{
|
|
53
|
+
label: 'Profile',
|
|
54
|
+
icon: 'i-lucide-user',
|
|
55
|
+
action: () => { emit('profile'); closeMenu() }
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
label: 'Settings',
|
|
59
|
+
icon: 'i-lucide-settings',
|
|
60
|
+
action: () => { emit('settings'); closeMenu() }
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
label: 'Log out',
|
|
64
|
+
icon: 'i-lucide-log-out',
|
|
65
|
+
action: () => { emit('logout'); closeMenu() }
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
// Close menu when clicking outside
|
|
70
|
+
onMounted(() => {
|
|
71
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
72
|
+
if (menuRef.value && !menuRef.value.contains(event.target as Node)) {
|
|
73
|
+
closeMenu()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
document.addEventListener('click', handleClickOutside)
|
|
77
|
+
onUnmounted(() => {
|
|
78
|
+
document.removeEventListener('click', handleClickOutside)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<template>
|
|
84
|
+
<div ref="menuRef" class="relative">
|
|
85
|
+
<UButton
|
|
86
|
+
:avatar="user?.avatar"
|
|
87
|
+
:label="collapsed ? undefined : user?.name"
|
|
88
|
+
:trailing-icon="collapsed ? undefined : 'i-lucide-chevrons-up-down'"
|
|
89
|
+
color="neutral"
|
|
90
|
+
variant="ghost"
|
|
91
|
+
block
|
|
92
|
+
:square="collapsed"
|
|
93
|
+
:class="[
|
|
94
|
+
'group',
|
|
95
|
+
isOpen && 'bg-white/5',
|
|
96
|
+
theme === 'dark' && 'hover:bg-white/5'
|
|
97
|
+
]"
|
|
98
|
+
:ui="buttonUi"
|
|
99
|
+
@click.stop="toggleMenu"
|
|
100
|
+
/>
|
|
101
|
+
|
|
102
|
+
<!-- Custom dropdown menu -->
|
|
103
|
+
<Transition
|
|
104
|
+
enter-active-class="transition ease-out duration-100"
|
|
105
|
+
enter-from-class="transform opacity-0 scale-95"
|
|
106
|
+
enter-to-class="transform opacity-100 scale-100"
|
|
107
|
+
leave-active-class="transition ease-in duration-75"
|
|
108
|
+
leave-from-class="transform opacity-100 scale-100"
|
|
109
|
+
leave-to-class="transform opacity-0 scale-95"
|
|
110
|
+
>
|
|
111
|
+
<div
|
|
112
|
+
v-if="isOpen"
|
|
113
|
+
class="absolute bottom-full left-0 mb-2 w-48 rounded-lg bg-white dark:bg-slate-800 shadow-lg ring-1 ring-black/5 dark:ring-white/10 py-1 z-50"
|
|
114
|
+
>
|
|
115
|
+
<button
|
|
116
|
+
v-for="item in menuItems"
|
|
117
|
+
:key="item.label"
|
|
118
|
+
class="w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-slate-700 transition-colors"
|
|
119
|
+
@click="item.action"
|
|
120
|
+
>
|
|
121
|
+
<UIcon :name="item.icon" class="size-4" />
|
|
122
|
+
{{ item.label }}
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
</Transition>
|
|
126
|
+
</div>
|
|
127
|
+
</template>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="va-dashboard-header animate-fade-in-down">
|
|
3
|
+
<div class="va-dashboard-header__content">
|
|
4
|
+
<h1 class="va-dashboard-header__title">
|
|
5
|
+
{{ greeting }}<span v-if="userName">, {{ userName }}</span>
|
|
6
|
+
</h1>
|
|
7
|
+
<p class="va-dashboard-header__date">{{ formattedDate }}</p>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div class="va-dashboard-header__actions">
|
|
11
|
+
<slot name="actions" />
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup lang="ts">
|
|
17
|
+
const props = withDefaults(defineProps<{
|
|
18
|
+
userName?: string
|
|
19
|
+
showDate?: boolean
|
|
20
|
+
}>(), {
|
|
21
|
+
userName: '',
|
|
22
|
+
showDate: true
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const greeting = computed(() => {
|
|
26
|
+
const hour = new Date().getHours()
|
|
27
|
+
if (hour < 12) return 'Good morning'
|
|
28
|
+
if (hour < 17) return 'Good afternoon'
|
|
29
|
+
return 'Good evening'
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const formattedDate = computed(() => {
|
|
33
|
+
if (!props.showDate) return ''
|
|
34
|
+
return new Date().toLocaleDateString('en-US', {
|
|
35
|
+
weekday: 'long',
|
|
36
|
+
year: 'numeric',
|
|
37
|
+
month: 'long',
|
|
38
|
+
day: 'numeric'
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<style scoped>
|
|
44
|
+
.va-dashboard-header {
|
|
45
|
+
display: flex;
|
|
46
|
+
justify-content: space-between;
|
|
47
|
+
align-items: flex-start;
|
|
48
|
+
gap: 1.5rem;
|
|
49
|
+
flex-wrap: wrap;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.va-dashboard-header__content {
|
|
53
|
+
flex: 1;
|
|
54
|
+
min-width: 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.va-dashboard-header__title {
|
|
58
|
+
font-family: var(--font-display);
|
|
59
|
+
font-size: 1.75rem;
|
|
60
|
+
font-weight: 700;
|
|
61
|
+
color: var(--color-text-primary);
|
|
62
|
+
letter-spacing: -0.02em;
|
|
63
|
+
line-height: 1.3;
|
|
64
|
+
margin: 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.dark .va-dashboard-header__title {
|
|
68
|
+
color: #ffffff;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.va-dashboard-header__date {
|
|
72
|
+
font-size: 0.9375rem;
|
|
73
|
+
color: var(--color-text-muted);
|
|
74
|
+
margin-top: 0.375rem;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.va-dashboard-header__actions {
|
|
78
|
+
flex-shrink: 0;
|
|
79
|
+
display: flex;
|
|
80
|
+
gap: 0.75rem;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@media (max-width: 640px) {
|
|
84
|
+
.va-dashboard-header {
|
|
85
|
+
flex-direction: column;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.va-dashboard-header__actions {
|
|
89
|
+
width: 100%;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.va-dashboard-header__actions :deep(button) {
|
|
93
|
+
width: 100%;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
</style>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const props = defineProps<{
|
|
3
|
+
title?: string
|
|
4
|
+
modelValue?: boolean
|
|
5
|
+
}>()
|
|
6
|
+
|
|
7
|
+
const emit = defineEmits(['update:modelValue'])
|
|
8
|
+
|
|
9
|
+
const isOpen = computed({
|
|
10
|
+
get: () => props.modelValue,
|
|
11
|
+
set: (val) => emit('update:modelValue', val)
|
|
12
|
+
})
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<UModal v-model="isOpen">
|
|
17
|
+
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
|
|
18
|
+
<template #header>
|
|
19
|
+
<div class="flex items-center justify-between">
|
|
20
|
+
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
|
21
|
+
{{ title }}
|
|
22
|
+
</h3>
|
|
23
|
+
<UButton color="gray" variant="ghost" icon="i-lucide-x" class="-my-1" @click="isOpen = false" />
|
|
24
|
+
</div>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<div class="p-4">
|
|
28
|
+
<slot />
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<template v-if="$slots.footer" #footer>
|
|
32
|
+
<slot name="footer" />
|
|
33
|
+
</template>
|
|
34
|
+
</UCard>
|
|
35
|
+
</UModal>
|
|
36
|
+
</template>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { RevenueData, RevenueCategoriesMultiple, xFormatter, yFormatter, DonutData } from '~/data/dashboardData';
|
|
3
|
+
import { computed } from 'vue';
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{ childOrder: [0 | 1, 0 | 1] }>();
|
|
6
|
+
|
|
7
|
+
const flexDirection = computed(() =>
|
|
8
|
+
props.childOrder[0] === 1 ? 'md:flex-row-reverse' : 'md:flex-row'
|
|
9
|
+
);
|
|
10
|
+
</script>
|
|
11
|
+
<template>
|
|
12
|
+
<div :class="`flex flex-col ${flexDirection} w-full px-4 md:px-8 mb-6 gap-6`">
|
|
13
|
+
<div class="w-full md:w-8/12">
|
|
14
|
+
<VaCardsRevenueBarChartCard
|
|
15
|
+
title="Revenue"
|
|
16
|
+
total-value="$4589.00"
|
|
17
|
+
:data="RevenueData"
|
|
18
|
+
:categories="RevenueCategoriesMultiple"
|
|
19
|
+
:y-axis="['total']"
|
|
20
|
+
:x-formatter="xFormatter"
|
|
21
|
+
:y-formatter="yFormatter"
|
|
22
|
+
/>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="w-full md:w-4/12 relative">
|
|
25
|
+
<VaCardsDonutChartCard
|
|
26
|
+
title="Revenue"
|
|
27
|
+
total-value="$4589.00"
|
|
28
|
+
:data="DonutData"
|
|
29
|
+
/>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</template>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { SalesData, RevenueDataCard, ReturnsData } from '~/data/dashboardData';
|
|
3
|
+
|
|
4
|
+
</script>
|
|
5
|
+
<template>
|
|
6
|
+
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-6 w-full px-4 md:px-8 mb-6">
|
|
7
|
+
<div class="w-full md:col-span-1 xl:col-span-2">
|
|
8
|
+
<VaCardsSalesCard
|
|
9
|
+
title="Sales"
|
|
10
|
+
value="1,245"
|
|
11
|
+
:chart-data="SalesData.chartData"
|
|
12
|
+
:categories="SalesData.categories"
|
|
13
|
+
/>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="w-full md:col-span-1 xl:col-span-2">
|
|
16
|
+
<VaCardsRevenueCard
|
|
17
|
+
title="Revenue"
|
|
18
|
+
value="$58,981.00"
|
|
19
|
+
:chart-data="RevenueDataCard.chartData"
|
|
20
|
+
:categories="RevenueDataCard.categories"
|
|
21
|
+
/>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="w-full md:col-span-2">
|
|
24
|
+
<VaCardsReturnsCard
|
|
25
|
+
title="Returns"
|
|
26
|
+
value="32"
|
|
27
|
+
:chart-data="ReturnsData.chartData"
|
|
28
|
+
:categories="ReturnsData.categories"
|
|
29
|
+
/>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</template>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import VaCardsLatestOrdersCard from '../Cards/VaCardsLatestOrdersCard.vue';
|
|
3
|
+
import VaCardsPopularProductsCard from '../Cards/VaCardsPopularProductsCard.vue';
|
|
4
|
+
import { orders, products } from '~/data/dashboardData';
|
|
5
|
+
|
|
6
|
+
defineProps<{ childOrder: [0 | 1, 0 | 1] }>();
|
|
7
|
+
|
|
8
|
+
const cardComponents = [VaCardsLatestOrdersCard, VaCardsPopularProductsCard] as const;
|
|
9
|
+
</script>
|
|
10
|
+
<template>
|
|
11
|
+
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-6 w-full px-4 md:px-8 mb-6">
|
|
12
|
+
<component
|
|
13
|
+
:is="cardComponents[childOrder[0]]"
|
|
14
|
+
v-bind="childOrder[0] === 0 ? { orders } : { products }"
|
|
15
|
+
class="col-span-1 md:col-span-2 xl:col-span-3"
|
|
16
|
+
/>
|
|
17
|
+
<component
|
|
18
|
+
:is="cardComponents[childOrder[1]]"
|
|
19
|
+
v-bind="childOrder[1] === 0 ? { orders } : { products }"
|
|
20
|
+
class="col-span-1 md:col-span-2 xl:col-span-3"
|
|
21
|
+
/>
|
|
22
|
+
</div>
|
|
23
|
+
</template>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
interface DonutChartData {
|
|
3
|
+
name: string;
|
|
4
|
+
value: number;
|
|
5
|
+
color: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
defineProps<{
|
|
9
|
+
data: DonutChartData[];
|
|
10
|
+
title: string;
|
|
11
|
+
totalValue: string;
|
|
12
|
+
}>();
|
|
13
|
+
</script>
|
|
14
|
+
<template>
|
|
15
|
+
<UCard>
|
|
16
|
+
<div>
|
|
17
|
+
<div class="flex items-center justify-between">
|
|
18
|
+
<h3 class="text-base font-normal text-lg text-highlighted mb-1">{{ title }}</h3>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="text-default font-medium dark:font-semibold text-2xl">
|
|
21
|
+
{{ totalValue }}
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
<DonutChart
|
|
25
|
+
:data="data.map((i) => i.value)"
|
|
26
|
+
:height="200"
|
|
27
|
+
:labels="data"
|
|
28
|
+
:hide-legend="true"
|
|
29
|
+
:radius="0"
|
|
30
|
+
>
|
|
31
|
+
<div class="absolute text-center">
|
|
32
|
+
<div class="font-semibold">Categories</div>
|
|
33
|
+
<div class="">Last 24 hours</div>
|
|
34
|
+
</div>
|
|
35
|
+
</DonutChart>
|
|
36
|
+
<div class="w-full space-y-2 divide-y divide-default">
|
|
37
|
+
<div
|
|
38
|
+
class="flex justify-between text-sm text-left py-4 font-medium text-toned tracking-tight"
|
|
39
|
+
>
|
|
40
|
+
<div>Category</div>
|
|
41
|
+
<div>Share</div>
|
|
42
|
+
</div>
|
|
43
|
+
<div
|
|
44
|
+
v-for="(cat, idx) in data"
|
|
45
|
+
:key="idx"
|
|
46
|
+
class="flex w-full items-center justify-between pb-2 text-sm text-toned dark:text-muted"
|
|
47
|
+
>
|
|
48
|
+
<div class="flex items-center gap-4">
|
|
49
|
+
<div
|
|
50
|
+
class="h-4 w-1 rounded"
|
|
51
|
+
:style="{ backgroundColor: cat.color }"
|
|
52
|
+
/>
|
|
53
|
+
<div>{{ cat.name }}</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div>{{ cat.value }}%</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</UCard>
|
|
59
|
+
</template>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<UCard>
|
|
3
|
+
<div class="flex items-center justify-between">
|
|
4
|
+
<div class="mb-2"><slot name="label" /></div>
|
|
5
|
+
<slot name="icon"/>
|
|
6
|
+
</div>
|
|
7
|
+
<div class="flex items-baseline mb-2">
|
|
8
|
+
<span class="text-3xl font-medium dark:font-semibold"><slot name="value" /></span>
|
|
9
|
+
<span class="ml-2 text-sm font-medium flex items-center">
|
|
10
|
+
<slot name="trend" />
|
|
11
|
+
</span>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="w-full rounded-md">
|
|
14
|
+
<slot name="chart" />
|
|
15
|
+
</div>
|
|
16
|
+
</UCard>
|
|
17
|
+
</template>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
interface KpiCategory {
|
|
3
|
+
name: string;
|
|
4
|
+
value: number;
|
|
5
|
+
budget: number;
|
|
6
|
+
color: string;
|
|
7
|
+
trend?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
defineProps<KpiCategory>();
|
|
11
|
+
|
|
12
|
+
const formatCurrency = (value: number) => {
|
|
13
|
+
return new Intl.NumberFormat("en-US", {
|
|
14
|
+
style: "currency",
|
|
15
|
+
currency: "USD",
|
|
16
|
+
minimumFractionDigits: 0,
|
|
17
|
+
maximumFractionDigits: 0,
|
|
18
|
+
}).format(value);
|
|
19
|
+
};
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<UCard>
|
|
24
|
+
<div class="mb-2 flex items-center justify-between">
|
|
25
|
+
<div class="flex items-center gap-4">
|
|
26
|
+
<div class="h-2 w-2 rounded-full" :style="{ backgroundColor: color }" />
|
|
27
|
+
<span class="font-medium text-default">{{ name }}</span>
|
|
28
|
+
</div>
|
|
29
|
+
<div v-if="trend" class="flex items-center gap-2 text-sm">
|
|
30
|
+
<span :class="trend >= 0 ? 'text-red-500' : 'text-(--ui-primary)'">
|
|
31
|
+
{{ trend >= 0 ? "+" : "" }}{{ trend }}%
|
|
32
|
+
</span>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div class="flex items-center justify-between">
|
|
37
|
+
<div class="text-lg font-semibold text-default">
|
|
38
|
+
{{ formatCurrency(value) }}
|
|
39
|
+
</div>
|
|
40
|
+
<div class="text-sm text-toned dark:text-muted">
|
|
41
|
+
{{ Math.round((value / budget) * 100) }}% of total
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="mt-2 h-1.5 overflow-hidden rounded-full bg-elevated">
|
|
46
|
+
<div
|
|
47
|
+
class="h-full rounded-full transition-all duration-300"
|
|
48
|
+
:style="{
|
|
49
|
+
backgroundColor: color,
|
|
50
|
+
width: `${Math.min((value / budget) * 100, 100)}%`,
|
|
51
|
+
}"
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
</UCard>
|
|
55
|
+
</template>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { TableColumn } from "@nuxt/ui";
|
|
3
|
+
|
|
4
|
+
export interface Order {
|
|
5
|
+
id: number;
|
|
6
|
+
customer: string;
|
|
7
|
+
date: string;
|
|
8
|
+
total: number;
|
|
9
|
+
status: 'Paid' | 'Pending' | 'Failed';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
defineProps<{
|
|
13
|
+
orders: Order[];
|
|
14
|
+
}>();
|
|
15
|
+
|
|
16
|
+
const columns: TableColumn<Order>[] = [
|
|
17
|
+
{
|
|
18
|
+
accessorKey: 'id',
|
|
19
|
+
header: 'Order',
|
|
20
|
+
cell: (ctx) => `#${ctx.row.getValue('id')}`
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
accessorKey: 'customer',
|
|
24
|
+
header: 'Customer',
|
|
25
|
+
cell: (ctx) => ctx.row.getValue('customer')
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
accessorKey: 'date',
|
|
29
|
+
header: 'Date',
|
|
30
|
+
cell: (ctx) => {
|
|
31
|
+
return new Date(ctx.row.getValue('date')).toLocaleString('en-US', {
|
|
32
|
+
day: 'numeric',
|
|
33
|
+
month: 'short',
|
|
34
|
+
hour: '2-digit',
|
|
35
|
+
minute: '2-digit',
|
|
36
|
+
hour12: false
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
accessorKey: 'total',
|
|
42
|
+
header: 'Total',
|
|
43
|
+
cell: (ctx) => `${ctx.row.getValue('total')}`
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
accessorKey: 'status',
|
|
47
|
+
header: 'Status',
|
|
48
|
+
cell: (ctx) => {
|
|
49
|
+
const status = ctx.row.getValue('status') as Order['status'];
|
|
50
|
+
return h(
|
|
51
|
+
resolveComponent('UBadge'),
|
|
52
|
+
{
|
|
53
|
+
label: status,
|
|
54
|
+
color: status === 'Paid' ? 'success' : status === 'Pending' ? 'warning' : 'error'
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
];
|
|
60
|
+
</script>
|
|
61
|
+
<template>
|
|
62
|
+
<UCard class="w-full">
|
|
63
|
+
<div class="flex items-center justify-between">
|
|
64
|
+
<h3 class="text-base font-semibold text-highlighted">Latest Orders</h3>
|
|
65
|
+
</div>
|
|
66
|
+
<UTable
|
|
67
|
+
:columns="columns"
|
|
68
|
+
:data="orders"
|
|
69
|
+
:ui="{ root: 'bg-transparent dark:bg-transparent !border-hidden dark:border-hidden' }"
|
|
70
|
+
>
|
|
71
|
+
<template #id="{ row }">
|
|
72
|
+
#{{ row.id }}
|
|
73
|
+
</template>
|
|
74
|
+
<template #status="{ row }">
|
|
75
|
+
<UBadge
|
|
76
|
+
:label="row.status"
|
|
77
|
+
:color="row.status === 'Paid' ? 'success' : row.status === 'Pending' ? 'warning' : 'error'"
|
|
78
|
+
/>
|
|
79
|
+
</template>
|
|
80
|
+
</UTable>
|
|
81
|
+
</UCard>
|
|
82
|
+
</template>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { h, resolveComponent } from "vue";
|
|
3
|
+
import type { TableColumn } from "@nuxt/ui";
|
|
4
|
+
|
|
5
|
+
interface Product {
|
|
6
|
+
id: number;
|
|
7
|
+
name: string;
|
|
8
|
+
category: string;
|
|
9
|
+
stock: number;
|
|
10
|
+
image: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
defineProps<{
|
|
14
|
+
products: Product[];
|
|
15
|
+
}>();
|
|
16
|
+
|
|
17
|
+
const columns: TableColumn<Product>[] = [
|
|
18
|
+
{
|
|
19
|
+
accessorKey: "image",
|
|
20
|
+
header: "Image",
|
|
21
|
+
cell: (ctx) => {
|
|
22
|
+
const image = ctx.row.getValue("image") as string;
|
|
23
|
+
const name = ctx.row.getValue("name") as string;
|
|
24
|
+
if (image) {
|
|
25
|
+
return h(
|
|
26
|
+
"div",
|
|
27
|
+
{
|
|
28
|
+
class:
|
|
29
|
+
"w-10 h-10 rounded bg-muted flex items-center justify-center overflow-hidden",
|
|
30
|
+
},
|
|
31
|
+
[h("img", { src: image, alt: name, class: "object-contain" })]
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return h(
|
|
35
|
+
"div",
|
|
36
|
+
{
|
|
37
|
+
class:
|
|
38
|
+
"w-10 h-10 rounded bg-muted flex items-center justify-center overflow-hidden",
|
|
39
|
+
},
|
|
40
|
+
[h("span", { class: "i-lucide-image text-2xl text-dimmed" })]
|
|
41
|
+
);
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
accessorKey: "name",
|
|
46
|
+
header: "Product",
|
|
47
|
+
cell: (ctx) => ctx.row.getValue("name"),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
accessorKey: "stock",
|
|
51
|
+
header: "Stock",
|
|
52
|
+
cell: (ctx) => {
|
|
53
|
+
const stock = ctx.row.getValue("stock") as number;
|
|
54
|
+
let label = "In stock",
|
|
55
|
+
color = "success";
|
|
56
|
+
if (stock === 0) {
|
|
57
|
+
label = "Out of stock";
|
|
58
|
+
color = "error";
|
|
59
|
+
} else if (stock < 10) {
|
|
60
|
+
label = "Low stock";
|
|
61
|
+
color = "warning";
|
|
62
|
+
}
|
|
63
|
+
return h(resolveComponent("UBadge"), {
|
|
64
|
+
label,
|
|
65
|
+
color,
|
|
66
|
+
variant: "subtle",
|
|
67
|
+
class: "font-medium rounded-full px-3 py-1 capitalize",
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<template>
|
|
75
|
+
<UCard class="w-full">
|
|
76
|
+
<div class="flex items-center justify-between">
|
|
77
|
+
<h3 class="text-base font-semibold text-highlighted">Popular Products</h3>
|
|
78
|
+
</div>
|
|
79
|
+
<UTable
|
|
80
|
+
:columns="columns"
|
|
81
|
+
:data="products"
|
|
82
|
+
class="w-full"
|
|
83
|
+
:ui="{
|
|
84
|
+
root: 'bg-transparent dark:bg-transparent !border-hidden dark:border-hidden',
|
|
85
|
+
}"
|
|
86
|
+
/>
|
|
87
|
+
</UCard>
|
|
88
|
+
</template>
|