- Remove GitHub step entirely; single input + Next button - Creates project immediately, redirects to /overview on success - Rewritten in Stackless inline style (no shadcn Dialog/Button/Input) Made-with: Cursor
186 lines
6.7 KiB
TypeScript
186 lines
6.7 KiB
TypeScript
'use client';
|
||
|
||
import { useState, useEffect, useRef } from 'react';
|
||
import { useRouter } from 'next/navigation';
|
||
import { toast } from 'sonner';
|
||
|
||
interface ProjectCreationModalProps {
|
||
open: boolean;
|
||
onOpenChange: (open: boolean) => void;
|
||
workspace: string;
|
||
initialWorkspacePath?: string;
|
||
}
|
||
|
||
export function ProjectCreationModal({ open, onOpenChange, workspace }: ProjectCreationModalProps) {
|
||
const router = useRouter();
|
||
const [productName, setProductName] = useState('');
|
||
const [loading, setLoading] = useState(false);
|
||
const inputRef = useRef<HTMLInputElement>(null);
|
||
|
||
useEffect(() => {
|
||
if (open) {
|
||
setProductName('');
|
||
setLoading(false);
|
||
setTimeout(() => inputRef.current?.focus(), 80);
|
||
}
|
||
}, [open]);
|
||
|
||
// Close on Escape
|
||
useEffect(() => {
|
||
if (!open) return;
|
||
const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onOpenChange(false); };
|
||
window.addEventListener('keydown', handler);
|
||
return () => window.removeEventListener('keydown', handler);
|
||
}, [open, onOpenChange]);
|
||
|
||
const handleCreate = async () => {
|
||
const name = productName.trim();
|
||
if (!name) return;
|
||
setLoading(true);
|
||
try {
|
||
const res = await fetch('/api/projects/create', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
projectName: name,
|
||
projectType: 'scratch',
|
||
slug: name.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
|
||
product: { name },
|
||
}),
|
||
});
|
||
if (!res.ok) {
|
||
const err = await res.json();
|
||
toast.error(err.error || 'Failed to create project');
|
||
return;
|
||
}
|
||
const data = await res.json();
|
||
onOpenChange(false);
|
||
router.push(`/${workspace}/project/${data.projectId}/overview`);
|
||
} catch {
|
||
toast.error('Something went wrong');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
if (!open) return null;
|
||
|
||
return (
|
||
<>
|
||
{/* Backdrop */}
|
||
<div
|
||
onClick={() => onOpenChange(false)}
|
||
style={{
|
||
position: 'fixed', inset: 0, zIndex: 50,
|
||
background: 'rgba(26,26,26,0.35)',
|
||
animation: 'fadeIn 0.15s ease',
|
||
}}
|
||
/>
|
||
|
||
{/* Modal */}
|
||
<div style={{
|
||
position: 'fixed', inset: 0, zIndex: 51,
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
padding: 24, pointerEvents: 'none',
|
||
}}>
|
||
<div
|
||
style={{
|
||
background: '#fff', borderRadius: 14,
|
||
boxShadow: '0 8px 40px rgba(26,26,26,0.14)',
|
||
padding: '36px 40px',
|
||
width: '100%', maxWidth: 480,
|
||
fontFamily: 'Outfit, sans-serif',
|
||
pointerEvents: 'all',
|
||
animation: 'slideUp 0.18s cubic-bezier(0.4,0,0.2,1)',
|
||
}}
|
||
onClick={e => e.stopPropagation()}
|
||
>
|
||
<style>{`
|
||
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||
@keyframes slideUp { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
|
||
`}</style>
|
||
|
||
{/* Header */}
|
||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 28 }}>
|
||
<div>
|
||
<h2 style={{ fontFamily: 'Newsreader, serif', fontSize: '1.35rem', fontWeight: 400, color: '#1a1a1a', marginBottom: 4 }}>
|
||
New project
|
||
</h2>
|
||
<p style={{ fontSize: '0.8rem', color: '#a09a90' }}>
|
||
Give your project a name to get started.
|
||
</p>
|
||
</div>
|
||
<button
|
||
onClick={() => onOpenChange(false)}
|
||
style={{
|
||
background: 'none', border: 'none', cursor: 'pointer',
|
||
color: '#b5b0a6', fontSize: '1.1rem', lineHeight: 1,
|
||
padding: '2px 4px', borderRadius: 4,
|
||
transition: 'color 0.12s',
|
||
}}
|
||
onMouseEnter={e => (e.currentTarget.style.color = '#6b6560')}
|
||
onMouseLeave={e => (e.currentTarget.style.color = '#b5b0a6')}
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
|
||
{/* Name input */}
|
||
<div style={{ marginBottom: 20 }}>
|
||
<label style={{ display: 'block', fontSize: '0.72rem', fontWeight: 600, color: '#6b6560', marginBottom: 7, letterSpacing: '0.02em' }}>
|
||
Project name
|
||
</label>
|
||
<input
|
||
ref={inputRef}
|
||
type="text"
|
||
value={productName}
|
||
onChange={e => setProductName(e.target.value)}
|
||
onKeyDown={e => { if (e.key === 'Enter' && productName.trim() && !loading) handleCreate(); }}
|
||
placeholder="e.g. Foxglove, Meridian, OpsAI…"
|
||
style={{
|
||
width: '100%', padding: '11px 14px',
|
||
borderRadius: 8, border: '1px solid #e0dcd4',
|
||
background: '#faf8f5', fontSize: '0.9rem',
|
||
fontFamily: 'Outfit, sans-serif', color: '#1a1a1a',
|
||
outline: 'none', transition: 'border-color 0.12s',
|
||
boxSizing: 'border-box',
|
||
}}
|
||
onFocus={e => (e.currentTarget.style.borderColor = '#1a1a1a')}
|
||
onBlur={e => (e.currentTarget.style.borderColor = '#e0dcd4')}
|
||
/>
|
||
</div>
|
||
|
||
{/* Create button */}
|
||
<button
|
||
onClick={handleCreate}
|
||
disabled={!productName.trim() || loading}
|
||
style={{
|
||
width: '100%', padding: '12px',
|
||
borderRadius: 8, border: 'none',
|
||
background: productName.trim() && !loading ? '#1a1a1a' : '#e0dcd4',
|
||
color: productName.trim() && !loading ? '#fff' : '#b5b0a6',
|
||
fontSize: '0.88rem', fontWeight: 600,
|
||
fontFamily: 'Outfit, sans-serif',
|
||
cursor: productName.trim() && !loading ? 'pointer' : 'not-allowed',
|
||
transition: 'opacity 0.15s, background 0.15s',
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
|
||
}}
|
||
onMouseEnter={e => { if (productName.trim() && !loading) (e.currentTarget.style.opacity = '0.85'); }}
|
||
onMouseLeave={e => { (e.currentTarget.style.opacity = '1'); }}
|
||
>
|
||
{loading ? (
|
||
<>
|
||
<span style={{ width: 14, height: 14, borderRadius: '50%', border: '2px solid #fff4', borderTopColor: '#fff', animation: 'spin 0.7s linear infinite', display: 'inline-block' }} />
|
||
<style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
|
||
Creating…
|
||
</>
|
||
) : (
|
||
'Next →'
|
||
)}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</>
|
||
);
|
||
}
|