Files
vibn-frontend/components/project-creation-modal.tsx
Mark Henderson 7602d81120 Simplify project creation: name → create → redirect
- 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
2026-03-02 19:05:50 -08:00

186 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
</>
);
}