feat: add playlist management components including header, table, loading states, and dialog
Some checks failed
Frontend CI / lint (push) Failing after 18s
Frontend CI / build (push) Has been skipped

This commit is contained in:
JSC
2025-08-15 12:27:01 +02:00
parent 907a5df5c7
commit 1e76516cfc
7 changed files with 491 additions and 358 deletions

View File

@@ -130,7 +130,7 @@ export function TopSoundsSection({
<Clock className="h-3 w-3" /> <Clock className="h-3 w-3" />
<NumberFlowDuration <NumberFlowDuration
duration={sound.duration} duration={sound.duration}
variant="wordy" variant="clock"
/> />
</span> </span>
)} )}

View File

@@ -0,0 +1,105 @@
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
interface CreatePlaylistDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
loading: boolean
name: string
description: string
genre: string
onNameChange: (name: string) => void
onDescriptionChange: (description: string) => void
onGenreChange: (genre: string) => void
onSubmit: () => void
onCancel: () => void
}
export function CreatePlaylistDialog({
open,
onOpenChange,
loading,
name,
description,
genre,
onNameChange,
onDescriptionChange,
onGenreChange,
onSubmit,
onCancel,
}: CreatePlaylistDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Create New Playlist</DialogTitle>
<DialogDescription>
Add a new playlist to organize your sounds. Give it a name
and optionally add a description and genre.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Name *</Label>
<Input
id="name"
placeholder="My awesome playlist"
value={name}
onChange={e => onNameChange(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
onSubmit()
}
}}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
placeholder="A collection of my favorite sounds..."
value={description}
onChange={e => onDescriptionChange(e.target.value)}
className="min-h-[80px]"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="genre">Genre</Label>
<Input
id="genre"
placeholder="Electronic, Rock, Comedy, etc."
value={genre}
onChange={e => onGenreChange(e.target.value)}
/>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={onCancel}
disabled={loading}
>
Cancel
</Button>
<Button
onClick={onSubmit}
disabled={loading || !name.trim()}
>
{loading ? 'Creating...' : 'Create Playlist'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,104 @@
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { TableCell, TableRow } from '@/components/ui/table'
import type { Playlist } from '@/lib/api/services/playlists'
import { formatDuration } from '@/utils/format-duration'
import { Calendar, Clock, Edit, Music, Play, User } from 'lucide-react'
interface PlaylistRowProps {
playlist: Playlist
onEdit: (playlist: Playlist) => void
onSetCurrent: (playlist: Playlist) => void
}
export function PlaylistRow({ playlist, onEdit, onSetCurrent }: PlaylistRowProps) {
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString()
}
return (
<TableRow className="hover:bg-muted/50">
<TableCell>
<div className="flex items-center gap-2">
<Music className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<div className="min-w-0">
<div className="font-medium truncate">{playlist.name}</div>
{playlist.description && (
<div className="text-sm text-muted-foreground truncate">
{playlist.description}
</div>
)}
</div>
</div>
</TableCell>
<TableCell>
{playlist.genre ? (
<Badge variant="secondary">{playlist.genre}</Badge>
) : (
<span className="text-muted-foreground">-</span>
)}
</TableCell>
<TableCell>
{playlist.user_name ? (
<div className="flex items-center gap-2">
<User className="h-4 w-4 text-muted-foreground" />
<span>{playlist.user_name}</span>
</div>
) : (
<span className="text-muted-foreground">System</span>
)}
</TableCell>
<TableCell className="text-center">
<div className="flex items-center justify-center gap-1">
<Music className="h-3 w-3 text-muted-foreground" />
{playlist.sound_count}
</div>
</TableCell>
<TableCell className="text-center">
<div className="flex items-center justify-center gap-1">
<Clock className="h-3 w-3 text-muted-foreground" />
{formatDuration(playlist.total_duration || 0)}
</div>
</TableCell>
<TableCell className="text-center">
<div className="flex items-center justify-center gap-1">
<Calendar className="h-3 w-3 text-muted-foreground" />
{formatDate(playlist.created_at)}
</div>
</TableCell>
<TableCell className="text-center">
<div className="flex items-center justify-center gap-1">
{playlist.is_current && <Badge variant="default">Current</Badge>}
{playlist.is_main && <Badge variant="outline">Main</Badge>}
{!playlist.is_current && !playlist.is_main && (
<span className="text-muted-foreground">-</span>
)}
</div>
</TableCell>
<TableCell className="text-center">
<div className="flex items-center justify-center gap-1">
<Button
size="sm"
variant="ghost"
onClick={() => onEdit(playlist)}
className="h-8 w-8 p-0"
title={`Edit "${playlist.name}"`}
>
<Edit className="h-4 w-4" />
</Button>
{!playlist.is_current && (
<Button
size="sm"
variant="ghost"
onClick={() => onSetCurrent(playlist)}
className="h-8 w-8 p-0"
title={`Set "${playlist.name}" as current playlist`}
>
<Play className="h-4 w-4" />
</Button>
)}
</div>
</TableCell>
</TableRow>
)
}

