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 { AppLayout } from '@/components/AppLayout'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
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 { formatDuration, formatFileSize } from '@/lib/format'
|
||||||
|
import NumberFlow from '@number-flow/react'
|
||||||
|
|
||||||
interface SoundboardStatistics {
|
interface SoundboardStatistics {
|
||||||
sound_count: number
|
sound_count: number
|
||||||
@@ -40,10 +42,37 @@ export function DashboardPage() {
|
|||||||
const [soundType, setSoundType] = useState('SDB')
|
const [soundType, setSoundType] = useState('SDB')
|
||||||
const [period, setPeriod] = useState('all_time')
|
const [period, setPeriod] = useState('all_time')
|
||||||
const [limit, setLimit] = useState(5)
|
const [limit, setLimit] = useState(5)
|
||||||
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
|
||||||
const fetchTopSounds = async () => {
|
const fetchStatistics = useCallback(async () => {
|
||||||
try {
|
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(
|
const response = await fetch(
|
||||||
`/api/v1/dashboard/top-sounds?sound_type=${soundType}&period=${period}&limit=${limit}`,
|
`/api/v1/dashboard/top-sounds?sound_type=${soundType}&period=${period}&limit=${limit}`,
|
||||||
{ credentials: 'include' }
|
{ credentials: 'include' }
|
||||||
@@ -54,46 +83,75 @@ export function DashboardPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json()
|
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) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch top sounds:', err)
|
console.error('Failed to fetch top sounds:', err)
|
||||||
} finally {
|
} 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(() => {
|
useEffect(() => {
|
||||||
const fetchStatistics = async () => {
|
const loadInitialData = async () => {
|
||||||
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const [soundboardResponse, trackResponse] = await Promise.all([
|
await fetchStatistics()
|
||||||
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 {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchStatistics()
|
loadInitialData()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Auto-refresh every 5 seconds
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
refreshAll()
|
||||||
|
}, 5000)
|
||||||
|
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [refreshAll])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchTopSounds()
|
fetchTopSounds(true) // Show loading on initial load and filter changes
|
||||||
}, [soundType, period, limit])
|
}, [fetchTopSounds])
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -192,6 +250,14 @@ export function DashboardPage() {
|
|||||||
Overview of your soundboard and track statistics
|
Overview of your soundboard and track statistics
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={refreshAll}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={refreshing}
|
||||||
|
>
|
||||||
|
<RefreshCw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -218,7 +284,7 @@ export function DashboardPage() {
|
|||||||
<Play className="h-4 w-4 text-muted-foreground" />
|
<Play className="h-4 w-4 text-muted-foreground" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<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">
|
<p className="text-xs text-muted-foreground">
|
||||||
All-time play count
|
All-time play count
|
||||||
</p>
|
</p>
|
||||||
@@ -390,10 +456,6 @@ export function DashboardPage() {
|
|||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="font-medium truncate">{sound.name}</div>
|
<div className="font-medium truncate">{sound.name}</div>
|
||||||
<div className="flex items-center gap-4 text-xs text-muted-foreground mt-1">
|
<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 && (
|
{sound.duration && (
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<Clock className="h-3 w-3" />
|
<Clock className="h-3 w-3" />
|
||||||
@@ -405,6 +467,10 @@ export function DashboardPage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user