Add inline song title editing in sidebar
This commit is contained in:
+115
-7
@@ -32,6 +32,10 @@ export const RightSidebar: React.FC<RightSidebarProps> = ({ song, onClose, onOpe
|
|||||||
const [shareModalOpen, setShareModalOpen] = useState(false);
|
const [shareModalOpen, setShareModalOpen] = useState(false);
|
||||||
const [copiedStyle, setCopiedStyle] = useState(false);
|
const [copiedStyle, setCopiedStyle] = useState(false);
|
||||||
const [copiedLyrics, setCopiedLyrics] = useState(false);
|
const [copiedLyrics, setCopiedLyrics] = useState(false);
|
||||||
|
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
||||||
|
const [titleDraft, setTitleDraft] = useState('');
|
||||||
|
const [titleError, setTitleError] = useState<string | null>(null);
|
||||||
|
const [isSavingTitle, setIsSavingTitle] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (song) {
|
if (song) {
|
||||||
@@ -39,6 +43,57 @@ export const RightSidebar: React.FC<RightSidebarProps> = ({ song, onClose, onOpe
|
|||||||
}
|
}
|
||||||
}, [song, user]);
|
}, [song, user]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (song) {
|
||||||
|
setTitleDraft(song.title || '');
|
||||||
|
setIsEditingTitle(false);
|
||||||
|
setTitleError(null);
|
||||||
|
setIsSavingTitle(false);
|
||||||
|
}
|
||||||
|
}, [song?.id]);
|
||||||
|
|
||||||
|
const startTitleEdit = () => {
|
||||||
|
if (!song || !isOwner) return;
|
||||||
|
setTitleDraft(song.title || '');
|
||||||
|
setTitleError(null);
|
||||||
|
setIsEditingTitle(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelTitleEdit = () => {
|
||||||
|
if (!song) return;
|
||||||
|
setTitleDraft(song.title || '');
|
||||||
|
setTitleError(null);
|
||||||
|
setIsEditingTitle(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveTitleEdit = async () => {
|
||||||
|
if (!song) return;
|
||||||
|
if (!token) {
|
||||||
|
setTitleError('Please sign in to rename.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const trimmed = titleDraft.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
setTitleError('Title cannot be empty.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (trimmed === song.title) {
|
||||||
|
setIsEditingTitle(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsSavingTitle(true);
|
||||||
|
setTitleError(null);
|
||||||
|
try {
|
||||||
|
await songsApi.updateSong(song.id, { title: trimmed }, token);
|
||||||
|
onSongUpdate?.({ ...song, title: trimmed });
|
||||||
|
setIsEditingTitle(false);
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : 'Rename failed';
|
||||||
|
setTitleError(message);
|
||||||
|
} finally {
|
||||||
|
setIsSavingTitle(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!song) return (
|
if (!song) return (
|
||||||
<div className="w-full h-full bg-zinc-50 dark:bg-suno-panel border-l border-zinc-200 dark:border-white/5 flex items-center justify-center text-zinc-400 dark:text-zinc-500 text-sm transition-colors duration-300">
|
<div className="w-full h-full bg-zinc-50 dark:bg-suno-panel border-l border-zinc-200 dark:border-white/5 flex items-center justify-center text-zinc-400 dark:text-zinc-500 text-sm transition-colors duration-300">
|
||||||
@@ -111,14 +166,67 @@ export const RightSidebar: React.FC<RightSidebarProps> = ({ song, onClose, onOpe
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex justify-between items-start gap-2">
|
<div className="flex justify-between items-start gap-2">
|
||||||
<div className="flex items-center gap-2 flex-1">
|
<div className="flex items-center gap-2 flex-1">
|
||||||
<h2
|
{!isEditingTitle ? (
|
||||||
onClick={() => onNavigateToSong?.(song.id)}
|
<h2
|
||||||
className="text-2xl font-bold text-zinc-900 dark:text-white leading-tight tracking-tight cursor-pointer hover:underline"
|
onClick={() => onNavigateToSong?.(song.id)}
|
||||||
>
|
className="text-2xl font-bold text-zinc-900 dark:text-white leading-tight tracking-tight cursor-pointer hover:underline"
|
||||||
{song.title}
|
>
|
||||||
</h2>
|
{song.title}
|
||||||
|
</h2>
|
||||||
|
) : (
|
||||||
|
<div className="w-full">
|
||||||
|
<input
|
||||||
|
value={titleDraft}
|
||||||
|
onChange={(e) => setTitleDraft(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
void saveTitleEdit();
|
||||||
|
}
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
cancelTitleEdit();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-full text-xl font-bold text-zinc-900 dark:text-white bg-white dark:bg-black/30 border border-zinc-200 dark:border-white/10 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-pink-500/40"
|
||||||
|
maxLength={120}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-2 mt-2">
|
||||||
|
<button
|
||||||
|
onClick={() => void saveTitleEdit()}
|
||||||
|
disabled={isSavingTitle}
|
||||||
|
className="px-3 py-1.5 rounded-md text-xs font-semibold bg-pink-600 text-white hover:bg-pink-700 disabled:opacity-60"
|
||||||
|
>
|
||||||
|
{isSavingTitle ? 'Saving...' : 'Save'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={cancelTitleEdit}
|
||||||
|
disabled={isSavingTitle}
|
||||||
|
className="px-3 py-1.5 rounded-md text-xs font-semibold bg-zinc-200 text-zinc-700 hover:bg-zinc-300 dark:bg-white/10 dark:text-zinc-200 dark:hover:bg-white/20 disabled:opacity-60"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
{titleError && (
|
||||||
|
<span className="text-xs text-red-500">{titleError}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
{isOwner && !isEditingTitle && (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
startTitleEdit();
|
||||||
|
}}
|
||||||
|
className="text-zinc-400 hover:text-black dark:hover:text-white p-1 mr-1"
|
||||||
|
title="Rename song"
|
||||||
|
>
|
||||||
|
<Edit3 size={18} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -332,4 +440,4 @@ const ActionButton: React.FC<{ icon: React.ReactNode; label?: string; active?: b
|
|||||||
{icon}
|
{icon}
|
||||||
{label && <span className="text-xs font-semibold">{label}</span>}
|
{label && <span className="text-xs font-semibold">{label}</span>}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
Generated
+5
@@ -53,6 +53,7 @@
|
|||||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.0",
|
||||||
@@ -1485,6 +1486,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -2136,6 +2138,7 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -2201,6 +2204,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -2536,6 +2540,7 @@
|
|||||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.4.4",
|
||||||
|
|||||||
Reference in New Issue
Block a user