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:
@@ -0,0 +1,7 @@
|
||||
export interface Summary {
|
||||
title: string
|
||||
icon: Component
|
||||
metric: number
|
||||
trend: number
|
||||
timeframe: 'yearly' | 'monthly' | 'weekly' | 'daily'
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user