All checks were successful
Build and Deploy LTY / build-and-deploy (push) Successful in 1h5m35s
- Update card models, serializers, views and URLs - Update dances, songs, users admin pages and API modules - Add card migrations (merge furniture into decoration) - Update middleware and settings Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
645 lines
22 KiB
TypeScript
645 lines
22 KiB
TypeScript
"use client"
|
||
|
||
import { useState, useEffect, useRef } from "react"
|
||
import { DashboardShell } from "@/components/dashboard-shell"
|
||
import { DashboardHeader } from "@/components/dashboard-header"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
||
import { Input } from "@/components/ui/input"
|
||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||
import { Badge } from "@/components/ui/badge"
|
||
import { Search, Edit, Play, Pause, Eye, Volume2, VolumeX, Archive } from "lucide-react"
|
||
import { AddSongDialog } from "@/components/songs/add-song-dialog"
|
||
import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog"
|
||
import { PublishConfirmationDialog } from "@/components/publish-confirmation-dialog"
|
||
import { useToast } from "@/components/ui/use-toast"
|
||
import Link from "next/link"
|
||
import { isSuperUser } from "@/lib/api/auth"
|
||
import { getSongs, publishSong, deleteSong, archiveSong } from "@/lib/api/songs"
|
||
import type { Song } from "@/lib/api/types"
|
||
import type { Song as ComponentSong } from "@/components/songs/song-detail-dialog"
|
||
import { apiSongToComponentSong, componentSongToApiSong } from "@/lib/api/adapters"
|
||
import { Slider } from "@/components/ui/slider"
|
||
|
||
export default function SongsPage() {
|
||
const { toast } = useToast()
|
||
|
||
// 存储API返回的完整歌曲数据,包括原始数据结构,便于后续更新操作
|
||
const [songs, setSongs] = useState<Song[]>([])
|
||
const [isLoading, setIsLoading] = useState(true)
|
||
const [searchTerm, setSearchTerm] = useState("")
|
||
const [currentPage, setCurrentPage] = useState(1)
|
||
const [selectedSong, setSelectedSong] = useState<ComponentSong | null>(null)
|
||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
|
||
const [playingSongId, setPlayingSongId] = useState<string | null>(null)
|
||
const [totalSongs, setTotalSongs] = useState(0)
|
||
const [totalPages, setTotalPages] = useState(1)
|
||
const [volume, setVolume] = useState(80)
|
||
const [isMuted, setIsMuted] = useState(false)
|
||
const [isAudioLoading, setIsAudioLoading] = useState(false)
|
||
const [refreshTrigger, setRefreshTrigger] = useState(0)
|
||
|
||
// 音频播放器引用
|
||
const audioRef = useRef<HTMLAudioElement | null>(null)
|
||
const currentSongUrlRef = useRef<string | null>(null)
|
||
|
||
const itemsPerPage = 10
|
||
|
||
// 加载歌曲数据
|
||
useEffect(() => {
|
||
const fetchSongs = async () => {
|
||
setIsLoading(true)
|
||
try {
|
||
const response = await getSongs({
|
||
page: currentPage,
|
||
pageSize: itemsPerPage,
|
||
search: searchTerm,
|
||
})
|
||
|
||
// 直接使用API返回的完整歌曲数据
|
||
// 但确保所有必需属性都有默认值
|
||
const songItems = response.items.map(song => ({
|
||
...song,
|
||
id: song.id || "",
|
||
name: song.name || "",
|
||
composer: song.composer || "",
|
||
lyricist: song.lyricist || "",
|
||
duration: song.duration || "",
|
||
status: song.status || "未知"
|
||
}))
|
||
|
||
setSongs(songItems)
|
||
setTotalSongs(response.total)
|
||
setTotalPages(response.totalPages)
|
||
} catch (error) {
|
||
console.error("获取歌曲失败:", error)
|
||
toast({
|
||
title: "获取歌曲失败",
|
||
description: "无法加载歌曲数据,请稍后重试",
|
||
variant: "destructive",
|
||
})
|
||
} finally {
|
||
setIsLoading(false)
|
||
}
|
||
}
|
||
|
||
fetchSongs()
|
||
}, [currentPage, searchTerm, toast, refreshTrigger])
|
||
|
||
// 创建音频元素
|
||
useEffect(() => {
|
||
audioRef.current = new Audio()
|
||
|
||
// 设置音频事件监听器
|
||
const audio = audioRef.current
|
||
|
||
// 音频加载完成时
|
||
audio.addEventListener('canplay', () => {
|
||
setIsAudioLoading(false)
|
||
audio.play().catch(err => {
|
||
console.error("播放失败:", err)
|
||
toast({
|
||
title: "播放失败",
|
||
description: "无法播放音频,请稍后重试",
|
||
variant: "destructive",
|
||
})
|
||
setPlayingSongId(null)
|
||
})
|
||
})
|
||
|
||
// 音频播放结束时
|
||
audio.addEventListener('ended', () => {
|
||
setPlayingSongId(null)
|
||
})
|
||
|
||
// 音频播放错误时
|
||
audio.addEventListener('error', () => {
|
||
setIsAudioLoading(false)
|
||
setPlayingSongId(null)
|
||
toast({
|
||
title: "播放错误",
|
||
description: "无法播放该音频文件",
|
||
variant: "destructive",
|
||
})
|
||
})
|
||
|
||
// 清理函数
|
||
return () => {
|
||
if (audio) {
|
||
audio.pause()
|
||
audio.src = ''
|
||
audio.removeEventListener('canplay', () => {})
|
||
audio.removeEventListener('ended', () => {})
|
||
audio.removeEventListener('error', () => {})
|
||
}
|
||
}
|
||
}, [toast])
|
||
|
||
// 监听音量变化
|
||
useEffect(() => {
|
||
if (audioRef.current) {
|
||
audioRef.current.volume = volume / 100
|
||
}
|
||
}, [volume])
|
||
|
||
// 监听静音状态变化
|
||
useEffect(() => {
|
||
if (audioRef.current) {
|
||
audioRef.current.muted = isMuted
|
||
}
|
||
}, [isMuted])
|
||
|
||
// 处理添加歌曲
|
||
const handleAddSong = (newSong: ComponentSong) => {
|
||
// 转换为API Song类型
|
||
const apiSong = componentSongToApiSong(newSong)
|
||
|
||
setSongs((prevSongs) => [...prevSongs, apiSong])
|
||
setRefreshTrigger(prev => prev + 1) // 触发刷新
|
||
toast({
|
||
title: "添加成功",
|
||
description: `歌曲 ${newSong.name} 已成功添加`,
|
||
})
|
||
}
|
||
|
||
// 处理编辑歌曲
|
||
const handleEditSong = (updatedSong: ComponentSong) => {
|
||
console.log('保存编辑后的歌曲', updatedSong);
|
||
|
||
// 找到原始歌曲
|
||
const originalSong = songs.find(s => s.id === updatedSong.id);
|
||
console.log('原始歌曲数据', originalSong);
|
||
|
||
if (!originalSong) {
|
||
console.error('找不到原始歌曲数据');
|
||
return;
|
||
}
|
||
|
||
// 转换为API Song类型,并保留原始数据
|
||
const apiSong = componentSongToApiSong(updatedSong, originalSong);
|
||
console.log('转换后的API歌曲数据', apiSong);
|
||
|
||
// 更新本地歌曲数据
|
||
setSongs((prevSongs) => prevSongs.map((song) => {
|
||
if (song.id === apiSong.id) {
|
||
return apiSong;
|
||
}
|
||
return song;
|
||
}));
|
||
|
||
setSelectedSong(null);
|
||
setIsEditDialogOpen(false);
|
||
setRefreshTrigger(prev => prev + 1); // 触发刷新
|
||
toast({
|
||
title: "更新成功",
|
||
description: `歌曲 ${updatedSong.name} 已成功更新`,
|
||
});
|
||
}
|
||
|
||
// 处理删除歌曲
|
||
const handleDeleteSong = async (songId: string) => {
|
||
try {
|
||
// 如果正在播放该歌曲,先停止播放
|
||
if (playingSongId === songId && audioRef.current) {
|
||
audioRef.current.pause()
|
||
setPlayingSongId(null)
|
||
}
|
||
|
||
await deleteSong(songId)
|
||
setRefreshTrigger(prev => prev + 1)
|
||
toast({
|
||
title: "删除成功",
|
||
description: "歌曲已成功删除",
|
||
variant: "destructive",
|
||
})
|
||
} catch (error) {
|
||
console.error("删除歌曲失败:", error)
|
||
toast({
|
||
title: "删除失败",
|
||
description: "无法删除该歌曲,请稍后重试",
|
||
variant: "destructive",
|
||
})
|
||
}
|
||
}
|
||
|
||
// 归档歌曲
|
||
const handleArchiveSong = async (songId: string, songName: string) => {
|
||
try {
|
||
await archiveSong(songId)
|
||
setRefreshTrigger(prev => prev + 1)
|
||
toast({
|
||
title: "归档成功",
|
||
description: `歌曲 "${songName}" 已归档`,
|
||
})
|
||
} catch (error) {
|
||
console.error("归档歌曲失败:", error)
|
||
toast({
|
||
title: "归档失败",
|
||
description: "无法归档歌曲,请稍后重试",
|
||
variant: "destructive",
|
||
})
|
||
}
|
||
}
|
||
|
||
// 打开编辑对话框
|
||
const openEditDialog = (song: Song) => {
|
||
console.log('编辑歌曲', song);
|
||
|
||
// 确保从rawData中获取最新的数据
|
||
if (song.rawData) {
|
||
console.log('使用完整原始数据', song.rawData);
|
||
|
||
// 转换为组件所需的Song类型,优先使用rawData中的完整数据
|
||
const attributes = song.rawData.attributes || {};
|
||
const componentSong: ComponentSong = {
|
||
id: song.id,
|
||
name: song.name,
|
||
composer: attributes.composer || song.composer || "",
|
||
lyricist: attributes.lyricist || song.lyricist || "",
|
||
duration: attributes.duration || song.duration || "",
|
||
releaseDate: song.releaseDate,
|
||
status: song.rawData.status_display || song.status || "",
|
||
image: song.rawData.image_url || song.image,
|
||
audioUrl: attributes.audio_file || song.audioUrl || "",
|
||
description: song.rawData.description || song.description || "",
|
||
rarity: song.rawData.rarity_display || song.rarity || "",
|
||
cardType: song.rawData.card_type_display || song.cardType || "",
|
||
genre: attributes.genre || song.genre || "",
|
||
lyrics: attributes.lyrics || song.lyrics || "",
|
||
bpm: attributes.bpm || song.bpm || 0
|
||
};
|
||
|
||
setSelectedSong(componentSong);
|
||
} else {
|
||
// 使用适配器功能
|
||
const componentSong = apiSongToComponentSong(song);
|
||
setSelectedSong(componentSong);
|
||
}
|
||
|
||
setIsEditDialogOpen(true);
|
||
}
|
||
|
||
// 处理播放/暂停
|
||
const togglePlay = (songId: string) => {
|
||
const song = songs.find(s => s.id === songId)
|
||
|
||
// 如果找不到歌曲或没有音频URL,则显示提示f
|
||
if (!song || !song.audioUrl) {
|
||
toast({
|
||
title: "无法播放",
|
||
description: "该歌曲没有可播放的音频文件",
|
||
variant: "destructive",
|
||
})
|
||
return
|
||
}
|
||
|
||
// 如果已经在播放这首歌,则暂停
|
||
if (playingSongId === songId) {
|
||
if (audioRef.current) {
|
||
audioRef.current.pause()
|
||
}
|
||
setPlayingSongId(null)
|
||
return
|
||
}
|
||
|
||
// 如果正在播放其他歌曲,先停止
|
||
if (playingSongId && audioRef.current) {
|
||
audioRef.current.pause()
|
||
}
|
||
|
||
// 播放新歌曲
|
||
setIsAudioLoading(true)
|
||
setPlayingSongId(songId)
|
||
|
||
// 如果是新的URL,则设置src
|
||
if (currentSongUrlRef.current !== song.audioUrl && audioRef.current) {
|
||
currentSongUrlRef.current = song.audioUrl
|
||
audioRef.current.src = song.audioUrl
|
||
audioRef.current.load()
|
||
} else if (audioRef.current) {
|
||
// 如果是相同的URL,直接播放
|
||
audioRef.current.play().catch(err => {
|
||
console.error("播放失败:", err)
|
||
setPlayingSongId(null)
|
||
setIsAudioLoading(false)
|
||
})
|
||
}
|
||
}
|
||
|
||
// 处理音量变化
|
||
const handleVolumeChange = (values: number[]) => {
|
||
const newVolume = values[0]
|
||
setVolume(newVolume)
|
||
|
||
// 如果音量从0调高,则取消静音
|
||
if (newVolume > 0 && isMuted) {
|
||
setIsMuted(false)
|
||
}
|
||
// 如果音量调到0,则设置为静音
|
||
else if (newVolume === 0 && !isMuted) {
|
||
setIsMuted(true)
|
||
}
|
||
}
|
||
|
||
// 切换静音状态
|
||
const toggleMute = () => {
|
||
setIsMuted(!isMuted)
|
||
}
|
||
|
||
// 处理发布歌曲
|
||
const handlePublishSong = async (songId: string) => {
|
||
try {
|
||
// 找到要发布的歌曲
|
||
const songToPublish = songs.find(s => s.id === songId);
|
||
if (!songToPublish) {
|
||
throw new Error('找不到要发布的歌曲');
|
||
}
|
||
|
||
setIsLoading(true);
|
||
console.log('正在发布歌曲', songId, songToPublish.name);
|
||
|
||
// 调用发布API
|
||
const publishedSong = await publishSong(songId);
|
||
console.log('发布成功', publishedSong);
|
||
|
||
// 更新本地歌曲数据
|
||
setSongs((prevSongs) => prevSongs.map((song) => {
|
||
if (song.id === songId) {
|
||
return {
|
||
...song,
|
||
status: "已发布",
|
||
releaseDate: new Date().toISOString().split('T')[0],
|
||
rawData: publishedSong.rawData
|
||
};
|
||
}
|
||
return song;
|
||
}));
|
||
|
||
// 触发刷新
|
||
setRefreshTrigger(prev => prev + 1);
|
||
|
||
toast({
|
||
title: "发布成功",
|
||
description: `歌曲 "${songToPublish.name}" 已成功发布`,
|
||
variant: "default",
|
||
});
|
||
} catch (error) {
|
||
console.error('发布歌曲失败:', error);
|
||
toast({
|
||
title: "发布失败",
|
||
description: error instanceof Error ? error.message : "无法发布歌曲,请稍后重试",
|
||
variant: "destructive",
|
||
});
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<DashboardShell>
|
||
<DashboardHeader heading="歌曲管理" text="管理洛天依的歌曲库">
|
||
<AddSongDialog onSave={handleAddSong} />
|
||
</DashboardHeader>
|
||
|
||
<div className="flex items-center justify-between space-y-2 mb-6">
|
||
<div className="flex items-center space-x-2">
|
||
<div className="relative">
|
||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||
<Input
|
||
type="search"
|
||
placeholder="搜索歌曲..."
|
||
className="w-[300px] pl-8 border-none bg-white shadow-md focus-visible:ring-pink-500"
|
||
value={searchTerm}
|
||
onChange={(e) => {
|
||
setSearchTerm(e.target.value)
|
||
setCurrentPage(1) // 重置到第一页
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 音频控制面板 - 当有歌曲正在播放时显示 */}
|
||
{playingSongId && (
|
||
<div className="flex items-center space-x-4 bg-pink-50 py-2 px-4 rounded-full shadow-sm">
|
||
<div className="flex-shrink-0">
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
className="h-8 w-8 rounded-full bg-pink-500 text-white hover:bg-pink-600"
|
||
onClick={() => togglePlay(playingSongId)}
|
||
>
|
||
{isAudioLoading ? (
|
||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
||
) : (
|
||
<Pause className="h-4 w-4" />
|
||
)}
|
||
</Button>
|
||
</div>
|
||
<div className="flex-1 min-w-0">
|
||
<div className="truncate text-sm font-medium text-pink-700">
|
||
{songs.find(s => s.id === playingSongId)?.name}
|
||
</div>
|
||
<div className="text-xs text-pink-500">
|
||
{songs.find(s => s.id === playingSongId)?.composer}
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center space-x-2">
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
className="h-7 w-7 rounded-full hover:bg-pink-100"
|
||
onClick={toggleMute}
|
||
>
|
||
{isMuted ? <VolumeX className="h-3.5 w-3.5 text-pink-500" /> : <Volume2 className="h-3.5 w-3.5 text-pink-500" />}
|
||
</Button>
|
||
<div className="w-24">
|
||
<Slider
|
||
value={[volume]}
|
||
min={0}
|
||
max={100}
|
||
step={1}
|
||
onValueChange={handleVolumeChange}
|
||
className="h-1"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<Card className="border-none shadow-lg bg-gradient-to-br from-white to-purple-50">
|
||
<CardHeader>
|
||
<CardTitle className="text-xl font-bold flex items-center">
|
||
<span className="bg-clip-text text-transparent bg-gradient-to-r from-purple-600 to-pink-600">歌曲列表</span>
|
||
<div className="ml-2 h-1 w-10 bg-gradient-to-r from-purple-600 to-pink-600 rounded-full"></div>
|
||
</CardTitle>
|
||
<CardDescription>管理洛天依可以演唱的歌曲</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<Table>
|
||
<TableHeader className="bg-gray-50">
|
||
<TableRow>
|
||
<TableHead className="w-[50px]"></TableHead>
|
||
<TableHead className="w-[100px]">ID</TableHead>
|
||
<TableHead>歌曲名称</TableHead>
|
||
<TableHead>作曲</TableHead>
|
||
<TableHead>作词</TableHead>
|
||
<TableHead>时长</TableHead>
|
||
<TableHead>状态</TableHead>
|
||
<TableHead className="text-right">操作</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{isLoading ? (
|
||
<TableRow>
|
||
<TableCell colSpan={8} className="h-24 text-center">
|
||
加载中...
|
||
</TableCell>
|
||
</TableRow>
|
||
) : songs.length > 0 ? (
|
||
songs.map((song) => (
|
||
<TableRow key={song.id} className="hover:bg-gray-50 transition-colors">
|
||
<TableCell>
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
className={`h-8 w-8 rounded-full ${
|
||
playingSongId === song.id
|
||
? "bg-pink-500 text-white hover:bg-pink-600"
|
||
: "bg-pink-100 hover:bg-pink-200 text-pink-600"
|
||
}`}
|
||
onClick={() => togglePlay(song.id)}
|
||
disabled={isAudioLoading && playingSongId === song.id}
|
||
>
|
||
{isAudioLoading && playingSongId === song.id ? (
|
||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||
) : playingSongId === song.id ? (
|
||
<Pause className="h-4 w-4" />
|
||
) : (
|
||
<Play className="h-4 w-4" />
|
||
)}
|
||
</Button>
|
||
</TableCell>
|
||
<TableCell className="font-medium">{song.id}</TableCell>
|
||
<TableCell className="font-medium text-pink-600">{song.name}</TableCell>
|
||
<TableCell>{song.composer}</TableCell>
|
||
<TableCell>{song.lyricist}</TableCell>
|
||
<TableCell>{song.duration}</TableCell>
|
||
<TableCell>
|
||
<Badge
|
||
className={
|
||
song.status === "已发布"
|
||
? "bg-green-500 hover:bg-green-600"
|
||
: song.status === "已归档"
|
||
? "bg-orange-500 hover:bg-orange-600"
|
||
: "bg-gray-500 hover:bg-gray-600"
|
||
}
|
||
>
|
||
{song.status}
|
||
</Badge>
|
||
</TableCell>
|
||
<TableCell className="text-right">
|
||
<Button variant="ghost" size="icon" className="hover:bg-pink-50 hover:text-pink-600" asChild>
|
||
<Link href={`/songs/${song.id}`}>
|
||
<Eye className="h-4 w-4" />
|
||
<span className="sr-only">查看详情</span>
|
||
</Link>
|
||
</Button>
|
||
|
||
{/* 草稿状态:显示发布按钮 */}
|
||
{song.status !== "已发布" && song.status !== "已归档" && (
|
||
<PublishConfirmationDialog
|
||
title="发布歌曲"
|
||
description="发布后该歌曲将可被用户使用"
|
||
itemName={song.name}
|
||
onPublish={() => handlePublishSong(song.id)}
|
||
/>
|
||
)}
|
||
|
||
{/* 已发布状态:显示归档按钮 */}
|
||
{song.status === "已发布" && (
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
className="hover:bg-orange-50 hover:text-orange-600"
|
||
title="归档"
|
||
onClick={() => handleArchiveSong(song.id, song.name)}
|
||
>
|
||
<Archive className="h-4 w-4" />
|
||
</Button>
|
||
)}
|
||
|
||
{(song.status !== "已发布" || isSuperUser()) && (
|
||
<>
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
className="hover:bg-pink-50 hover:text-pink-600"
|
||
onClick={() => openEditDialog(song)}
|
||
>
|
||
<Edit className="h-4 w-4" />
|
||
</Button>
|
||
|
||
<DeleteConfirmationDialog
|
||
title="删除歌曲"
|
||
description="此操作将永久删除该歌曲及其所有相关数据。"
|
||
itemName={song.name}
|
||
onDelete={() => handleDeleteSong(song.id)}
|
||
/>
|
||
</>
|
||
)}
|
||
</TableCell>
|
||
</TableRow>
|
||
))
|
||
) : (
|
||
<TableRow>
|
||
<TableCell colSpan={8} className="h-24 text-center">
|
||
没有找到匹配的歌曲
|
||
</TableCell>
|
||
</TableRow>
|
||
)}
|
||
</TableBody>
|
||
</Table>
|
||
</CardContent>
|
||
<CardFooter className="flex justify-between">
|
||
<div className="text-sm text-muted-foreground">
|
||
显示 {songs.length > 0 ? (currentPage - 1) * itemsPerPage + 1 : 0}-
|
||
{Math.min(currentPage * itemsPerPage, totalSongs)} 共 {totalSongs} 首歌曲
|
||
</div>
|
||
<div className="flex items-center space-x-2">
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
className="hover:bg-pink-50 hover:text-pink-700 transition-all duration-200"
|
||
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
|
||
disabled={currentPage === 1}
|
||
>
|
||
上一页
|
||
</Button>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
className="hover:bg-pink-50 hover:text-pink-700 transition-all duration-200"
|
||
onClick={() => setCurrentPage((prev) => Math.min(prev + 1, totalPages))}
|
||
disabled={currentPage === totalPages || totalPages === 0}
|
||
>
|
||
下一页
|
||
</Button>
|
||
</div>
|
||
</CardFooter>
|
||
</Card>
|
||
|
||
{/* 编辑歌曲对话框 - 当选中歌曲时显示 */}
|
||
{selectedSong && isEditDialogOpen && (
|
||
<AddSongDialog
|
||
mode="edit"
|
||
initialSong={selectedSong}
|
||
open={isEditDialogOpen}
|
||
onOpenChange={setIsEditDialogOpen}
|
||
onSave={handleEditSong}
|
||
/>
|
||
)}
|
||
</DashboardShell>
|
||
)
|
||
}
|