feat: [US-015] - Rename project
Add rename functionality to project cards on the dashboard: - Edit/rename icon button on each project card (visible on hover) - Modal dialog with project name input field - Supabase update for project name - Real-time UI update without page reload - Success toast notification after rename - Enter key support for quick rename - Error handling and display Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
9e03a2b9b3
commit
4d3f288784
|
|
@ -9,6 +9,7 @@ interface ProjectCardProps {
|
||||||
name: string
|
name: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
onDelete: (id: string) => void
|
onDelete: (id: string) => void
|
||||||
|
onRename: (id: string, newName: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(dateString: string): string {
|
function formatDate(dateString: string): string {
|
||||||
|
|
@ -27,9 +28,14 @@ export default function ProjectCard({
|
||||||
name,
|
name,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onRename,
|
||||||
}: ProjectCardProps) {
|
}: ProjectCardProps) {
|
||||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||||
const [isDeleting, setIsDeleting] = useState(false)
|
const [isDeleting, setIsDeleting] = useState(false)
|
||||||
|
const [showRenameDialog, setShowRenameDialog] = useState(false)
|
||||||
|
const [isRenaming, setIsRenaming] = useState(false)
|
||||||
|
const [newName, setNewName] = useState(name)
|
||||||
|
const [renameError, setRenameError] = useState<string | null>(null)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const handleCardClick = () => {
|
const handleCardClick = () => {
|
||||||
|
|
@ -65,15 +71,80 @@ export default function ProjectCard({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleRenameClick = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setNewName(name)
|
||||||
|
setRenameError(null)
|
||||||
|
setShowRenameDialog(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirmRename = async () => {
|
||||||
|
if (!newName.trim()) {
|
||||||
|
setRenameError('Project name cannot be empty')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newName.trim() === name) {
|
||||||
|
setShowRenameDialog(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsRenaming(true)
|
||||||
|
setRenameError(null)
|
||||||
|
|
||||||
|
const supabase = createClient()
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('projects')
|
||||||
|
.update({ name: newName.trim() })
|
||||||
|
.eq('id', id)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
setIsRenaming(false)
|
||||||
|
setRenameError('Failed to rename project: ' + error.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsRenaming(false)
|
||||||
|
setShowRenameDialog(false)
|
||||||
|
onRename(id, newName.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancelRename = () => {
|
||||||
|
if (!isRenaming) {
|
||||||
|
setShowRenameDialog(false)
|
||||||
|
setRenameError(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
onClick={handleCardClick}
|
onClick={handleCardClick}
|
||||||
className="group relative cursor-pointer rounded-lg border border-zinc-200 bg-white p-6 transition-all hover:border-blue-300 hover:shadow-md dark:border-zinc-700 dark:bg-zinc-800 dark:hover:border-blue-600"
|
className="group relative cursor-pointer rounded-lg border border-zinc-200 bg-white p-6 transition-all hover:border-blue-300 hover:shadow-md dark:border-zinc-700 dark:bg-zinc-800 dark:hover:border-blue-600"
|
||||||
>
|
>
|
||||||
|
<div className="absolute right-3 top-3 flex gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
||||||
|
<button
|
||||||
|
onClick={handleRenameClick}
|
||||||
|
className="rounded p-1 text-zinc-400 hover:bg-blue-100 hover:text-blue-600 dark:hover:bg-blue-900/30 dark:hover:text-blue-400"
|
||||||
|
title="Rename project"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleDeleteClick}
|
onClick={handleDeleteClick}
|
||||||
className="absolute right-3 top-3 rounded p-1 text-zinc-400 opacity-0 transition-opacity hover:bg-red-100 hover:text-red-600 group-hover:opacity-100 dark:hover:bg-red-900/30 dark:hover:text-red-400"
|
className="rounded p-1 text-zinc-400 hover:bg-red-100 hover:text-red-600 dark:hover:bg-red-900/30 dark:hover:text-red-400"
|
||||||
title="Delete project"
|
title="Delete project"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
|
@ -90,6 +161,7 @@ export default function ProjectCard({
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
<h2 className="pr-8 text-lg font-semibold text-zinc-900 group-hover:text-blue-600 dark:text-zinc-50 dark:group-hover:text-blue-400">
|
<h2 className="pr-8 text-lg font-semibold text-zinc-900 group-hover:text-blue-600 dark:text-zinc-50 dark:group-hover:text-blue-400">
|
||||||
{name}
|
{name}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
@ -155,6 +227,87 @@ export default function ProjectCard({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showRenameDialog && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-black/50"
|
||||||
|
onClick={handleCancelRename}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<div className="relative w-full max-w-md rounded-lg bg-white p-6 shadow-xl dark:bg-zinc-800">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900/30">
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5 text-blue-600 dark:text-blue-400"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-lg font-semibold text-zinc-900 dark:text-zinc-50">
|
||||||
|
Rename Project
|
||||||
|
</h3>
|
||||||
|
<div className="mt-4">
|
||||||
|
<label
|
||||||
|
htmlFor="project-name"
|
||||||
|
className="block text-sm font-medium text-zinc-700 dark:text-zinc-300"
|
||||||
|
>
|
||||||
|
Project Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="project-name"
|
||||||
|
value={newName}
|
||||||
|
onChange={(e) => setNewName(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && !isRenaming) {
|
||||||
|
handleConfirmRename()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
disabled={isRenaming}
|
||||||
|
className="mt-1 block w-full rounded-md border border-zinc-300 bg-white px-3 py-2 text-zinc-900 placeholder-zinc-400 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:cursor-not-allowed disabled:opacity-50 dark:border-zinc-600 dark:bg-zinc-700 dark:text-zinc-50 dark:placeholder-zinc-500 dark:focus:border-blue-400 dark:focus:ring-blue-400"
|
||||||
|
placeholder="Enter project name"
|
||||||
|
/>
|
||||||
|
{renameError && (
|
||||||
|
<p className="mt-2 text-sm text-red-600 dark:text-red-400">
|
||||||
|
{renameError}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex justify-end gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleCancelRename}
|
||||||
|
disabled={isRenaming}
|
||||||
|
className="rounded-md border border-zinc-300 bg-white px-4 py-2 text-sm font-medium text-zinc-700 transition-colors hover:bg-zinc-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-zinc-600 dark:bg-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-600 dark:focus:ring-offset-zinc-800"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleConfirmRename}
|
||||||
|
disabled={isRenaming}
|
||||||
|
className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:focus:ring-offset-zinc-800"
|
||||||
|
>
|
||||||
|
{isRenaming ? 'Renaming...' : 'Rename'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,13 @@ export default function ProjectList({ initialProjects }: ProjectListProps) {
|
||||||
setToast({ message: 'Project deleted successfully', type: 'success' })
|
setToast({ message: 'Project deleted successfully', type: 'success' })
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const handleRename = useCallback((id: string, newName: string) => {
|
||||||
|
setProjects((prev) =>
|
||||||
|
prev.map((p) => (p.id === id ? { ...p, name: newName } : p))
|
||||||
|
)
|
||||||
|
setToast({ message: 'Project renamed successfully', type: 'success' })
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleCloseToast = useCallback(() => {
|
const handleCloseToast = useCallback(() => {
|
||||||
setToast(null)
|
setToast(null)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
@ -67,6 +74,7 @@ export default function ProjectList({ initialProjects }: ProjectListProps) {
|
||||||
name={project.name}
|
name={project.name}
|
||||||
updatedAt={project.updated_at}
|
updatedAt={project.updated_at}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
|
onRename={handleRename}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue