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 { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { NumberFlowDuration } from '@/components/ui/number-flow-duration'
|
||||
import { NumberFlowSize } from '@/components/ui/number-flow-size'
|
||||
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 { DashboardHeader } from '@/components/dashboard/DashboardHeader'
|
||||
import { ErrorState, LoadingSkeleton } from '@/components/dashboard/DashboardLoadingStates'
|
||||
import { StatisticsGrid } from '@/components/dashboard/StatisticsGrid'
|
||||
import { TopSoundsSection } from '@/components/dashboard/TopSoundsSection'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
interface SoundboardStatistics {
|
||||
@@ -176,94 +158,11 @@ export function DashboardPage() {
|
||||
}, [fetchTopSounds])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
return <LoadingSkeleton title="Dashboard" description="Loading dashboard statistics..." />
|
||||
}
|
||||
|
||||
if (error || !soundboardStatistics || !trackStatistics) {
|
||||
return (
|
||||
<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 <ErrorState error={error || 'Failed to load statistics'} />
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -273,307 +172,25 @@ export function DashboardPage() {
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<Button
|
||||
onClick={refreshAll}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={refreshing}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
<DashboardHeader onRefresh={refreshAll} isRefreshing={refreshing} />
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Soundboard Statistics */}
|
||||
<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">
|
||||
<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>
|
||||
<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={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"
|
||||
<StatisticsGrid
|
||||
soundboardStatistics={soundboardStatistics}
|
||||
trackStatistics={trackStatistics}
|
||||
/>
|
||||
</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}
|
||||
<TopSoundsSection
|
||||
topSounds={topSounds}
|
||||
loading={topSoundsLoading}
|
||||
soundType={soundType}
|
||||
period={period}
|
||||
limit={limit}
|
||||
onSoundTypeChange={setSoundType}
|
||||
onPeriodChange={setPeriod}
|
||||
onLimitChange={setLimit}
|
||||
/>
|
||||
</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>
|
||||
</AppLayout>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user