feat: [US-013] - Create new project
Add NewProjectButton component with modal dialog for creating new projects: - Button displays on dashboard with plus icon - Modal with project name input - Creates project in Supabase with empty flowchart_data - Redirects to /editor/[projectId] on success - Error handling with user feedback Also fixes lint error in signup page (setState in effect). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5b87af6244
commit
10ac9fe1e0
|
|
@ -1,5 +1,6 @@
|
||||||
import { createClient } from '@/lib/supabase/server'
|
import { createClient } from '@/lib/supabase/server'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import NewProjectButton from '@/components/NewProjectButton'
|
||||||
|
|
||||||
interface Project {
|
interface Project {
|
||||||
id: string
|
id: string
|
||||||
|
|
@ -47,13 +48,16 @@ export default async function DashboardPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-8">
|
<div className="mb-8 flex items-start justify-between">
|
||||||
<h1 className="text-2xl font-bold text-zinc-900 dark:text-zinc-50">
|
<div>
|
||||||
Your Projects
|
<h1 className="text-2xl font-bold text-zinc-900 dark:text-zinc-50">
|
||||||
</h1>
|
Your Projects
|
||||||
<p className="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
|
</h1>
|
||||||
Select a project to open the flowchart editor
|
<p className="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
|
||||||
</p>
|
Select a project to open the flowchart editor
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<NewProjectButton />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{projects && projects.length > 0 ? (
|
{projects && projects.length > 0 ? (
|
||||||
|
|
|
||||||
|
|
@ -8,19 +8,14 @@ import { createClient } from '@/lib/supabase/client'
|
||||||
export default function SignupPage() {
|
export default function SignupPage() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const [email, setEmail] = useState('')
|
// Pre-fill email if provided in URL (from invite link)
|
||||||
|
const [email, setEmail] = useState(searchParams.get('email') ?? '')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const [confirmPassword, setConfirmPassword] = useState('')
|
const [confirmPassword, setConfirmPassword] = useState('')
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Pre-fill email if provided in URL (from invite link)
|
|
||||||
const emailParam = searchParams.get('email')
|
|
||||||
if (emailParam) {
|
|
||||||
setEmail(emailParam)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle invite/signup token from URL hash
|
// Handle invite/signup token from URL hash
|
||||||
// Supabase adds tokens to the URL hash after redirect
|
// Supabase adds tokens to the URL hash after redirect
|
||||||
const handleTokenFromUrl = async () => {
|
const handleTokenFromUrl = async () => {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { createClient } from '@/lib/supabase/client'
|
||||||
|
|
||||||
|
export default function NewProjectButton() {
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const [projectName, setProjectName] = useState('')
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const handleOpen = () => {
|
||||||
|
setIsOpen(true)
|
||||||
|
setProjectName('')
|
||||||
|
setError(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
if (!isLoading) {
|
||||||
|
setIsOpen(false)
|
||||||
|
setProjectName('')
|
||||||
|
setError(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (!projectName.trim()) {
|
||||||
|
setError('Project name is required')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
const supabase = createClient()
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: { user },
|
||||||
|
} = await supabase.auth.getUser()
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
setError('You must be logged in to create a project')
|
||||||
|
setIsLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error: insertError } = await supabase
|
||||||
|
.from('projects')
|
||||||
|
.insert({
|
||||||
|
user_id: user.id,
|
||||||
|
name: projectName.trim(),
|
||||||
|
flowchart_data: { nodes: [], edges: [] },
|
||||||
|
})
|
||||||
|
.select('id')
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (insertError) {
|
||||||
|
setError(insertError.message || 'Failed to create project')
|
||||||
|
setIsLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
router.push(`/editor/${data.id}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={handleOpen}
|
||||||
|
className="inline-flex items-center gap-2 rounded-lg 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 dark:focus:ring-offset-zinc-900"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M12 4v16m8-8H4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
New Project
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-black/50"
|
||||||
|
onClick={handleClose}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<div className="relative w-full max-w-md rounded-lg bg-white p-6 shadow-xl dark:bg-zinc-800">
|
||||||
|
<h2 className="text-lg font-semibold text-zinc-900 dark:text-zinc-50">
|
||||||
|
Create New Project
|
||||||
|
</h2>
|
||||||
|
<p className="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
|
||||||
|
Enter a name for your new visual novel project.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="mt-4">
|
||||||
|
<label
|
||||||
|
htmlFor="projectName"
|
||||||
|
className="block text-sm font-medium text-zinc-700 dark:text-zinc-300"
|
||||||
|
>
|
||||||
|
Project Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="projectName"
|
||||||
|
value={projectName}
|
||||||
|
onChange={(e) => setProjectName(e.target.value)}
|
||||||
|
placeholder="My Visual Novel"
|
||||||
|
disabled={isLoading}
|
||||||
|
autoFocus
|
||||||
|
className="mt-1 block w-full rounded-md border border-zinc-300 bg-white px-3 py-2 text-zinc-900 placeholder-zinc-400 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"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<p className="mt-2 text-sm text-red-600 dark:text-red-400">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-6 flex justify-end gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleClose}
|
||||||
|
disabled={isLoading}
|
||||||
|
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="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
{isLoading ? 'Creating...' : 'Create Project'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue