feat: redesign Dashboard - greeting, amber stats, rich empty state
This commit is contained in:
@@ -1,61 +1,80 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<h1 class="text-2xl font-bold mb-6">Dashboard</h1>
|
<!-- Empty state -->
|
||||||
|
<div v-if="isEmpty" class="flex flex-col items-center justify-center py-16">
|
||||||
<!-- Stats Cards -->
|
<Clock class="w-12 h-12 text-text-tertiary" :stroke-width="1.5" />
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
<p class="text-sm text-text-secondary mt-4">Start tracking your time</p>
|
||||||
<!-- This Week -->
|
<p class="text-xs text-text-tertiary mt-2 max-w-xs text-center">Your dashboard will come alive with stats, charts, and recent activity once you start logging hours.</p>
|
||||||
<div class="bg-surface border border-border rounded-lg p-6">
|
<router-link to="/timer" class="mt-4 px-4 py-2 bg-accent text-bg-base text-xs font-medium rounded hover:bg-accent-hover transition-colors">
|
||||||
<p class="text-text-secondary text-sm mb-1">This Week</p>
|
Go to Timer
|
||||||
<p class="text-3xl font-bold text-text-primary">{{ formatDuration(weekStats.totalSeconds) }}</p>
|
</router-link>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- This Month -->
|
|
||||||
<div class="bg-surface border border-border rounded-lg p-6">
|
|
||||||
<p class="text-text-secondary text-sm mb-1">This Month</p>
|
|
||||||
<p class="text-3xl font-bold text-text-primary">{{ formatDuration(monthStats.totalSeconds) }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Active Projects -->
|
|
||||||
<div class="bg-surface border border-border rounded-lg p-6">
|
|
||||||
<p class="text-text-secondary text-sm mb-1">Active Projects</p>
|
|
||||||
<p class="text-3xl font-bold text-text-primary">{{ activeProjectsCount }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Weekly Chart -->
|
<!-- Main content -->
|
||||||
<div class="bg-surface border border-border rounded-lg p-6 mb-6">
|
<template v-else>
|
||||||
<h2 class="text-lg font-semibold mb-4">This Week's Hours</h2>
|
<!-- Greeting header -->
|
||||||
<div class="h-64">
|
<div class="mb-8">
|
||||||
<Bar v-if="chartData" :data="chartData" :options="chartOptions" />
|
<p class="text-lg text-text-secondary">{{ greeting }}</p>
|
||||||
|
<p class="text-xs text-text-tertiary mt-1">{{ formattedDate }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Recent Time Entries -->
|
<!-- Stats row — 4 columns -->
|
||||||
<div class="bg-surface border border-border rounded-lg p-6">
|
<div class="grid grid-cols-4 gap-6 mb-8">
|
||||||
<h2 class="text-lg font-semibold mb-4">Recent Entries</h2>
|
<div>
|
||||||
|
<p class="text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1">Today</p>
|
||||||
<div v-if="recentEntries.length > 0" class="space-y-3">
|
<p class="text-[1.25rem] font-mono text-accent-text font-medium">{{ formatDuration(todayStats.totalSeconds) }}</p>
|
||||||
<div
|
</div>
|
||||||
v-for="entry in recentEntries"
|
<div>
|
||||||
:key="entry.id"
|
<p class="text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1">This Week</p>
|
||||||
class="flex items-center justify-between py-2 border-b border-border last:border-0"
|
<p class="text-[1.25rem] font-mono text-accent-text font-medium">{{ formatDuration(weekStats.totalSeconds) }}</p>
|
||||||
>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div>
|
||||||
<div
|
<p class="text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1">This Month</p>
|
||||||
class="w-3 h-3 rounded-full"
|
<p class="text-[1.25rem] font-mono text-accent-text font-medium">{{ formatDuration(monthStats.totalSeconds) }}</p>
|
||||||
:style="{ backgroundColor: getProjectColor(entry.project_id) }"
|
</div>
|
||||||
></div>
|
<div>
|
||||||
<span class="text-text-primary">{{ getProjectName(entry.project_id) }}</span>
|
<p class="text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1">Active Projects</p>
|
||||||
</div>
|
<p class="text-[1.25rem] font-mono text-accent-text font-medium">{{ activeProjectsCount }}</p>
|
||||||
<span class="text-text-secondary font-mono">{{ formatDuration(entry.duration) }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="text-center text-text-secondary py-8">
|
<!-- Weekly chart -->
|
||||||
No time entries yet. Start tracking your time!
|
<div class="mb-8">
|
||||||
|
<h2 class="text-[1rem] font-medium text-text-primary mb-4">Weekly Hours</h2>
|
||||||
|
<div class="h-48">
|
||||||
|
<Bar v-if="chartData" :data="chartData" :options="chartOptions" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<!-- Recent entries -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h2 class="text-[1rem] font-medium text-text-primary">Recent Entries</h2>
|
||||||
|
<router-link to="/entries" class="text-xs text-text-tertiary hover:text-text-secondary transition-colors">View all</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="recentEntries.length > 0">
|
||||||
|
<div
|
||||||
|
v-for="entry in recentEntries"
|
||||||
|
:key="entry.id"
|
||||||
|
class="flex items-center justify-between py-3 border-b border-border-subtle last:border-0"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
class="w-2 h-2 rounded-full shrink-0"
|
||||||
|
:style="{ backgroundColor: getProjectColor(entry.project_id) }"
|
||||||
|
/>
|
||||||
|
<span class="text-[0.75rem] text-text-primary">{{ getProjectName(entry.project_id) }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-[0.75rem] font-mono text-text-secondary">{{ formatDuration(entry.duration) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-else class="text-[0.75rem] text-text-tertiary py-8">
|
||||||
|
No entries yet. Start tracking your time.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -72,6 +91,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Legend
|
Legend
|
||||||
} from 'chart.js'
|
} from 'chart.js'
|
||||||
|
import { Clock } from 'lucide-vue-next'
|
||||||
import { useEntriesStore } from '../stores/entries'
|
import { useEntriesStore } from '../stores/entries'
|
||||||
import { useProjectsStore } from '../stores/projects'
|
import { useProjectsStore } from '../stores/projects'
|
||||||
import type { TimeEntry } from '../stores/entries'
|
import type { TimeEntry } from '../stores/entries'
|
||||||
@@ -82,10 +102,34 @@ ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend)
|
|||||||
const entriesStore = useEntriesStore()
|
const entriesStore = useEntriesStore()
|
||||||
const projectsStore = useProjectsStore()
|
const projectsStore = useProjectsStore()
|
||||||
|
|
||||||
|
const todayStats = ref<{ totalSeconds: number; byProject: unknown[] }>({ totalSeconds: 0, byProject: [] })
|
||||||
const weekStats = ref<{ totalSeconds: number; byProject: unknown[] }>({ totalSeconds: 0, byProject: [] })
|
const weekStats = ref<{ totalSeconds: number; byProject: unknown[] }>({ totalSeconds: 0, byProject: [] })
|
||||||
const monthStats = ref<{ totalSeconds: number; byProject: unknown[] }>({ totalSeconds: 0, byProject: [] })
|
const monthStats = ref<{ totalSeconds: number; byProject: unknown[] }>({ totalSeconds: 0, byProject: [] })
|
||||||
const recentEntries = ref<TimeEntry[]>([])
|
const recentEntries = ref<TimeEntry[]>([])
|
||||||
|
|
||||||
|
// Greeting based on time of day
|
||||||
|
const greeting = computed(() => {
|
||||||
|
const hour = new Date().getHours()
|
||||||
|
if (hour < 12) return 'Good morning'
|
||||||
|
if (hour < 18) return 'Good afternoon'
|
||||||
|
return 'Good evening'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Formatted date
|
||||||
|
const formattedDate = computed(() => {
|
||||||
|
return new Date().toLocaleDateString('en-US', {
|
||||||
|
weekday: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Empty state check
|
||||||
|
const isEmpty = computed(() => {
|
||||||
|
return recentEntries.value.length === 0 && weekStats.value.totalSeconds === 0
|
||||||
|
})
|
||||||
|
|
||||||
// Get start of current week (Monday)
|
// Get start of current week (Monday)
|
||||||
function getWeekStart(): string {
|
function getWeekStart(): string {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
@@ -138,6 +182,8 @@ const chartData = computed(() => {
|
|||||||
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
||||||
const weekStart = getWeekStart()
|
const weekStart = getWeekStart()
|
||||||
const today = getToday()
|
const today = getToday()
|
||||||
|
const todayIndex = new Date().getDay()
|
||||||
|
const todayArrayIndex = todayIndex === 0 ? 6 : todayIndex - 1
|
||||||
|
|
||||||
// Initialize hours for each day
|
// Initialize hours for each day
|
||||||
const hoursPerDay = [0, 0, 0, 0, 0, 0, 0]
|
const hoursPerDay = [0, 0, 0, 0, 0, 0, 0]
|
||||||
@@ -147,20 +193,22 @@ const chartData = computed(() => {
|
|||||||
const entryDate = new Date(entry.start_time).toISOString().split('T')[0]
|
const entryDate = new Date(entry.start_time).toISOString().split('T')[0]
|
||||||
if (entryDate >= weekStart && entryDate <= today) {
|
if (entryDate >= weekStart && entryDate <= today) {
|
||||||
const dayIndex = new Date(entry.start_time).getDay()
|
const dayIndex = new Date(entry.start_time).getDay()
|
||||||
// Convert Monday=0, Sunday=6 to array index
|
|
||||||
const index = dayIndex === 0 ? 6 : dayIndex - 1
|
const index = dayIndex === 0 ? 6 : dayIndex - 1
|
||||||
hoursPerDay[index] += entry.duration / 3600
|
hoursPerDay[index] += entry.duration / 3600
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Color today's bar lighter amber
|
||||||
|
const colors = hoursPerDay.map((_, i) => i === todayArrayIndex ? '#FBBF24' : '#D97706')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
labels: days,
|
labels: days,
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: 'Hours',
|
label: 'Hours',
|
||||||
data: hoursPerDay,
|
data: hoursPerDay,
|
||||||
backgroundColor: '#F59E0B',
|
backgroundColor: colors,
|
||||||
borderRadius: 4
|
borderRadius: 2
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -187,10 +235,10 @@ const chartOptions = {
|
|||||||
y: {
|
y: {
|
||||||
beginAtZero: true,
|
beginAtZero: true,
|
||||||
grid: {
|
grid: {
|
||||||
color: '#2E2E2E'
|
color: '#2E2E2A'
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
color: '#A0A0A0'
|
color: '#5A5A54'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
x: {
|
x: {
|
||||||
@@ -198,7 +246,7 @@ const chartOptions = {
|
|||||||
display: false
|
display: false
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
color: '#A0A0A0'
|
color: '#5A5A54'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,13 +254,18 @@ const chartOptions = {
|
|||||||
|
|
||||||
// Load data on mount
|
// Load data on mount
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// Fetch projects first
|
|
||||||
await projectsStore.fetchProjects()
|
await projectsStore.fetchProjects()
|
||||||
|
|
||||||
// Fetch all entries
|
|
||||||
await entriesStore.fetchEntries()
|
await entriesStore.fetchEntries()
|
||||||
|
|
||||||
// Get this week's stats
|
try {
|
||||||
|
todayStats.value = await invoke('get_reports', {
|
||||||
|
startDate: getToday(),
|
||||||
|
endDate: getToday()
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch today stats:', error)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
weekStats.value = await invoke('get_reports', {
|
weekStats.value = await invoke('get_reports', {
|
||||||
startDate: getWeekStart(),
|
startDate: getWeekStart(),
|
||||||
@@ -222,7 +275,6 @@ onMounted(async () => {
|
|||||||
console.error('Failed to fetch week stats:', error)
|
console.error('Failed to fetch week stats:', error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get this month's stats
|
|
||||||
try {
|
try {
|
||||||
monthStats.value = await invoke('get_reports', {
|
monthStats.value = await invoke('get_reports', {
|
||||||
startDate: getMonthStart(),
|
startDate: getMonthStart(),
|
||||||
@@ -232,7 +284,6 @@ onMounted(async () => {
|
|||||||
console.error('Failed to fetch month stats:', error)
|
console.error('Failed to fetch month stats:', error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get recent entries (last 5)
|
|
||||||
recentEntries.value = entriesStore.entries.slice(0, 5)
|
recentEntries.value = entriesStore.entries.slice(0, 5)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user