feat(pub/base): add reusable Summary Card + Summary type

- Add `summary-card.vue` with skeleton state 
- Add `summary-card.type.ts` with `Summary` interface and timeframe mapping 📅
This commit is contained in:
Khafid Prayoga
2025-08-12 14:38:20 +07:00
parent cecd591f47
commit 7f7dfe0a02
2 changed files with 78 additions and 0 deletions
@@ -0,0 +1,7 @@
export interface Summary {
title: string
icon: Component
metric: number
trend: number
timeframe: 'yearly' | 'monthly' | 'weekly' | 'daily'
}
+71
View File
@@ -0,0 +1,71 @@
<script setup lang="ts">
import type { Summary } from './summary-card.type'
import { ChevronDown, ChevronUp } from 'lucide-vue-next'
import { cn } from '~/lib/utils'
const props = defineProps<{
stat?: Summary
isSkeleton?: boolean
}>()
const timeFrame = computed((): string => {
if (!props.stat?.timeframe) return 'from unknown timeframe'
let word: string = ''
switch (props.stat.timeframe) {
case 'daily':
word = 'from yesterday'
break
case 'weekly':
word = 'from last week'
break
case 'monthly':
word = 'from last month'
break
case 'yearly':
word = 'from last year'
break
}
return word
})
const isTrending = computed<boolean>(() => (props.stat?.trend ?? 0) > 0)
</script>
<template>
<Card v-if="props.isSkeleton">
<CardHeader class="flex flex-row items-center justify-between pb-2">
<Skeleton class="h-6 w-32 bg-gray-100 text-sm font-medium" />
<Skeleton class="h-4 w-4 bg-gray-100" />
</CardHeader>
<CardContent>
<Skeleton class="mb-2 h-6 w-48 bg-gray-100 text-2xl" />
<Skeleton class="h-4 w-64 bg-gray-100 text-xs font-medium" />
</CardContent>
</Card>
<Card v-else-if="props.stat && !props.isSkeleton">
<CardHeader class="flex flex-row items-center justify-between pb-2">
<CardTitle class="text-sm font-medium"> {{ props.stat.title }} </CardTitle>
<component :is="props.stat.icon" class="text-muted-foreground h-4 w-4" />
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">
{{ props.stat.metric.toLocaleString('id-ID') }}
</div>
<p class="text-muted-foreground flex items-center gap-1 text-xs">
<component
:is="isTrending ? ChevronUp : ChevronDown"
:class="cn('h-4 w-4', { 'text-green-500': isTrending }, { 'text-red-500': !isTrending })"
/>
<span :class="cn('font-medium', { 'text-green-500': isTrending }, { 'text-red-500': !isTrending })">
{{ props.stat.trend.toFixed(1) }}%
<!-- {{ Math.abs(props.stat.trend).toFixed(1) }}% -->
</span>
<span>{{ timeFrame }}</span>
</p>
</CardContent>
</Card>
</template>
<style scoped></style>