feat: [US-014] - Delete project
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
72a66ba39c
commit
87653b86cb
|
|
@ -1,23 +1,6 @@
|
|||
import { createClient } from '@/lib/supabase/server'
|
||||
import Link from 'next/link'
|
||||
import NewProjectButton from '@/components/NewProjectButton'
|
||||
|
||||
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',
|
||||
})
|
||||
}
|
||||
import ProjectList from '@/components/ProjectList'
|
||||
|
||||
export default async function DashboardPage() {
|
||||
const supabase = await createClient()
|
||||
|
|
@ -60,47 +43,7 @@ export default async function DashboardPage() {
|
|||
<NewProjectButton />
|
||||
</div>
|
||||
|
||||
{projects && projects.length > 0 ? (
|
||||
<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>
|
||||
)}
|
||||
<ProjectList initialProjects={projects || []} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 "{name}"? 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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue