refactor(dashboard): extract TimelineChart component
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4e9de2d3c3
commit
0a6ce6c3c4
1 changed files with 99 additions and 0 deletions
99
web/src/components/dashboard/TimelineChart.vue
Normal file
99
web/src/components/dashboard/TimelineChart.vue
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import Card from '@/components/ui/Card.vue'
|
||||
import CardHeader from '@/components/ui/CardHeader.vue'
|
||||
import CardTitle from '@/components/ui/CardTitle.vue'
|
||||
import CardContent from '@/components/ui/CardContent.vue'
|
||||
import { formatDuration } from '@/lib/utils'
|
||||
import type { DailyPoint, DowDataPoint } from '@/types'
|
||||
|
||||
const props = defineProps<{
|
||||
daily: DailyPoint[]
|
||||
dow: DowDataPoint[]
|
||||
loading: boolean
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const maxDailyHours = computed(() => Math.max(...props.daily.map((d) => d.hours), 1))
|
||||
const maxDowHours = computed(() => Math.max(...props.dow.map((d) => d.hours), 1))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<!-- Hours by Day bar chart -->
|
||||
<Card class="border-border/60 bg-card panel-glow">
|
||||
<CardHeader class="pb-2">
|
||||
<CardTitle class="text-xs font-semibold text-muted-foreground uppercase tracking-widest">Hours by Day</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<!-- Loading skeleton -->
|
||||
<div v-if="loading" class="h-40 flex items-end gap-px">
|
||||
<div
|
||||
v-for="i in 30" :key="i"
|
||||
class="flex-1 bg-muted animate-pulse rounded-t"
|
||||
:style="{ height: `${20 + Math.random() * 60}%` }"
|
||||
/>
|
||||
</div>
|
||||
<!-- Empty state -->
|
||||
<div v-else-if="daily.length === 0" class="h-40 flex flex-col items-center justify-center gap-2">
|
||||
<svg class="h-8 w-8 text-muted-foreground/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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>
|
||||
<p class="text-xs text-muted-foreground">No sessions in this period</p>
|
||||
</div>
|
||||
<!-- Chart -->
|
||||
<div v-else class="h-40 flex items-end gap-px overflow-hidden">
|
||||
<div
|
||||
v-for="point in daily"
|
||||
:key="point.date"
|
||||
class="flex-1 bg-primary/70 hover:bg-primary rounded-t transition-colors duration-150 cursor-pointer"
|
||||
:style="{ height: `${Math.max((point.hours / maxDailyHours) * 160, 2)}px` }"
|
||||
:title="`${point.date}: ${formatDuration(point.hours)}`"
|
||||
@click="router.push({ path: '/calendar', query: { date: point.date } })"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Day of week bar chart -->
|
||||
<Card class="border-border/60 bg-card panel-glow">
|
||||
<CardHeader class="pb-2">
|
||||
<CardTitle class="text-xs font-semibold text-muted-foreground uppercase tracking-widest">By Day of Week</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<!-- Loading skeleton -->
|
||||
<div v-if="loading" class="h-40 flex items-end gap-2">
|
||||
<div v-for="i in 7" :key="i" class="flex-1 flex flex-col items-center gap-1">
|
||||
<div class="w-full bg-muted animate-pulse rounded-t" :style="{ height: `${30 + i * 8}%` }" />
|
||||
<div class="h-3 w-4 bg-muted animate-pulse rounded" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Empty state -->
|
||||
<div v-else-if="dow.length === 0" class="h-40 flex flex-col items-center justify-center gap-2">
|
||||
<svg class="h-8 w-8 text-muted-foreground/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<p class="text-xs text-muted-foreground">No sessions in this period</p>
|
||||
</div>
|
||||
<!-- Chart -->
|
||||
<div v-else class="flex items-end gap-2" style="height: 160px">
|
||||
<div
|
||||
v-for="point in dow"
|
||||
:key="point.dow"
|
||||
class="flex-1 flex flex-col items-center gap-1 cursor-default"
|
||||
style="height: 160px; justify-content: flex-end"
|
||||
>
|
||||
<div
|
||||
class="w-full bg-primary/70 hover:bg-primary rounded-t transition-colors duration-150"
|
||||
:style="{ height: `${Math.max((point.hours / maxDowHours) * 128, 2)}px` }"
|
||||
:title="`${point.label}: ${formatDuration(point.hours)}`"
|
||||
/>
|
||||
<span class="text-[10px] text-muted-foreground font-medium">{{ point.label.slice(0, 2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
Loading…
Add table
Reference in a new issue