refactor(dashboard): extract TimelineChart component

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-05-13 11:09:43 +01:00
parent 4e9de2d3c3
commit 0a6ce6c3c4

View 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>