feat: Refactor playlist edit components for improved structure and functionality
- Added AvailableSound component for displaying and adding sounds to playlists. - Introduced DragOverlayComponents for drag-and-drop functionality with inline previews and drop areas. - Created PlaylistDetailsCard for editing playlist details with save and cancel options. - Implemented PlaylistEditHeader for displaying playlist title and current status. - Added PlaylistStatsCard to show statistics about the playlist. - Refactored PlaylistEditPage to utilize new components, enhancing readability and maintainability. - Introduced loading and error states with PlaylistEditLoading and PlaylistEditError components. - Updated SortableTableRow and SimpleSortableRow for better drag-and-drop handling.
This commit is contained in:
@@ -1,26 +1,35 @@
|
||||
import { AppLayout } from '@/components/AppLayout'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { AvailableSound } from '@/components/playlists/playlist-edit/AvailableSound'
|
||||
import {
|
||||
DragOverlayContent,
|
||||
EndDropArea,
|
||||
InlinePreview,
|
||||
} from '@/components/playlists/playlist-edit/DragOverlayComponents'
|
||||
import { PlaylistDetailsCard } from '@/components/playlists/playlist-edit/PlaylistDetailsCard'
|
||||
import {
|
||||
PlaylistEditError,
|
||||
PlaylistEditLoading,
|
||||
} from '@/components/playlists/playlist-edit/PlaylistEditLoadingStates'
|
||||
import { PlaylistEditHeader } from '@/components/playlists/playlist-edit/PlaylistEditHeader'
|
||||
import { PlaylistStatsCard } from '@/components/playlists/playlist-edit/PlaylistStatsCard'
|
||||
import { SimpleSortableRow } from '@/components/playlists/playlist-edit/SimpleSortableRow'
|
||||
import { SortableTableRow } from '@/components/playlists/playlist-edit/SortableTableRow'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import {
|
||||
type Playlist,
|
||||
type PlaylistSound,
|
||||
playlistsService,
|
||||
} from '@/lib/api/services/playlists'
|
||||
import { type Sound, soundsService } from '@/lib/api/services/sounds'
|
||||
import { formatDuration } from '@/utils/format-duration'
|
||||
import {
|
||||
DndContext,
|
||||
type DragEndEvent,
|
||||
@@ -32,322 +41,15 @@ import {
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core'
|
||||
import { useDroppable } from '@dnd-kit/core'
|
||||
import {
|
||||
SortableContext,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import {
|
||||
AlertCircle,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Clock,
|
||||
Edit,
|
||||
Minus,
|
||||
Music,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
Save,
|
||||
Trash2,
|
||||
X,
|
||||
} from 'lucide-react'
|
||||
import { Minus, Music, Plus, RefreshCw } from 'lucide-react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
// Sortable table row component for normal table view
|
||||
interface SortableTableRowProps {
|
||||
sound: PlaylistSound
|
||||
index: number
|
||||
onMoveSoundUp: (index: number) => void
|
||||
onMoveSoundDown: (index: number) => void
|
||||
onRemoveSound: (soundId: number) => void
|
||||
totalSounds: number
|
||||
}
|
||||
|
||||
function SortableTableRow({
|
||||
sound,
|
||||
index,
|
||||
onMoveSoundUp,
|
||||
onMoveSoundDown,
|
||||
onRemoveSound,
|
||||
totalSounds,
|
||||
}: SortableTableRowProps) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: `table-sound-${sound.id}` })
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.8 : 1,
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={sound.id}
|
||||
className="hover:bg-muted/50"
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
>
|
||||
<TableCell className="text-center text-muted-foreground font-mono text-sm">
|
||||
<div
|
||||
className="flex items-center justify-center gap-2 cursor-grab active:cursor-grabbing"
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div className="w-1 h-1 bg-muted-foreground/60 rounded-full"></div>
|
||||
<div className="w-1 h-1 bg-muted-foreground/60 rounded-full mt-0.5"></div>
|
||||
<div className="w-1 h-1 bg-muted-foreground/60 rounded-full mt-0.5"></div>
|
||||
</div>
|
||||
<span>{index + 1}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<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">{sound.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{formatDuration(sound.duration || 0)}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{sound.type}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{sound.play_count}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => onMoveSoundUp(index)}
|
||||
disabled={index === 0}
|
||||
className="h-8 w-8 p-0"
|
||||
title="Move up"
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => onMoveSoundDown(index)}
|
||||
disabled={index === totalSounds - 1}
|
||||
className="h-8 w-8 p-0"
|
||||
title="Move down"
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => onRemoveSound(sound.id)}
|
||||
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
|
||||
title="Remove from playlist"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
|
||||
// Simplified sortable row component for add mode
|
||||
interface SimpleSortableRowProps {
|
||||
sound: PlaylistSound
|
||||
index: number
|
||||
onRemoveSound: (soundId: number) => void
|
||||
}
|
||||
|
||||
function SimpleSortableRow({
|
||||
sound,
|
||||
index,
|
||||
onRemoveSound,
|
||||
}: SimpleSortableRowProps) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: `playlist-sound-${sound.id}` })
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.8 : 1,
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className="flex items-center gap-3 p-3 border rounded-lg hover:bg-muted/50 cursor-grab active:cursor-grabbing group"
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
>
|
||||
<span className="text-sm font-mono text-muted-foreground min-w-[1.5rem] text-center flex-shrink-0">
|
||||
{index + 1}
|
||||
</span>
|
||||
|
||||
<Music className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="font-medium truncate">{sound.name}</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
onRemoveSound(sound.id)
|
||||
}}
|
||||
className="h-4 w-4 p-0 text-destructive hover:text-destructive flex-shrink-0"
|
||||
title="Remove from playlist"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Available sound component for dragging
|
||||
interface AvailableSoundProps {
|
||||
sound: Sound
|
||||
onAddToPlaylist: (soundId: number) => void
|
||||
}
|
||||
|
||||
function AvailableSound({ sound, onAddToPlaylist }: AvailableSoundProps) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: `available-sound-${sound.id}` })
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.8 : 1,
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className="flex items-center gap-3 p-3 border rounded-lg hover:bg-muted/50 cursor-grab active:cursor-grabbing group"
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
>
|
||||
<Music className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="font-medium truncate">{sound.name}</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
onAddToPlaylist(sound.id)
|
||||
}}
|
||||
className="h-4 w-4 p-0 text-primary hover:text-primary flex-shrink-0"
|
||||
title="Add to playlist"
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Simple drop area for the end of the playlist
|
||||
function EndDropArea() {
|
||||
const { setNodeRef } = useDroppable({
|
||||
id: 'playlist-end',
|
||||
data: { type: 'playlist-end' },
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
className="h-8 w-full" // Invisible drop area
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Inline preview component that shows where the sound will be dropped
|
||||
interface InlinePreviewProps {
|
||||
sound: Sound | PlaylistSound
|
||||
position: number
|
||||
}
|
||||
|
||||
function InlinePreview({ sound, position }: InlinePreviewProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 p-3 border-2 border-dashed border-primary rounded-lg bg-primary/10 animate-pulse">
|
||||
<span className="text-sm font-mono text-primary min-w-[1.5rem] text-center flex-shrink-0">
|
||||
{position + 1}
|
||||
</span>
|
||||
|
||||
<Music className="h-4 w-4 text-primary flex-shrink-0" />
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="font-medium truncate text-primary">{sound.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Drag overlay component that shows the dragged item
|
||||
interface DragOverlayContentProps {
|
||||
sound: Sound | PlaylistSound
|
||||
position?: number
|
||||
}
|
||||
|
||||
function DragOverlayContent({ sound, position }: DragOverlayContentProps) {
|
||||
// If position is provided, show as current playlist style
|
||||
if (position !== undefined) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 p-3 border rounded-lg bg-background shadow-lg">
|
||||
<span className="text-sm font-mono text-muted-foreground min-w-[1.5rem] text-center flex-shrink-0">
|
||||
{position + 1}
|
||||
</span>
|
||||
|
||||
<Music className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="font-medium truncate">{sound.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Default available sound style
|
||||
return (
|
||||
<div className="flex items-center gap-3 p-3 border rounded-lg bg-background shadow-lg">
|
||||
<Music className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="font-medium truncate">{sound.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function PlaylistEditPage() {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
@@ -827,55 +529,11 @@ export function PlaylistEditPage() {
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<AppLayout
|
||||
breadcrumb={{
|
||||
items: [
|
||||
{ label: 'Dashboard', href: '/' },
|
||||
{ label: 'Playlists', href: '/playlists' },
|
||||
{ label: 'Edit' },
|
||||
],
|
||||
}}
|
||||
>
|
||||
<div className="flex-1 rounded-xl bg-muted/50 p-4 space-y-6">
|
||||
<Skeleton className="h-8 w-64" />
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Skeleton className="h-96" />
|
||||
<Skeleton className="h-96" />
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
return <PlaylistEditLoading playlistName={playlist?.name} />
|
||||
}
|
||||
|
||||
if (error || !playlist) {
|
||||
return (
|
||||
<AppLayout
|
||||
breadcrumb={{
|
||||
items: [
|
||||
{ label: 'Dashboard', href: '/' },
|
||||
{ label: 'Playlists', href: '/playlists' },
|
||||
{ label: 'Edit' },
|
||||
],
|
||||
}}
|
||||
>
|
||||
<div className="flex-1 rounded-xl bg-muted/50 p-4">
|
||||
<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 playlist
|
||||
</h3>
|
||||
<p className="text-muted-foreground mb-4">{error}</p>
|
||||
<button
|
||||
onClick={() => navigate('/playlists')}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Back to playlists
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
return <PlaylistEditError error={error || 'Playlist not found'} onBackToPlaylists={() => navigate('/playlists')} />
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -896,207 +554,26 @@ export function PlaylistEditPage() {
|
||||
}}
|
||||
>
|
||||
<div className="flex-1 rounded-xl bg-muted/50 p-4">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">{playlist.name}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
View and manage your playlist
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{!playlist.is_current && !isEditMode && (
|
||||
<Button variant="outline" onClick={handleSetCurrent}>
|
||||
Set as Current
|
||||
</Button>
|
||||
)}
|
||||
{playlist.is_current && (
|
||||
<Badge variant="default">Current Playlist</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<PlaylistEditHeader
|
||||
playlist={playlist}
|
||||
isEditMode={isEditMode}
|
||||
onSetCurrent={handleSetCurrent}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Playlist Details */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Music className="h-5 w-5" />
|
||||
Playlist Details
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{isEditMode ? (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">Name *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={formData.name}
|
||||
onChange={e =>
|
||||
handleInputChange('name', e.target.value)
|
||||
}
|
||||
placeholder="Playlist name"
|
||||
/>
|
||||
</div>
|
||||
<PlaylistDetailsCard
|
||||
playlist={playlist}
|
||||
isEditMode={isEditMode}
|
||||
formData={formData}
|
||||
hasChanges={hasChanges}
|
||||
saving={saving}
|
||||
onInputChange={handleInputChange}
|
||||
onSave={handleSave}
|
||||
onCancelEdit={handleCancelEdit}
|
||||
onStartEdit={() => setIsEditMode(true)}
|
||||
/>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={formData.description}
|
||||
onChange={e =>
|
||||
handleInputChange('description', e.target.value)
|
||||
}
|
||||
placeholder="Playlist description"
|
||||
className="min-h-[100px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="genre">Genre</Label>
|
||||
<Input
|
||||
id="genre"
|
||||
value={formData.genre}
|
||||
onChange={e =>
|
||||
handleInputChange('genre', e.target.value)
|
||||
}
|
||||
placeholder="Electronic, Rock, Comedy, etc."
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label className="text-sm font-medium text-muted-foreground">
|
||||
Name
|
||||
</Label>
|
||||
<p className="text-lg font-semibold">{playlist.name}</p>
|
||||
</div>
|
||||
|
||||
{playlist.description && (
|
||||
<div>
|
||||
<Label className="text-sm font-medium text-muted-foreground">
|
||||
Description
|
||||
</Label>
|
||||
<p className="text-sm">{playlist.description}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{playlist.genre && (
|
||||
<div>
|
||||
<Label className="text-sm font-medium text-muted-foreground">
|
||||
Genre
|
||||
</Label>
|
||||
<Badge variant="secondary" className="mt-1">
|
||||
{playlist.genre}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!playlist.description && !playlist.genre && (
|
||||
<p className="text-sm text-muted-foreground italic">
|
||||
No additional details provided
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Edit/Save/Cancel buttons */}
|
||||
<div className="pt-4 border-t">
|
||||
{isEditMode ? (
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={handleCancelEdit}>
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={!hasChanges || saving}
|
||||
>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
{saving ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => setIsEditMode(true)}
|
||||
className="w-full"
|
||||
>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Playlist Stats */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Clock className="h-5 w-5" />
|
||||
Playlist Statistics
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="text-center p-3 bg-muted rounded-lg">
|
||||
<div className="text-2xl font-bold">{sounds.length}</div>
|
||||
<div className="text-sm text-muted-foreground">Tracks</div>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-muted rounded-lg">
|
||||
<div className="text-2xl font-bold">
|
||||
{formatDuration(
|
||||
sounds.reduce(
|
||||
(total, sound) => total + (sound.duration || 0),
|
||||
0,
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Duration
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>Created:</span>
|
||||
<span>
|
||||
{new Date(playlist.created_at).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
{playlist.updated_at && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>Updated:</span>
|
||||
<span>
|
||||
{new Date(playlist.updated_at).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>Status:</span>
|
||||
<div className="flex gap-1">
|
||||
{playlist.is_main && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Main
|
||||
</Badge>
|
||||
)}
|
||||
{playlist.is_current && (
|
||||
<Badge variant="default" className="text-xs">
|
||||
Current
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<PlaylistStatsCard playlist={playlist} sounds={sounds} />
|
||||
</div>
|
||||
|
||||
{/* Playlist Sounds */}
|
||||
@@ -1254,9 +731,7 @@ export function PlaylistEditPage() {
|
||||
<AvailableSound
|
||||
key={sound.id}
|
||||
sound={sound}
|
||||
onAddToPlaylist={soundId =>
|
||||
handleAddSoundToPlaylist(soundId)
|
||||
}
|
||||
onAddToPlaylist={handleAddSoundToPlaylist}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user