'use client'; import { useState, useCallback, useEffect, useRef } from 'react'; import { useRouter } from 'next/navigation'; interface ProjectSettingsFormProps { workspaceId: string; projectId: string; initialName: string; initialDescription: string; initialAppUrl: string; initialFramework: string; memberRole: string; } const SECTION: React.CSSProperties = { background: '#FFFFFF', border: '1px solid rgba(0,0,0,0.07)', borderRadius: 14, padding: '24px 28px', marginBottom: 20, }; const LABEL: React.CSSProperties = { display: 'block', fontSize: '0.8125rem', fontWeight: 600, color: '#0A0A0A', marginBottom: 6, }; const INPUT: React.CSSProperties = { width: '100%', fontSize: '0.875rem', padding: '9px 12px', border: '1px solid rgba(0,0,0,0.12)', borderRadius: 9, outline: 'none', fontFamily: "'Inter', -apple-system, sans-serif", color: '#0A0A0A', background: '#FAFAFA', boxSizing: 'border-box', }; const BTN_PRIMARY: React.CSSProperties = { display: 'inline-flex', alignItems: 'center', gap: 6, background: '#0A0A0A', color: '#FFFFFF', fontSize: '0.875rem', fontWeight: 600, padding: '9px 20px', borderRadius: 9, border: 'none', cursor: 'pointer', letterSpacing: '-0.01em', }; const FRAMEWORKS = ['', 'Next.js', 'Vite + React', 'Remix', 'SvelteKit', 'Nuxt', 'Other'] as const; export function ProjectSettingsForm({ workspaceId, projectId, initialName, initialDescription, initialAppUrl, initialFramework, memberRole, }: ProjectSettingsFormProps) { const router = useRouter(); const isOwnerOrDev = memberRole === 'OWNER' || memberRole === 'DEVELOPER' || memberRole === 'DESIGNER'; /* ── Form state ── */ const [name, setName] = useState(initialName); const [description, setDescription] = useState(initialDescription); const [appUrl, setAppUrl] = useState(initialAppUrl); const [framework, setFramework] = useState(initialFramework); const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle'); const saveTimer = useRef | undefined>(undefined); useEffect(() => { return () => { clearTimeout(saveTimer.current); }; }, []); const isDirty = name.trim() !== initialName || description !== initialDescription || appUrl !== initialAppUrl || framework !== initialFramework; const save = useCallback(async () => { if (!isDirty || !name.trim()) return; clearTimeout(saveTimer.current); setSaveStatus('saving'); try { const res = await fetch(`/api/workspace/${workspaceId}/projects/${projectId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name.trim(), description: description || null, app_url: appUrl || null, framework: framework || null, }), }); if (!res.ok) throw new Error('Save failed'); setSaveStatus('saved'); router.refresh(); saveTimer.current = setTimeout(() => setSaveStatus('idle'), 2500); } catch { setSaveStatus('error'); saveTimer.current = setTimeout(() => setSaveStatus('idle'), 3000); } }, [isDirty, name, description, appUrl, framework, workspaceId, projectId, router]); /* ── Delete ── */ const [deleteConfirm, setDeleteConfirm] = useState(''); const [deleting, setDeleting] = useState(false); // Guard against deleting an unsaved name: always compare against the persisted name. const deleteProject = useCallback(async () => { if (deleteConfirm.trim() !== initialName.trim()) return; setDeleting(true); try { const res = await fetch(`/api/workspace/${workspaceId}/projects/${projectId}`, { method: 'DELETE' }); if (!res.ok) throw new Error('Delete failed'); router.push(`/workspace/${workspaceId}`); } catch { setDeleting(false); window.alert('Delete failed — please try again.'); } }, [deleteConfirm, initialName, workspaceId, projectId, router]); return ( <> {/* General */}

General

setName(e.target.value)} disabled={!isOwnerOrDev} />