feat: implement dashboard components including header, loading states, statistics grid, and top sounds section
This commit is contained in:
30
src/components/dashboard/DashboardHeader.tsx
Normal file
30
src/components/dashboard/DashboardHeader.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { RefreshCw } from 'lucide-react'
|
||||||
|
|
||||||
|
interface DashboardHeaderProps {
|
||||||
|
onRefresh: () => void
|
||||||
|
isRefreshing: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DashboardHeader({ onRefresh, isRefreshing }: DashboardHeaderProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold">Dashboard</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Overview of your soundboard and track statistics
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={onRefresh}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={isRefreshing}
|
||||||
|
>
|
||||||
|
<RefreshCw
|
||||||
|
className={`h-4 w-4 ${isRefreshing ? 'animate-spin' : ''}`}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
79
src/components/dashboard/DashboardLoadingStates.tsx
Normal file
79
src/components/dashboard/DashboardLoadingStates.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { AppLayout } from '@/components/AppLayout'
|
||||||
|
import { DashboardHeader } from '@/components/dashboard/DashboardHeader'
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
|
||||||
|
interface LoadingSkeletonProps {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoadingSkeleton({ title, description }: LoadingSkeletonProps) {
|
||||||
|
return (
|
||||||
|
<AppLayout
|
||||||
|
breadcrumb={{
|
||||||
|
items: [{ label: 'Dashboard' }],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex-1 rounded-xl bg-muted/50 p-4">
|
||||||
|
<DashboardHeader onRefresh={() => {}} isRefreshing={false} />
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold mb-3 text-muted-foreground">
|
||||||
|
Soundboard Statistics
|
||||||
|
</h2>
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{[...Array(4)].map((_, i) => (
|
||||||
|
<Card key={i}>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">
|
||||||
|
Loading...
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold animate-pulse">---</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold mb-3 text-muted-foreground">
|
||||||
|
Track Statistics
|
||||||
|
</h2>
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{[...Array(4)].map((_, i) => (
|
||||||
|
<Card key={i + 4}>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">
|
||||||
|
Loading...
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold animate-pulse">---</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AppLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ErrorState({ error }: { error: string }) {
|
||||||
|
return (
|
||||||
|
<AppLayout
|
||||||
|
breadcrumb={{
|
||||||
|
items: [{ label: 'Dashboard' }],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex-1 rounded-xl bg-muted/50 p-4">
|
||||||
|
<DashboardHeader onRefresh={() => {}} isRefreshing={false} />
|
||||||
|
<div className="border-2 border-dashed border-destructive/25 rounded-lg p-4">
|
||||||
|
<p className="text-destructive">Error loading statistics: {error}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AppLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
25
src/components/dashboard/StatisticCard.tsx
Normal file
25
src/components/dashboard/StatisticCard.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { LucideIcon } from 'lucide-react'
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
interface StatisticCardProps {
|
||||||
|
title: string
|
||||||
|
icon: LucideIcon
|
||||||
|
value: ReactNode
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StatisticCard({ title, icon: Icon, value, description }: StatisticCardProps) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
||||||
|
<Icon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{value}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">{description}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
114
src/components/dashboard/StatisticsGrid.tsx
Normal file
114
src/components/dashboard/StatisticsGrid.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { StatisticCard } from '@/components/dashboard/StatisticCard'
|
||||||
|
import { NumberFlowDuration } from '@/components/ui/number-flow-duration'
|
||||||
|
import { NumberFlowSize } from '@/components/ui/number-flow-size'
|
||||||
|
import NumberFlow from '@number-flow/react'
|
||||||
|
import { Clock, HardDrive, Music, Play, Volume2 } from 'lucide-react'
|
||||||
|
|
||||||
|
interface SoundboardStatistics {
|
||||||
|
sound_count: number
|
||||||
|
total_play_count: number
|
||||||
|
total_duration: number
|
||||||
|
total_size: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TrackStatistics {
|
||||||
|
track_count: number
|
||||||
|
total_play_count: number
|
||||||
|
total_duration: number
|
||||||
|
total_size: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StatisticsGridProps {
|
||||||
|
soundboardStatistics: SoundboardStatistics
|
||||||
|
trackStatistics: TrackStatistics
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StatisticsGrid({ soundboardStatistics, trackStatistics }: StatisticsGridProps) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold mb-3 text-muted-foreground">
|
||||||
|
Soundboard Statistics
|
||||||
|
</h2>
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
<StatisticCard
|
||||||
|
title="Total Sounds"
|
||||||
|
icon={Volume2}
|
||||||
|
value={<NumberFlow value={soundboardStatistics.sound_count} />}
|
||||||
|
description="Soundboard audio files"
|
||||||
|
/>
|
||||||
|
<StatisticCard
|
||||||
|
title="Total Plays"
|
||||||
|
icon={Play}
|
||||||
|
value={<NumberFlow value={soundboardStatistics.total_play_count} />}
|
||||||
|
description="All-time play count"
|
||||||
|
/>
|
||||||
|
<StatisticCard
|
||||||
|
title="Total Duration"
|
||||||
|
icon={Clock}
|
||||||
|
value={
|
||||||
|
<NumberFlowDuration
|
||||||
|
duration={soundboardStatistics.total_duration}
|
||||||
|
variant="wordy"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
description="Combined audio duration"
|
||||||
|
/>
|
||||||
|
<StatisticCard
|
||||||
|
title="Total Size"
|
||||||
|
icon={HardDrive}
|
||||||
|
value={
|
||||||
|
<NumberFlowSize
|
||||||
|
size={soundboardStatistics.total_size}
|
||||||
|
binary={true}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
description="Original + normalized files"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold mb-3 text-muted-foreground">
|
||||||
|
Track Statistics
|
||||||
|
</h2>
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
<StatisticCard
|
||||||
|
title="Total Tracks"
|
||||||
|
icon={Music}
|
||||||
|
value={<NumberFlow value={trackStatistics.track_count} />}
|
||||||
|
description="Extracted audio tracks"
|
||||||
|
/>
|
||||||
|
<StatisticCard
|
||||||
|
title="Total Plays"
|
||||||
|
icon={Play}
|
||||||
|
value={<NumberFlow value={trackStatistics.total_play_count} />}
|
||||||
|
description="All-time play count"
|
||||||
|
/>
|
||||||
|
<StatisticCard
|
||||||
|
title="Total Duration"
|
||||||
|
icon={Clock}
|
||||||
|
value={
|
||||||
|
<NumberFlowDuration
|
||||||
|
duration={trackStatistics.total_duration}
|
||||||
|
variant="wordy"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
description="Combined track duration"
|
||||||
|
/>
|
||||||
|
<StatisticCard
|
||||||
|
title="Total Size"
|
||||||
|
icon={HardDrive}
|
||||||
|
value={
|
||||||
|
<NumberFlowSize
|
||||||
|
size={trackStatistics.total_size}
|
||||||
|
binary={true}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
description="Original + normalized files"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
156
src/components/dashboard/TopSoundsSection.tsx
Normal file
156
src/components/dashboard/TopSoundsSection.tsx
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { NumberFlowDuration } from '@/components/ui/number-flow-duration'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select'
|
||||||
|
import NumberFlow from '@number-flow/react'
|
||||||
|
import { Clock, Loader2, Music, Trophy } from 'lucide-react'
|
||||||
|
|
||||||
|
interface TopSound {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
play_count: number
|
||||||
|
duration: number | null
|
||||||
|
created_at: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TopSoundsSectionProps {
|
||||||
|
topSounds: TopSound[]
|
||||||
|
loading: boolean
|
||||||
|
soundType: string
|
||||||
|
period: string
|
||||||
|
limit: number
|
||||||
|
onSoundTypeChange: (value: string) => void
|
||||||
|
onPeriodChange: (value: string) => void
|
||||||
|
onLimitChange: (value: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TopSoundsSection({
|
||||||
|
topSounds,
|
||||||
|
loading,
|
||||||
|
soundType,
|
||||||
|
period,
|
||||||
|
limit,
|
||||||
|
onSoundTypeChange,
|
||||||
|
onPeriodChange,
|
||||||
|
onLimitChange,
|
||||||
|
}: TopSoundsSectionProps) {
|
||||||
|
return (
|
||||||
|
<div className="mt-8">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Trophy className="h-5 w-5" />
|
||||||
|
<CardTitle>Top Sounds</CardTitle>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-medium">Type:</span>
|
||||||
|
<Select value={soundType} onValueChange={onSoundTypeChange}>
|
||||||
|
<SelectTrigger className="w-32">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All</SelectItem>
|
||||||
|
<SelectItem value="SDB">Soundboard</SelectItem>
|
||||||
|
<SelectItem value="EXT">Tracks</SelectItem>
|
||||||
|
<SelectItem value="TTS">TTS</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-medium">Period:</span>
|
||||||
|
<Select value={period} onValueChange={onPeriodChange}>
|
||||||
|
<SelectTrigger className="w-32">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="today">Today</SelectItem>
|
||||||
|
<SelectItem value="1_day">1 Day</SelectItem>
|
||||||
|
<SelectItem value="1_week">1 Week</SelectItem>
|
||||||
|
<SelectItem value="1_month">1 Month</SelectItem>
|
||||||
|
<SelectItem value="1_year">1 Year</SelectItem>
|
||||||
|
<SelectItem value="all_time">All Time</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-medium">Count:</span>
|
||||||
|
<Select
|
||||||
|
value={limit.toString()}
|
||||||
|
onValueChange={value => onLimitChange(parseInt(value))}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-20">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="5">5</SelectItem>
|
||||||
|
<SelectItem value="10">10</SelectItem>
|
||||||
|
<SelectItem value="25">25</SelectItem>
|
||||||
|
<SelectItem value="50">50</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin mr-2" />
|
||||||
|
Loading top sounds...
|
||||||
|
</div>
|
||||||
|
) : topSounds.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
<Music className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
||||||
|
<p>No sounds found for the selected criteria</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{topSounds.map((sound, index) => (
|
||||||
|
<div
|
||||||
|
key={sound.id}
|
||||||
|
className="flex items-center gap-4 p-3 bg-muted/30 rounded-lg"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center w-8 h-8 bg-primary text-primary-foreground rounded-full font-bold text-sm">
|
||||||
|
{index + 1}
|
||||||
|
</div>
|
||||||
|
<Music className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="font-medium truncate">{sound.name}</div>
|
||||||
|
<div className="flex items-center gap-4 text-xs text-muted-foreground mt-1">
|
||||||
|
{sound.duration && (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Clock className="h-3 w-3" />
|
||||||
|
<NumberFlowDuration
|
||||||
|
duration={sound.duration}
|
||||||
|
variant="wordy"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="px-1.5 py-0.5 bg-secondary rounded text-xs">
|
||||||
|
{sound.type}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-2xl font-bold text-primary">
|
||||||
|
<NumberFlow value={sound.play_count} />
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">plays</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,26 +1,8 @@
|
|||||||
import { AppLayout } from '@/components/AppLayout'
|
import { AppLayout } from '@/components/AppLayout'
|
||||||
import { Button } from '@/components/ui/button'
|
import { DashboardHeader } from '@/components/dashboard/DashboardHeader'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { ErrorState, LoadingSkeleton } from '@/components/dashboard/DashboardLoadingStates'
|
||||||
import { NumberFlowDuration } from '@/components/ui/number-flow-duration'
|
import { StatisticsGrid } from '@/components/dashboard/StatisticsGrid'
|
||||||
import { NumberFlowSize } from '@/components/ui/number-flow-size'
|
import { TopSoundsSection } from '@/components/dashboard/TopSoundsSection'
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select'
|
|
||||||
import NumberFlow from '@number-flow/react'
|
|
||||||
import {
|
|
||||||
Clock,
|
|
||||||
HardDrive,
|
|
||||||
Loader2,
|
|
||||||
Music,
|
|
||||||
Play,
|
|
||||||
RefreshCw,
|
|
||||||
Trophy,
|
|
||||||
Volume2,
|
|
||||||
} from 'lucide-react'
|
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
interface SoundboardStatistics {
|
interface SoundboardStatistics {
|
||||||
@@ -176,94 +158,11 @@ export function DashboardPage() {
|
|||||||
}, [fetchTopSounds])
|
}, [fetchTopSounds])
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return <LoadingSkeleton title="Dashboard" description="Loading dashboard statistics..." />
|
||||||
<AppLayout
|
|
||||||
breadcrumb={{
|
|
||||||
items: [{ label: 'Dashboard' }],
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex-1 rounded-xl bg-muted/50 p-4">
|
|
||||||
<div className="flex items-center justify-between mb-6">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-bold">Dashboard</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Overview of your soundboard and track statistics
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-lg font-semibold mb-3 text-muted-foreground">
|
|
||||||
Soundboard Statistics
|
|
||||||
</h2>
|
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
||||||
{[...Array(4)].map((_, i) => (
|
|
||||||
<Card key={i}>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">
|
|
||||||
Loading...
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold animate-pulse">
|
|
||||||
---
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 className="text-lg font-semibold mb-3 text-muted-foreground">
|
|
||||||
Track Statistics
|
|
||||||
</h2>
|
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
||||||
{[...Array(4)].map((_, i) => (
|
|
||||||
<Card key={i + 4}>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">
|
|
||||||
Loading...
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold animate-pulse">
|
|
||||||
---
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AppLayout>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error || !soundboardStatistics || !trackStatistics) {
|
if (error || !soundboardStatistics || !trackStatistics) {
|
||||||
return (
|
return <ErrorState error={error || 'Failed to load statistics'} />
|
||||||
<AppLayout
|
|
||||||
breadcrumb={{
|
|
||||||
items: [{ label: 'Dashboard' }],
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex-1 rounded-xl bg-muted/50 p-4">
|
|
||||||
<div className="flex items-center justify-between mb-6">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-bold">Dashboard</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Overview of your soundboard and track statistics
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="border-2 border-dashed border-destructive/25 rounded-lg p-4">
|
|
||||||
<p className="text-destructive">
|
|
||||||
Error loading statistics: {error}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AppLayout>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -273,306 +172,24 @@ export function DashboardPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex-1 rounded-xl bg-muted/50 p-4">
|
<div className="flex-1 rounded-xl bg-muted/50 p-4">
|
||||||
<div className="flex items-center justify-between mb-6">
|
<DashboardHeader onRefresh={refreshAll} isRefreshing={refreshing} />
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-bold">Dashboard</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Overview of your soundboard and track statistics
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
onClick={refreshAll}
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
disabled={refreshing}
|
|
||||||
>
|
|
||||||
<RefreshCw
|
|
||||||
className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Soundboard Statistics */}
|
<StatisticsGrid
|
||||||
<div>
|
soundboardStatistics={soundboardStatistics}
|
||||||
<h2 className="text-lg font-semibold mb-3 text-muted-foreground">
|
trackStatistics={trackStatistics}
|
||||||
Soundboard Statistics
|
/>
|
||||||
</h2>
|
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">
|
|
||||||
Total Sounds
|
|
||||||
</CardTitle>
|
|
||||||
<Volume2 className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">
|
|
||||||
<NumberFlow value={soundboardStatistics.sound_count} />
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Soundboard audio files
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
<TopSoundsSection
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
topSounds={topSounds}
|
||||||
<CardTitle className="text-sm font-medium">
|
loading={topSoundsLoading}
|
||||||
Total Plays
|
soundType={soundType}
|
||||||
</CardTitle>
|
period={period}
|
||||||
<Play className="h-4 w-4 text-muted-foreground" />
|
limit={limit}
|
||||||
</CardHeader>
|
onSoundTypeChange={setSoundType}
|
||||||
<CardContent>
|
onPeriodChange={setPeriod}
|
||||||
<div className="text-2xl font-bold">
|
onLimitChange={setLimit}
|
||||||
<NumberFlow value={soundboardStatistics.total_play_count} />
|
/>
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
All-time play count
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">
|
|
||||||
Total Duration
|
|
||||||
</CardTitle>
|
|
||||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">
|
|
||||||
<NumberFlowDuration
|
|
||||||
duration={soundboardStatistics.total_duration}
|
|
||||||
variant="wordy"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Combined audio duration
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">
|
|
||||||
Total Size
|
|
||||||
</CardTitle>
|
|
||||||
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">
|
|
||||||
<NumberFlowSize
|
|
||||||
size={soundboardStatistics.total_size}
|
|
||||||
binary={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Original + normalized files
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Track Statistics */}
|
|
||||||
<div>
|
|
||||||
<h2 className="text-lg font-semibold mb-3 text-muted-foreground">
|
|
||||||
Track Statistics
|
|
||||||
</h2>
|
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">
|
|
||||||
Total Tracks
|
|
||||||
</CardTitle>
|
|
||||||
<Music className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">
|
|
||||||
<NumberFlow value={trackStatistics.track_count} />
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Extracted audio tracks
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">
|
|
||||||
Total Plays
|
|
||||||
</CardTitle>
|
|
||||||
<Play className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">
|
|
||||||
<NumberFlow value={trackStatistics.total_play_count} />
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
All-time play count
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">
|
|
||||||
Total Duration
|
|
||||||
</CardTitle>
|
|
||||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">
|
|
||||||
<NumberFlowDuration
|
|
||||||
duration={trackStatistics.total_duration}
|
|
||||||
variant="wordy"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Combined track duration
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">
|
|
||||||
Total Size
|
|
||||||
</CardTitle>
|
|
||||||
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">
|
|
||||||
<NumberFlowSize
|
|
||||||
size={trackStatistics.total_size}
|
|
||||||
binary={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Original + normalized files
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Top Sounds Section */}
|
|
||||||
<div className="mt-8">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Trophy className="h-5 w-5" />
|
|
||||||
<CardTitle>Top Sounds</CardTitle>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-sm font-medium">Type:</span>
|
|
||||||
<Select value={soundType} onValueChange={setSoundType}>
|
|
||||||
<SelectTrigger className="w-32">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">All</SelectItem>
|
|
||||||
<SelectItem value="SDB">Soundboard</SelectItem>
|
|
||||||
<SelectItem value="EXT">Tracks</SelectItem>
|
|
||||||
<SelectItem value="TTS">TTS</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-sm font-medium">Period:</span>
|
|
||||||
<Select value={period} onValueChange={setPeriod}>
|
|
||||||
<SelectTrigger className="w-32">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="today">Today</SelectItem>
|
|
||||||
<SelectItem value="1_day">1 Day</SelectItem>
|
|
||||||
<SelectItem value="1_week">1 Week</SelectItem>
|
|
||||||
<SelectItem value="1_month">1 Month</SelectItem>
|
|
||||||
<SelectItem value="1_year">1 Year</SelectItem>
|
|
||||||
<SelectItem value="all_time">All Time</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-sm font-medium">Count:</span>
|
|
||||||
<Select
|
|
||||||
value={limit.toString()}
|
|
||||||
onValueChange={value => setLimit(parseInt(value))}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-20">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="5">5</SelectItem>
|
|
||||||
<SelectItem value="10">10</SelectItem>
|
|
||||||
<SelectItem value="25">25</SelectItem>
|
|
||||||
<SelectItem value="50">50</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{topSoundsLoading ? (
|
|
||||||
<div className="flex items-center justify-center py-8">
|
|
||||||
<Loader2 className="h-6 w-6 animate-spin mr-2" />
|
|
||||||
Loading top sounds...
|
|
||||||
</div>
|
|
||||||
) : topSounds.length === 0 ? (
|
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
|
||||||
<Music className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
|
||||||
<p>No sounds found for the selected criteria</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{topSounds.map((sound, index) => (
|
|
||||||
<div
|
|
||||||
key={sound.id}
|
|
||||||
className="flex items-center gap-4 p-3 bg-muted/30 rounded-lg"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-center w-8 h-8 bg-primary text-primary-foreground rounded-full font-bold text-sm">
|
|
||||||
{index + 1}
|
|
||||||
</div>
|
|
||||||
<Music className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="font-medium truncate">
|
|
||||||
{sound.name}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-4 text-xs text-muted-foreground mt-1">
|
|
||||||
{sound.duration && (
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<Clock className="h-3 w-3" />
|
|
||||||
<NumberFlowDuration
|
|
||||||
duration={sound.duration}
|
|
||||||
variant="wordy"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span className="px-1.5 py-0.5 bg-secondary rounded text-xs">
|
|
||||||
{sound.type}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-right">
|
|
||||||
<div className="text-2xl font-bold text-primary">
|
|
||||||
<NumberFlow value={sound.play_count} />
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-muted-foreground">
|
|
||||||
plays
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
|
|||||||
Reference in New Issue
Block a user