feat: enhance DashboardPage with data fetching improvements, auto-refresh, and UI updates
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { AppLayout } from '@/components/AppLayout'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Volume2, Play, Clock, HardDrive, Music, Trophy, Loader2 } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Volume2, Play, Clock, HardDrive, Music, Trophy, Loader2, RefreshCw } from 'lucide-react'
|
||||
import { formatDuration, formatFileSize } from '@/lib/format'
|
||||
import NumberFlow from '@number-flow/react'
|
||||
|
||||
interface SoundboardStatistics {
|
||||
sound_count: number
|
||||
@@ -40,10 +42,37 @@ export function DashboardPage() {
|
||||
const [soundType, setSoundType] = useState('SDB')
|
||||
const [period, setPeriod] = useState('all_time')
|
||||
const [limit, setLimit] = useState(5)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
|
||||
const fetchTopSounds = async () => {
|
||||
const fetchStatistics = useCallback(async () => {
|
||||
try {
|
||||
setTopSoundsLoading(true)
|
||||
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')
|
||||
}
|
||||
}, [])
|
||||
|
||||
const fetchTopSounds = useCallback(async (showLoading = false) => {
|
||||
try {
|
||||
if (showLoading) {
|
||||
setTopSoundsLoading(true)
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`/api/v1/dashboard/top-sounds?sound_type=${soundType}&period=${period}&limit=${limit}`,
|
||||
{ credentials: 'include' }
|
||||
@@ -54,46 +83,75 @@ export function DashboardPage() {
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
setTopSounds(data)
|
||||
|
||||
// Graceful update: merge new data while preserving animations
|
||||
setTopSounds(prevTopSounds => {
|
||||
// Create a map of existing sounds for efficient lookup
|
||||
const existingSoundsMap = new Map(prevTopSounds.map(sound => [sound.id, sound]))
|
||||
|
||||
// Update existing sounds and add new ones
|
||||
return data.map((newSound: TopSound) => {
|
||||
const existingSound = existingSoundsMap.get(newSound.id)
|
||||
if (existingSound) {
|
||||
// Preserve object reference if data hasn't changed to avoid re-renders
|
||||
if (
|
||||
existingSound.name === newSound.name &&
|
||||
existingSound.type === newSound.type &&
|
||||
existingSound.play_count === newSound.play_count &&
|
||||
existingSound.duration === newSound.duration
|
||||
) {
|
||||
return existingSound
|
||||
}
|
||||
}
|
||||
return newSound
|
||||
})
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch top sounds:', err)
|
||||
} finally {
|
||||
setTopSoundsLoading(false)
|
||||
if (showLoading) {
|
||||
setTopSoundsLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [soundType, period, limit])
|
||||
|
||||
const refreshAll = useCallback(async () => {
|
||||
setRefreshing(true)
|
||||
try {
|
||||
await Promise.all([
|
||||
fetchStatistics(),
|
||||
fetchTopSounds()
|
||||
])
|
||||
} finally {
|
||||
setRefreshing(false)
|
||||
}
|
||||
}, [fetchStatistics, fetchTopSounds])
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStatistics = async () => {
|
||||
const loadInitialData = async () => {
|
||||
setLoading(true)
|
||||
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')
|
||||
await fetchStatistics()
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchStatistics()
|
||||
loadInitialData()
|
||||
}, [])
|
||||
|
||||
// Auto-refresh every 5 seconds
|
||||
useEffect(() => {
|
||||
fetchTopSounds()
|
||||
}, [soundType, period, limit])
|
||||
const interval = setInterval(() => {
|
||||
refreshAll()
|
||||
}, 5000)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [refreshAll])
|
||||
|
||||
useEffect(() => {
|
||||
fetchTopSounds(true) // Show loading on initial load and filter changes
|
||||
}, [fetchTopSounds])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -192,6 +250,14 @@ export function DashboardPage() {
|
||||
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">
|
||||
@@ -218,7 +284,7 @@ export function DashboardPage() {
|
||||
<Play className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{soundboardStatistics.total_play_count}</div>
|
||||
<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>
|
||||
@@ -390,10 +456,6 @@ export function DashboardPage() {
|
||||
<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">
|
||||
<span className="flex items-center gap-1">
|
||||
<Play className="h-3 w-3" />
|
||||
{sound.play_count} plays
|
||||
</span>
|
||||
{sound.duration && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
@@ -405,6 +467,10 @@ export function DashboardPage() {
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user