feat: [US-014] - Delete project

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Gustavo Henrique Santos Souza de Miranda 2026-01-21 04:18:56 -03:00
parent 72a66ba39c
commit 87653b86cb
4 changed files with 328 additions and 59 deletions

View File

@ -1,23 +1,6 @@
import { createClient } from '@/lib/supabase/server' import { createClient } from '@/lib/supabase/server'
import Link from 'next/link'
import NewProjectButton from '@/components/NewProjectButton' import NewProjectButton from '@/components/NewProjectButton'
import ProjectList from '@/components/ProjectList'
interface Project {
id: string
name: string
updated_at: string
}
function formatDate(dateString: string): string {
const date = new Date(dateString)
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
}
export default async function DashboardPage() { export default async function DashboardPage() {
const supabase = await createClient() const supabase = await createClient()
@ -60,47 +43,7 @@ export default async function DashboardPage() {
<NewProjectButton /> <NewProjectButton />
</div> </div>
{projects && projects.length > 0 ? ( <ProjectList initialProjects={projects || []} />
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{projects.map((project: Project) => (
<Link
key={project.id}
href={`/editor/${project.id}`}
className="group 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"
>
<h2 className="text-lg font-semibold text-zinc-900 group-hover:text-blue-600 dark:text-zinc-50 dark:group-hover:text-blue-400">
{project.name}
</h2>
<p className="mt-2 text-sm text-zinc-500 dark:text-zinc-400">
Last updated: {formatDate(project.updated_at)}
</p>
</Link>
))}
</div>
) : (
<div className="rounded-lg border-2 border-dashed border-zinc-300 bg-zinc-50 p-12 text-center dark:border-zinc-700 dark:bg-zinc-800/50">
<svg
className="mx-auto h-12 w-12 text-zinc-400 dark:text-zinc-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
/>
</svg>
<h3 className="mt-4 text-lg font-medium text-zinc-900 dark:text-zinc-50">
No projects yet
</h3>
<p className="mt-2 text-sm text-zinc-500 dark:text-zinc-400">
Get started by creating your first visual novel project.
</p>
</div>
)}
</div> </div>
) )
} }

View File

@ -0,0 +1,160 @@
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { createClient } from '@/lib/supabase/client'
interface ProjectCardProps {
id: string
name: string
updatedAt: string
onDelete: (id: string) => void
}
function formatDate(dateString: string): string {
const date = new Date(dateString)
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
}
export default function ProjectCard({
id,
name,
updatedAt,
onDelete,
}: ProjectCardProps) {
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
const router = useRouter()
const handleCardClick = () => {
router.push(`/editor/${id}`)
}
const handleDeleteClick = (e: React.MouseEvent) => {
e.stopPropagation()
setShowDeleteDialog(true)
}
const handleConfirmDelete = async () => {
setIsDeleting(true)
const supabase = createClient()
const { error } = await supabase.from('projects').delete().eq('id', id)
if (error) {
setIsDeleting(false)
setShowDeleteDialog(false)
alert('Failed to delete project: ' + error.message)
return
}
setIsDeleting(false)
setShowDeleteDialog(false)
onDelete(id)
}
const handleCancelDelete = () => {
if (!isDeleting) {
setShowDeleteDialog(false)
}
}
return (
<>
<div
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"
>
<button
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"
title="Delete project"
>
<svg
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button>
<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}
</h2>
<p className="mt-2 text-sm text-zinc-500 dark:text-zinc-400">
Last updated: {formatDate(updatedAt)}
</p>
</div>
{showDeleteDialog && (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div
className="absolute inset-0 bg-black/50"
onClick={handleCancelDelete}
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-red-100 dark:bg-red-900/30">
<svg
className="h-5 w-5 text-red-600 dark:text-red-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
</div>
<div>
<h3 className="text-lg font-semibold text-zinc-900 dark:text-zinc-50">
Delete Project
</h3>
<p className="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
Are you sure you want to delete &quot;{name}&quot;? This action
cannot be undone and all flowchart data will be permanently
removed.
</p>
</div>
</div>
<div className="mt-6 flex justify-end gap-3">
<button
type="button"
onClick={handleCancelDelete}
disabled={isDeleting}
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={handleConfirmDelete}
disabled={isDeleting}
className="rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:focus:ring-offset-zinc-800"
>
{isDeleting ? 'Deleting...' : 'Delete'}
</button>
</div>
</div>
</div>
)}
</>
)
}

View File

@ -0,0 +1,82 @@
'use client'
import { useState, useCallback } from 'react'
import ProjectCard from './ProjectCard'
import Toast from './Toast'
interface Project {
id: string
name: string
updated_at: string
}
interface ProjectListProps {
initialProjects: Project[]
}
export default function ProjectList({ initialProjects }: ProjectListProps) {
const [projects, setProjects] = useState<Project[]>(initialProjects)
const [toast, setToast] = useState<{
message: string
type: 'success' | 'error'
} | null>(null)
const handleDelete = useCallback((deletedId: string) => {
setProjects((prev) => prev.filter((p) => p.id !== deletedId))
setToast({ message: 'Project deleted successfully', type: 'success' })
}, [])
const handleCloseToast = useCallback(() => {
setToast(null)
}, [])
if (projects.length === 0) {
return (
<div className="rounded-lg border-2 border-dashed border-zinc-300 bg-zinc-50 p-12 text-center dark:border-zinc-700 dark:bg-zinc-800/50">
<svg
className="mx-auto h-12 w-12 text-zinc-400 dark:text-zinc-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
/>
</svg>
<h3 className="mt-4 text-lg font-medium text-zinc-900 dark:text-zinc-50">
No projects yet
</h3>
<p className="mt-2 text-sm text-zinc-500 dark:text-zinc-400">
Get started by creating your first visual novel project.
</p>
</div>
)
}
return (
<>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{projects.map((project) => (
<ProjectCard
key={project.id}
id={project.id}
name={project.name}
updatedAt={project.updated_at}
onDelete={handleDelete}
/>
))}
</div>
{toast && (
<Toast
message={toast.message}
type={toast.type}
onClose={handleCloseToast}
/>
)}
</>
)
}

84
src/components/Toast.tsx Normal file
View File

@ -0,0 +1,84 @@
'use client'
import { useEffect } from 'react'
interface ToastProps {
message: string
type: 'success' | 'error'
onClose: () => void
}
export default function Toast({ message, type, onClose }: ToastProps) {
useEffect(() => {
const timer = setTimeout(() => {
onClose()
}, 3000)
return () => clearTimeout(timer)
}, [onClose])
const bgColor =
type === 'success'
? 'bg-green-600 dark:bg-green-700'
: 'bg-red-600 dark:bg-red-700'
const icon =
type === 'success' ? (
<svg
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
) : (
<svg
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
)
return (
<div className="fixed bottom-4 right-4 z-50 animate-in fade-in slide-in-from-bottom-4">
<div
className={`flex items-center gap-2 rounded-lg px-4 py-3 text-sm font-medium text-white shadow-lg ${bgColor}`}
>
{icon}
<span>{message}</span>
<button
onClick={onClose}
className="ml-2 rounded p-0.5 hover:bg-white/20"
>
<svg
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
</div>
)
}