feat: update loading skeleton and playlist handling for main playlists
Some checks failed
Frontend CI / lint (push) Failing after 17s
Frontend CI / build (push) Has been skipped

This commit is contained in:
JSC
2025-08-16 01:21:34 +02:00
parent 7b01ace746
commit f6117ededd
13 changed files with 141 additions and 55 deletions

View File

@@ -2,12 +2,7 @@ import { AppLayout } from '@/components/AppLayout'
import { DashboardHeader } from '@/components/dashboard/DashboardHeader' import { DashboardHeader } from '@/components/dashboard/DashboardHeader'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
interface LoadingSkeletonProps { export function LoadingSkeleton() {
title: string
description: string
}
export function LoadingSkeleton({ title, description }: LoadingSkeletonProps) {
return ( return (
<AppLayout <AppLayout
breadcrumb={{ breadcrumb={{

View File

@@ -1,6 +1,6 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { LucideIcon } from 'lucide-react' import type { LucideIcon } from 'lucide-react'
import { ReactNode } from 'react' import type { ReactNode } from 'react'
interface StatisticCardProps { interface StatisticCardProps {
title: string title: string

View File

@@ -13,7 +13,8 @@ export function CreditsNav({ user }: CreditsNavProps) {
const [credits, setCredits] = useState(user.credits) const [credits, setCredits] = useState(user.credits)
useEffect(() => { useEffect(() => {
const handleCreditsChanged = (data: { credits_after: number }) => { const handleCreditsChanged = (...args: unknown[]) => {
const data = args[0] as { credits_after: number }
setCredits(data.credits_after) setCredits(data.credits_after)
} }

View File

@@ -51,7 +51,8 @@ export function CompactPlayer({ className }: CompactPlayerProps) {
// Listen for player state updates // Listen for player state updates
useEffect(() => { useEffect(() => {
const handlePlayerState = (newState: PlayerState) => { const handlePlayerState = (...args: unknown[]) => {
const newState = args[0] as PlayerState
setState(newState) setState(newState)
} }

View File

@@ -99,7 +99,8 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) {
// Listen for player state updates // Listen for player state updates
useEffect(() => { useEffect(() => {
const handlePlayerState = (newState: PlayerState) => { const handlePlayerState = (...args: unknown[]) => {
const newState = args[0] as PlayerState
setState(newState) setState(newState)
} }

View File

@@ -2,7 +2,7 @@ import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { TableCell, TableRow } from '@/components/ui/table' import { TableCell, TableRow } from '@/components/ui/table'
import type { Playlist } from '@/lib/api/services/playlists' import type { Playlist } from '@/lib/api/services/playlists'
import { formatDate, formatDateDistanceToNow } from '@/utils/format-date' import { formatDateDistanceToNow } from '@/utils/format-date'
import { formatDuration } from '@/utils/format-duration' import { formatDuration } from '@/utils/format-duration'
import { Calendar, Clock, Edit, Music, Play, User } from 'lucide-react' import { Calendar, Clock, Edit, Music, Play, User } from 'lucide-react'

View File

@@ -117,7 +117,13 @@ export function PlaylistDetailsCard({
{/* Edit/Save/Cancel buttons */} {/* Edit/Save/Cancel buttons */}
<div className="pt-4 border-t"> <div className="pt-4 border-t">
{isEditMode ? ( {playlist.is_main ? (
<div className="text-center">
<p className="text-sm text-muted-foreground">
Main playlist details cannot be edited
</p>
</div>
) : isEditMode ? (
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button variant="outline" onClick={onCancelEdit}> <Button variant="outline" onClick={onCancelEdit}>
<X className="h-4 w-4 mr-2" /> <X className="h-4 w-4 mr-2" />

View File

@@ -7,13 +7,15 @@ import { Music, X } from 'lucide-react'
interface SimpleSortableRowProps { interface SimpleSortableRowProps {
sound: PlaylistSound sound: PlaylistSound
index: number index: number
onRemoveSound: (soundId: number) => void onRemoveSound?: (soundId: number) => void
isMainPlaylist?: boolean
} }
export function SimpleSortableRow({ export function SimpleSortableRow({
sound, sound,
index, index,
onRemoveSound, onRemoveSound,
isMainPlaylist = false,
}: SimpleSortableRowProps) { }: SimpleSortableRowProps) {
const { const {
attributes, attributes,
@@ -48,6 +50,7 @@ export function SimpleSortableRow({
<div className="font-medium truncate">{sound.name}</div> <div className="font-medium truncate">{sound.name}</div>
</div> </div>
{onRemoveSound && !isMainPlaylist && (
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
@@ -60,6 +63,7 @@ export function SimpleSortableRow({
> >
<X className="h-3 w-3" /> <X className="h-3 w-3" />
</Button> </Button>
)}
</div> </div>
) )
} }

View File

@@ -12,8 +12,9 @@ interface SortableTableRowProps {
index: number index: number
onMoveSoundUp: (index: number) => void onMoveSoundUp: (index: number) => void
onMoveSoundDown: (index: number) => void onMoveSoundDown: (index: number) => void
onRemoveSound: (soundId: number) => void onRemoveSound?: (soundId: number) => void
totalSounds: number totalSounds: number
isMainPlaylist?: boolean
} }
export function SortableTableRow({ export function SortableTableRow({
@@ -23,6 +24,7 @@ export function SortableTableRow({
onMoveSoundDown, onMoveSoundDown,
onRemoveSound, onRemoveSound,
totalSounds, totalSounds,
isMainPlaylist = false,
}: SortableTableRowProps) { }: SortableTableRowProps) {
const { const {
attributes, attributes,
@@ -97,6 +99,7 @@ export function SortableTableRow({
> >
<ChevronDown className="h-4 w-4" /> <ChevronDown className="h-4 w-4" />
</Button> </Button>
{onRemoveSound && !isMainPlaylist && (
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
@@ -106,6 +109,7 @@ export function SortableTableRow({
> >
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
</Button> </Button>
)}
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>

View File

@@ -2,7 +2,7 @@
* Simple event emitter for cross-component communication * Simple event emitter for cross-component communication
*/ */
type EventHandler = (...args: unknown[]) => void export type EventHandler = (...args: unknown[]) => void
class EventEmitter { class EventEmitter {
private events: Map<string, EventHandler[]> = new Map() private events: Map<string, EventHandler[]> = new Map()

View File

@@ -158,7 +158,7 @@ export function DashboardPage() {
}, [fetchTopSounds]) }, [fetchTopSounds])
if (loading) { if (loading) {
return <LoadingSkeleton title="Dashboard" description="Loading dashboard statistics..." /> return <LoadingSkeleton />
} }
if (error || !soundboardStatistics || !trackStatistics) { if (error || !soundboardStatistics || !trackStatistics) {

View File

@@ -282,6 +282,11 @@ export function PlaylistEditPage() {
} }
const handleAddSoundToPlaylist = async (soundId: number) => { const handleAddSoundToPlaylist = async (soundId: number) => {
if (playlist?.is_main) {
toast.error('Cannot add sounds to the main playlist')
return
}
try { try {
// Add at the end - backend should handle position gaps automatically // Add at the end - backend should handle position gaps automatically
const position = sounds.length const position = sounds.length
@@ -328,6 +333,11 @@ export function PlaylistEditPage() {
} }
const handleRemoveSound = async (soundId: number) => { const handleRemoveSound = async (soundId: number) => {
if (playlist?.is_main) {
toast.error('Cannot remove sounds from the main playlist')
return
}
try { try {
// Find the sound being removed // Find the sound being removed
const removedSound = sounds.find(s => s.id === soundId) const removedSound = sounds.find(s => s.id === soundId)
@@ -417,6 +427,12 @@ export function PlaylistEditPage() {
const activeId = active.id as string const activeId = active.id as string
const overId = over.id as string const overId = over.id as string
// Prevent adding sounds to main playlist
if (playlist?.is_main && activeId.startsWith('available-sound-')) {
toast.error('Cannot add sounds to the main playlist')
return
}
// Handle adding sound from available list to playlist // Handle adding sound from available list to playlist
if ( if (
activeId.startsWith('available-sound-') && activeId.startsWith('available-sound-') &&
@@ -585,6 +601,7 @@ export function PlaylistEditPage() {
Playlist Sounds ({sounds.length}) Playlist Sounds ({sounds.length})
</CardTitle> </CardTitle>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{!playlist.is_main && (
<Button <Button
variant={isAddMode ? 'default' : 'outline'} variant={isAddMode ? 'default' : 'outline'}
size="sm" size="sm"
@@ -601,6 +618,7 @@ export function PlaylistEditPage() {
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
)} )}
</Button> </Button>
)}
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
@@ -621,6 +639,60 @@ export function PlaylistEditPage() {
<Skeleton key={i} className="h-12 w-full" /> <Skeleton key={i} className="h-12 w-full" />
))} ))}
</div> </div>
) : playlist.is_main ? (
// Main playlist: Only show sounds in read-only mode with reordering
sounds.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 in main playlist</p>
<p className="text-xs mt-1">
Main playlist sounds are managed automatically
</p>
</div>
) : (
<div className="rounded-md border">
<SortableContext
items={sounds.map(sound => `table-sound-${sound.id}`)}
strategy={verticalListSortingStrategy}
>
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-16 text-center">
<div className="flex items-center justify-center gap-1">
<div className="flex flex-col">
<div className="w-1 h-1 bg-muted-foreground/40 rounded-full"></div>
<div className="w-1 h-1 bg-muted-foreground/40 rounded-full mt-0.5"></div>
<div className="w-1 h-1 bg-muted-foreground/40 rounded-full mt-0.5"></div>
</div>
<span className="text-xs">#</span>
</div>
</TableHead>
<TableHead>Name</TableHead>
<TableHead>Duration</TableHead>
<TableHead>Type</TableHead>
<TableHead>Plays</TableHead>
<TableHead className="w-32">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sounds.map((sound, index) => (
<SortableTableRow
key={sound.id}
sound={sound}
index={index}
onMoveSoundUp={handleMoveSoundUp}
onMoveSoundDown={handleMoveSoundDown}
onRemoveSound={undefined} // Disable remove for main playlist
totalSounds={sounds.length}
isMainPlaylist={true}
/>
))}
</TableBody>
</Table>
</SortableContext>
</div>
)
) : isAddMode ? ( ) : isAddMode ? (
// Add Mode: Split layout with simplified playlist and available sounds // Add Mode: Split layout with simplified playlist and available sounds
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
@@ -785,6 +857,7 @@ export function PlaylistEditPage() {
onMoveSoundDown={handleMoveSoundDown} onMoveSoundDown={handleMoveSoundDown}
onRemoveSound={handleRemoveSound} onRemoveSound={handleRemoveSound}
totalSounds={sounds.length} totalSounds={sounds.length}
isMainPlaylist={false}
/> />
))} ))}
</TableBody> </TableBody>

View File

@@ -141,7 +141,8 @@ export function SoundsPage() {
// Listen for sound_played events and update play_count // Listen for sound_played events and update play_count
useEffect(() => { useEffect(() => {
const handleSoundPlayed = (eventData: SoundPlayedEventData) => { const handleSoundPlayed = (...args: unknown[]) => {
const eventData = args[0] as SoundPlayedEventData
setSounds(prevSounds => setSounds(prevSounds =>
prevSounds.map(sound => prevSounds.map(sound =>
sound.id === eventData.sound_id sound.id === eventData.sound_id