feat: add utility functions for formatting duration and file size
This commit is contained in:
38
src/lib/format.ts
Normal file
38
src/lib/format.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Utility functions for formatting data for display
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function formatDuration(durationMs: number): string {
|
||||||
|
if (!durationMs) return "0s"
|
||||||
|
|
||||||
|
const totalSeconds = Math.floor(durationMs / 1000)
|
||||||
|
const hours = Math.floor(totalSeconds / 3600)
|
||||||
|
const minutes = Math.floor((totalSeconds % 3600) / 60)
|
||||||
|
const seconds = totalSeconds % 60
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return `${hours}h ${minutes}m ${seconds}s`
|
||||||
|
}
|
||||||
|
if (minutes > 0) {
|
||||||
|
return `${minutes}m ${seconds}s`
|
||||||
|
}
|
||||||
|
return `${seconds}s`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatFileSize(sizeBytes: number): string {
|
||||||
|
if (!sizeBytes) return "0 B"
|
||||||
|
|
||||||
|
const units = ["B", "KB", "MB", "GB", "TB"]
|
||||||
|
let size = sizeBytes
|
||||||
|
let unitIndex = 0
|
||||||
|
|
||||||
|
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||||
|
size /= 1024
|
||||||
|
unitIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unitIndex === 0) {
|
||||||
|
return `${Math.floor(size)} ${units[unitIndex]}`
|
||||||
|
}
|
||||||
|
return `${size.toFixed(1)} ${units[unitIndex]}`
|
||||||
|
}
|
||||||
@@ -1,6 +1,59 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
import { AppLayout } from '@/components/AppLayout'
|
import { AppLayout } from '@/components/AppLayout'
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Volume2, Play, Clock, HardDrive, Music } from 'lucide-react'
|
||||||
|
import { formatDuration, formatFileSize } from '@/lib/format'
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
export function DashboardPage() {
|
export function DashboardPage() {
|
||||||
|
const [soundboardStatistics, setSoundboardStatistics] = useState<SoundboardStatistics | null>(null)
|
||||||
|
const [trackStatistics, setTrackStatistics] = useState<TrackStatistics | null>(null)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchStatistics = async () => {
|
||||||
|
try {
|
||||||
|
const [soundboardResponse, trackResponse] = await Promise.all([
|
||||||
|
fetch('/api/v1/dashboard/soundboard-statistics', { credentials: 'include' }),
|
||||||
|
fetch('/api/v1/dashboard/track-statistics', { credentials: 'include' })
|
||||||
|
])
|
||||||
|
|
||||||
|
if (!soundboardResponse.ok || !trackResponse.ok) {
|
||||||
|
throw new Error('Failed to fetch statistics')
|
||||||
|
}
|
||||||
|
|
||||||
|
const [soundboardData, trackData] = await Promise.all([
|
||||||
|
soundboardResponse.json(),
|
||||||
|
trackResponse.json()
|
||||||
|
])
|
||||||
|
|
||||||
|
setSoundboardStatistics(soundboardData)
|
||||||
|
setTrackStatistics(trackData)
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'An error occurred')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchStatistics()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<AppLayout
|
<AppLayout
|
||||||
breadcrumb={{
|
breadcrumb={{
|
||||||
@@ -11,9 +64,168 @@ export function DashboardPage() {
|
|||||||
>
|
>
|
||||||
<div className="flex-1 rounded-xl bg-muted/30 p-4">
|
<div className="flex-1 rounded-xl bg-muted/30 p-4">
|
||||||
<h1 className="text-2xl font-bold mb-4">Dashboard</h1>
|
<h1 className="text-2xl font-bold mb-4">Dashboard</h1>
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
<div className="border-2 border-dashed border-muted-foreground/25 rounded-lg h-64 flex items-center justify-center">
|
{[...Array(8)].map((_, i) => (
|
||||||
<p className="text-muted-foreground">Dashboard content coming soon...</p>
|
<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>
|
||||||
|
</AppLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !soundboardStatistics || !trackStatistics) {
|
||||||
|
return (
|
||||||
|
<AppLayout
|
||||||
|
breadcrumb={{
|
||||||
|
items: [
|
||||||
|
{ label: 'Dashboard' }
|
||||||
|
]
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex-1 rounded-xl bg-muted/30 p-4">
|
||||||
|
<h1 className="text-2xl font-bold mb-4">Dashboard</h1>
|
||||||
|
<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 (
|
||||||
|
<AppLayout
|
||||||
|
breadcrumb={{
|
||||||
|
items: [
|
||||||
|
{ label: 'Dashboard' }
|
||||||
|
]
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex-1 rounded-xl bg-muted/30 p-4">
|
||||||
|
<h1 className="text-2xl font-bold mb-6">Dashboard</h1>
|
||||||
|
|
||||||
|
<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">{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">{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">{formatDuration(soundboardStatistics.total_duration)}</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">{formatFileSize(soundboardStatistics.total_size)}</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">{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">{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">{formatDuration(trackStatistics.total_duration)}</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">{formatFileSize(trackStatistics.total_size)}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Original + normalized files
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,16 @@ export default defineConfig({
|
|||||||
'@': path.resolve(__dirname, './src'),
|
'@': path.resolve(__dirname, './src'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// Development server configuration
|
||||||
|
server: {
|
||||||
|
port: 8001,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8000',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
// Production build optimization
|
// Production build optimization
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist',
|
outDir: 'dist',
|
||||||
|
|||||||
Reference in New Issue
Block a user