View File

@@ -0,0 +1,46 @@
import { PlaylistRow } from '@/components/playlists/PlaylistRow'
import {
Table,
TableBody,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import type { Playlist } from '@/lib/api/services/playlists'
interface PlaylistTableProps {
playlists: Playlist[]
onEdit: (playlist: Playlist) => void
onSetCurrent: (playlist: Playlist) => void
}
export function PlaylistTable({ playlists, onEdit, onSetCurrent }: PlaylistTableProps) {
return (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Genre</TableHead>
<TableHead>User</TableHead>
<TableHead className="text-center">Tracks</TableHead>
<TableHead className="text-center">Duration</TableHead>
<TableHead className="text-center">Created</TableHead>
<TableHead className="text-center">Status</TableHead>
<TableHead className="text-center">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{playlists.map(playlist => (
<PlaylistRow
key={playlist.id}
playlist={playlist}
onEdit={onEdit}
onSetCurrent={onSetCurrent}
/>
))}
</TableBody>
</Table>
</div>
)
}

View File

@@ -0,0 +1,134 @@
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import type { PlaylistSortField, SortOrder } from '@/lib/api/services/playlists'
import { Plus, RefreshCw, Search, SortAsc, SortDesc, X } from 'lucide-react'
interface PlaylistsHeaderProps {
searchQuery: string
onSearchChange: (query: string) => void
sortBy: PlaylistSortField
onSortByChange: (sortBy: PlaylistSortField) => void
sortOrder: SortOrder
onSortOrderChange: (order: SortOrder) => void
onRefresh: () => void
onCreateClick: () => void
loading: boolean
error: string | null
playlistCount: number
}
export function PlaylistsHeader({
searchQuery,
onSearchChange,
sortBy,
onSortByChange,
sortOrder,
onSortOrderChange,
onRefresh,
onCreateClick,
loading,
error,
playlistCount,
}: PlaylistsHeaderProps) {
return (
<>
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-2xl font-bold">Playlists</h1>
<p className="text-muted-foreground">
Manage and browse your soundboard playlists
</p>
</div>
<div className="flex items-center gap-4">
<Button onClick={onCreateClick}>
<Plus className="h-4 w-4 mr-2" />
Add Playlist
</Button>
{!loading && !error && (
<div className="text-sm text-muted-foreground">
{playlistCount} playlist{playlistCount !== 1 ? 's' : ''}
</div>
)}
</div>
</div>
{/* Search and Sort Controls */}
<div className="flex flex-col sm:flex-row gap-4 mb-6">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search playlists..."
value={searchQuery}
onChange={e => onSearchChange(e.target.value)}
className="pl-9 pr-9"
/>
{searchQuery && (
<Button
variant="ghost"
size="sm"
onClick={() => onSearchChange('')}
className="absolute right-1 top-1/2 transform -translate-y-1/2 h-7 w-7 p-0 hover:bg-muted"
title="Clear search"
>
<X className="h-3 w-3" />
</Button>
)}
</div>
</div>
<div className="flex gap-2">
<Select
value={sortBy}
onValueChange={value => onSortByChange(value as PlaylistSortField)}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
<SelectItem value="name">Name</SelectItem>
<SelectItem value="genre">Genre</SelectItem>
<SelectItem value="sound_count">Track Count</SelectItem>
<SelectItem value="total_duration">Duration</SelectItem>
<SelectItem value="created_at">Created Date</SelectItem>
<SelectItem value="updated_at">Updated Date</SelectItem>
</SelectContent>
</Select>
<Button
variant="outline"
size="icon"
onClick={() => onSortOrderChange(sortOrder === 'asc' ? 'desc' : 'asc')}
title={sortOrder === 'asc' ? 'Sort ascending' : 'Sort descending'}
>
{sortOrder === 'asc' ? (
<SortAsc className="h-4 w-4" />
) : (
<SortDesc className="h-4 w-4" />
)}
</Button>
<Button
variant="outline"
size="icon"
onClick={onRefresh}
disabled={loading}
title="Refresh playlists"
>
<RefreshCw
className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`}
/>
</Button>
</div>
</div>
</>
)
}

View File

@@ -0,0 +1,58 @@
import { Skeleton } from '@/components/ui/skeleton'
import { AlertCircle, Music } from 'lucide-react'
interface PlaylistsLoadingProps {
count?: number
}
export function PlaylistsLoading({ count = 5 }: PlaylistsLoadingProps) {
return (
<div className="space-y-3">
<Skeleton className="h-12 w-full" />
{Array.from({ length: count }).map((_, i) => (
<Skeleton key={i} className="h-16 w-full" />
))}
</div>
)
}
interface PlaylistsErrorProps {
error: string
onRetry: () => void
}
export function PlaylistsError({ error, onRetry }: PlaylistsErrorProps) {
return (
<div className="flex flex-col items-center justify-center py-12 text-center">
<AlertCircle className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-semibold mb-2">Failed to load playlists</h3>
<p className="text-muted-foreground mb-4">{error}</p>
<button
onClick={onRetry}
className="text-primary hover:underline"
>
Try again
</button>
</div>
)
}
interface PlaylistsEmptyProps {
searchQuery: string
}
export function PlaylistsEmpty({ searchQuery }: PlaylistsEmptyProps) {
return (
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="h-12 w-12 rounded-full bg-muted flex items-center justify-center mb-4">
<Music className="h-6 w-6 text-muted-foreground" />
</div>
<h3 className="text-lg font-semibold mb-2">No playlists found</h3>
<p className="text-muted-foreground">
{searchQuery
? 'No playlists match your search criteria.'
: 'No playlists are available.'}
</p>
</div>
)
}

View File

@@ -1,56 +1,18 @@
import { AppLayout } from '@/components/AppLayout' import { AppLayout } from '@/components/AppLayout'
import { Badge } from '@/components/ui/badge' import { CreatePlaylistDialog } from '@/components/playlists/CreatePlaylistDialog'
import { Button } from '@/components/ui/button' import { PlaylistsHeader } from '@/components/playlists/PlaylistsHeader'
import { import {
Dialog, PlaylistsEmpty,
DialogContent, PlaylistsError,
DialogDescription, PlaylistsLoading,
DialogFooter, } from '@/components/playlists/PlaylistsLoadingStates'
DialogHeader, import { PlaylistTable } from '@/components/playlists/PlaylistTable'
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Skeleton } from '@/components/ui/skeleton'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { Textarea } from '@/components/ui/textarea'
import { import {
type Playlist, type Playlist,
type PlaylistSortField, type PlaylistSortField,
type SortOrder, type SortOrder,
playlistsService, playlistsService,
} from '@/lib/api/services/playlists' } from '@/lib/api/services/playlists'
import { formatDuration } from '@/utils/format-duration'
import {
AlertCircle,
Calendar,
Clock,
Edit,
Music,
Play,
Plus,
RefreshCw,
Search,
SortAsc,
SortDesc,
User,
X,
} from 'lucide-react'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router' import { useNavigate } from 'react-router'
import { toast } from 'sonner' import { toast } from 'sonner'
@@ -160,164 +122,29 @@ export function PlaylistsPage() {
} }
} }
const formatDate = (dateString: string) => { const handleEditPlaylist = (playlist: Playlist) => {
return new Date(dateString).toLocaleDateString() navigate(`/playlists/${playlist.id}/edit`)
} }
const renderContent = () => { const renderContent = () => {
if (loading) { if (loading) {
return ( return <PlaylistsLoading />
<div className="space-y-3">
<Skeleton className="h-12 w-full" />
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} className="h-16 w-full" />
))}
</div>
)
} }
if (error) { if (error) {
return ( return <PlaylistsError error={error} onRetry={fetchPlaylists} />
<div className="flex flex-col items-center justify-center py-12 text-center">
<AlertCircle className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-semibold mb-2">
Failed to load playlists
</h3>
<p className="text-muted-foreground mb-4">{error}</p>
<button
onClick={fetchPlaylists}
className="text-primary hover:underline"
>
Try again
</button>
</div>
)
} }
if (playlists.length === 0) { if (playlists.length === 0) {
return ( return <PlaylistsEmpty searchQuery={searchQuery} />
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="h-12 w-12 rounded-full bg-muted flex items-center justify-center mb-4">
<Music className="h-6 w-6 text-muted-foreground" />
</div>
<h3 className="text-lg font-semibold mb-2">No playlists found</h3>
<p className="text-muted-foreground">
{searchQuery
? 'No playlists match your search criteria.'
: 'No playlists are available.'}
</p>
</div>
)
} }
return ( return (
<div className="rounded-md border"> <PlaylistTable
<Table> playlists={playlists}
<TableHeader> onEdit={handleEditPlaylist}
<TableRow> onSetCurrent={handleSetCurrent}
<TableHead>Name</TableHead> />
<TableHead>Genre</TableHead>
<TableHead>User</TableHead>
<TableHead className="text-center">Tracks</TableHead>
<TableHead className="text-center">Duration</TableHead>
<TableHead className="text-center">Created</TableHead>
<TableHead className="text-center">Status</TableHead>
<TableHead className="text-center">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{playlists.map(playlist => (
<TableRow key={playlist.id} className="hover:bg-muted/50">
<TableCell>
<div className="flex items-center gap-2">
<Music className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<div className="min-w-0">
<div className="font-medium truncate">
{playlist.name}
</div>
{playlist.description && (
<div className="text-sm text-muted-foreground truncate">
{playlist.description}
</div>
)}
</div>
</div>
</TableCell>
<TableCell>
{playlist.genre ? (
<Badge variant="secondary">{playlist.genre}</Badge>
) : (
<span className="text-muted-foreground">-</span>
)}
</TableCell>
<TableCell>
{playlist.user_name ? (
<div className="flex items-center gap-2">
<User className="h-4 w-4 text-muted-foreground" />
<span>{playlist.user_name}</span>
</div>
) : (
<span className="text-muted-foreground">System</span>
)}
</TableCell>
<TableCell className="text-center">
<div className="flex items-center justify-center gap-1">
<Music className="h-3 w-3 text-muted-foreground" />
{playlist.sound_count}
</div>
</TableCell>
<TableCell className="text-center">
<div className="flex items-center justify-center gap-1">
<Clock className="h-3 w-3 text-muted-foreground" />
{formatDuration(playlist.total_duration || 0)}
</div>
</TableCell>
<TableCell className="text-center">
<div className="flex items-center justify-center gap-1">
<Calendar className="h-3 w-3 text-muted-foreground" />
{formatDate(playlist.created_at)}
</div>
</TableCell>
<TableCell className="text-center">
<div className="flex items-center justify-center gap-1">
{playlist.is_current && (
<Badge variant="default">Current</Badge>
)}
{playlist.is_main && <Badge variant="outline">Main</Badge>}
{!playlist.is_current && !playlist.is_main && (
<span className="text-muted-foreground">-</span>
)}
</div>
</TableCell>
<TableCell className="text-center">
<div className="flex items-center justify-center gap-1">
<Button
size="sm"
variant="ghost"
onClick={() => navigate(`/playlists/${playlist.id}/edit`)}
className="h-8 w-8 p-0"
title={`Edit "${playlist.name}"`}
>
<Edit className="h-4 w-4" />
</Button>
{!playlist.is_current && (
<Button
size="sm"
variant="ghost"
onClick={() => handleSetCurrent(playlist)}
className="h-8 w-8 p-0"
title={`Set "${playlist.name}" as current playlist`}
>
<Play className="h-4 w-4" />
</Button>
)}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
) )
} }
@@ -328,174 +155,33 @@ export function PlaylistsPage() {
}} }}
> >
<div className="flex-1 rounded-xl bg-muted/50 p-4"> <div className="flex-1 rounded-xl bg-muted/50 p-4">
<div className="flex items-center justify-between mb-6"> <PlaylistsHeader
<div> searchQuery={searchQuery}
<h1 className="text-2xl font-bold">Playlists</h1> onSearchChange={setSearchQuery}
<p className="text-muted-foreground"> sortBy={sortBy}
Manage and browse your soundboard playlists onSortByChange={setSortBy}
</p> sortOrder={sortOrder}
</div> onSortOrderChange={setSortOrder}
<div className="flex items-center gap-4"> onRefresh={fetchPlaylists}
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}> onCreateClick={() => setShowCreateDialog(true)}
<DialogTrigger asChild> loading={loading}
<Button> error={error}
<Plus className="h-4 w-4 mr-2" /> playlistCount={playlists.length}
Add Playlist
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Create New Playlist</DialogTitle>
<DialogDescription>
Add a new playlist to organize your sounds. Give it a name
and optionally add a description and genre.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Name *</Label>
<Input
id="name"
placeholder="My awesome playlist"
value={newPlaylist.name}
onChange={e =>
setNewPlaylist(prev => ({
...prev,
name: e.target.value,
}))
}
onKeyDown={e => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleCreatePlaylist()
}
}}
/> />
</div>
<div className="grid gap-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
placeholder="A collection of my favorite sounds..."
value={newPlaylist.description}
onChange={e =>
setNewPlaylist(prev => ({
...prev,
description: e.target.value,
}))
}
className="min-h-[80px]"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="genre">Genre</Label>
<Input
id="genre"
placeholder="Electronic, Rock, Comedy, etc."
value={newPlaylist.genre}
onChange={e =>
setNewPlaylist(prev => ({
...prev,
genre: e.target.value,
}))
}
/>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={handleCancelCreate}
disabled={createLoading}
>
Cancel
</Button>
<Button
onClick={handleCreatePlaylist}
disabled={createLoading || !newPlaylist.name.trim()}
>
{createLoading ? 'Creating...' : 'Create Playlist'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{!loading && !error && (
<div className="text-sm text-muted-foreground">
{playlists.length} playlist{playlists.length !== 1 ? 's' : ''}
</div>
)}
</div>
</div>
{/* Search and Sort Controls */} <CreatePlaylistDialog
<div className="flex flex-col sm:flex-row gap-4 mb-6"> open={showCreateDialog}
<div className="flex-1"> onOpenChange={setShowCreateDialog}
<div className="relative"> loading={createLoading}
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" /> name={newPlaylist.name}
<Input description={newPlaylist.description}
placeholder="Search playlists..." genre={newPlaylist.genre}
value={searchQuery} onNameChange={name => setNewPlaylist(prev => ({ ...prev, name }))}
onChange={e => setSearchQuery(e.target.value)} onDescriptionChange={description => setNewPlaylist(prev => ({ ...prev, description }))}
className="pl-9 pr-9" onGenreChange={genre => setNewPlaylist(prev => ({ ...prev, genre }))}
onSubmit={handleCreatePlaylist}
onCancel={handleCancelCreate}
/> />
{searchQuery && (
<Button
variant="ghost"
size="sm"
onClick={() => setSearchQuery('')}
className="absolute right-1 top-1/2 transform -translate-y-1/2 h-7 w-7 p-0 hover:bg-muted"
title="Clear search"
>
<X className="h-3 w-3" />
</Button>
)}
</div>
</div>
<div className="flex gap-2">
<Select
value={sortBy}
onValueChange={value => setSortBy(value as PlaylistSortField)}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
<SelectItem value="name">Name</SelectItem>
<SelectItem value="genre">Genre</SelectItem>
<SelectItem value="sound_count">Track Count</SelectItem>
<SelectItem value="total_duration">Duration</SelectItem>
<SelectItem value="created_at">Created Date</SelectItem>
<SelectItem value="updated_at">Updated Date</SelectItem>
</SelectContent>
</Select>
<Button
variant="outline"
size="icon"
onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}
title={sortOrder === 'asc' ? 'Sort ascending' : 'Sort descending'}
>
{sortOrder === 'asc' ? (
<SortAsc className="h-4 w-4" />
) : (
<SortDesc className="h-4 w-4" />
)}
</Button>
<Button
variant="outline"
size="icon"
onClick={fetchPlaylists}
disabled={loading}
title="Refresh playlists"
>
<RefreshCw
className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`}
/>
</Button>
</div>
</div>
{renderContent()} {renderContent()}
</div> </div>