// Repo Creator — connects to real /create and /check-name endpoints const { useState, useEffect, useRef, useMemo, useCallback } = React; const STAGES = [ { name: 'Validate', pct: 20 }, { name: 'Compose', pct: 50 }, { name: 'Push', pct: 80 }, { name: 'Topics', pct: 95 }, ]; const TERMINAL_LINES = [ { d: 0, p: '$', t: 'devex create --interactive', cls: 'br' }, { d: 280, p: '›', t: 'validating repository name…', cls: 'dim' }, { d: 720, p: '✓', t: 'name available', cls: 'ok' }, { d: 1100, p: '›', t: 'composing service manifest', cls: 'dim' }, { d: 1500, p: '+', t: 'docker-compose.yml', cls: 'ok' }, { d: 1700, p: '+', t: '.github/workflows/deploy.yml', cls: 'ok' }, { d: 2200, p: '›', t: 'pushing to github…', cls: 'dim' }, { d: 3200, p: '✓', t: 'remote: ref refs/heads/main updated', cls: 'ok' }, { d: 3500, p: '›', t: 'tagging topics: devex, vps', cls: 'dim' }, { d: 4000, p: '✓', t: 'topics applied', cls: 'ok' }, { d: 4300, p: '✓', t: 'ready · done', cls: 'br' }, ]; function Creator({ tweaks, onOpenReadme, onTriggerConfetti }) { const { SERVICES, TEMPLATES, randName } = window.DEVEX_DATA; const I = window.I; const [tab, setTab] = useState('app'); const [name, setName] = useState(() => randName()); const [validation, setValidation] = useState('checking'); const [shuffleSpin, setShuffleSpin] = useState(false); const [selected, setSelected] = useState(SERVICES.length > 0 ? [SERVICES[0].id] : []); const [template, setTemplate] = useState(TEMPLATES.length > 0 ? TEMPLATES[0].id : ''); const [topics, setTopics] = useState(true); const [phase, setPhase] = useState('idle'); const [progress, setProgress] = useState(0); const [stageIdx, setStageIdx] = useState(-1); const [logLines, setLogLines] = useState([]); const [createdData, setCreatedData] = useState(null); const [createError, setCreateError] = useState(null); const pollRef = useRef(null); // Real name check via /check-name useEffect(() => { if (tweaks.demoValidation && tweaks.demoValidation !== 'auto') { setValidation(tweaks.demoValidation); return; } if (!name.trim()) { setValidation('invalid'); return; } setValidation('checking'); const ctrl = new AbortController(); const t = setTimeout(async () => { try { const r = await fetch(`/check-name?name=${encodeURIComponent(name)}`, { signal: ctrl.signal }); const data = await r.json(); if (!data.valid) setValidation('invalid'); else if (data.available === false) setValidation('invalid'); else setValidation('valid'); } catch (err) { if (err.name !== 'AbortError') setValidation('valid'); // allow if offline check fails } }, 450); return () => { clearTimeout(t); ctrl.abort(); }; }, [name, tweaks.demoValidation]); useEffect(() => { if (tweaks.demoPhase && tweaks.demoPhase !== 'auto') { if (tweaks.demoPhase === 'creating') { setPhase('creating'); setProgress(60); setStageIdx(2); } else if (tweaks.demoPhase === 'success') jumpSuccess(); else if (tweaks.demoPhase === 'idle') resetAll(); } }, [tweaks.demoPhase]); const toggleService = (id) => { if (phase !== 'idle') return; setSelected(s => s.includes(id) ? s.filter(x => x !== id) : [...s, id]); }; const doShuffle = () => { setShuffleSpin(true); setName(randName()); setTimeout(() => setShuffleSpin(false), 420); }; const resetAll = () => { setPhase('idle'); setProgress(0); setStageIdx(-1); setLogLines([]); setCreatedData(null); setCreateError(null); if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } }; const jumpSuccess = () => { const userLogin = window.DEVEX_INIT?.user?.login || 'org'; setPhase('success'); setProgress(100); setStageIdx(STAGES.length - 1); setLogLines(TERMINAL_LINES); setCreatedData({ repo_url: `https://github.com/${userLogin}/${name}`, app_name: name }); }; const startCreate = async (silent = false) => { if (phase !== 'idle') return; setPhase('creating'); setProgress(5); setStageIdx(0); setLogLines([]); setCreatedData(null); setCreateError(null); // Optimistic progress animation const ANIM = [ { d: 400, pct: 20, idx: 0 }, { d: 1400, pct: 48, idx: 1 }, { d: 2800, pct: 72, idx: 2 }, ]; const timers = ANIM.map(s => setTimeout(() => { setProgress(s.pct); setStageIdx(s.idx); }, s.d)); TERMINAL_LINES.forEach(line => { timers.push(setTimeout(() => setLogLines(prev => [...prev, line]), line.d)); }); const formData = new FormData(); formData.append('app_name', name); formData.append('description', ''); if (tab === 'app') { formData.append('project_type', 'vps'); selected.forEach(id => formData.append('service_type', id)); } else { formData.append('project_type', 'repo-template'); formData.append('repo_template', template); } try { const r = await fetch('/create', { method: 'POST', headers: { 'X-Devex-SPA': '1' }, body: formData, }); timers.forEach(clearTimeout); const json = await r.json(); if (!r.ok && !json.pending) { setCreateError(json.error || 'Creation failed. Please try again.'); setPhase('idle'); setProgress(0); return; } setProgress(90); setStageIdx(2); if (json.pending) { // Repo template flow — poll setProgress(95); setStageIdx(3); let attempts = 0; const MAX = 60; pollRef.current = setInterval(async () => { attempts++; try { const pr = await fetch(`/check-repo-ready?owner=${encodeURIComponent(json.owner)}&repo=${encodeURIComponent(json.repo)}`); const pj = await pr.json(); if (pj.ready) { clearInterval(pollRef.current); pollRef.current = null; setProgress(100); setStageIdx(STAGES.length - 1); setCreatedData(json); setPhase('success'); if (!silent) onTriggerConfetti(); } } catch (_) {} if (attempts >= MAX) { clearInterval(pollRef.current); pollRef.current = null; setCreatedData(json); setPhase('success'); if (!silent) onTriggerConfetti(); } }, 3000); } else { setProgress(100); setStageIdx(STAGES.length - 1); setCreatedData(json); setPhase('success'); if (!silent) onTriggerConfetti(); } } catch (err) { timers.forEach(clearTimeout); setCreateError('Network error. Please try again.'); setPhase('idle'); setProgress(0); } }; useEffect(() => () => { if (pollRef.current) clearInterval(pollRef.current); }, []); const submitDisabled = phase !== 'idle' || validation !== 'valid' || (tab === 'app' && selected.length === 0) || (tab === 'template' && !template); if (phase === 'success') { return ; } return (
New Repository
esc to cancel
{/* Tabs */}
{/* Name field */}
Repo Name {(window.DEVEX_INIT?.user?.login || 'org')}/{name || '…'}
{(window.DEVEX_INIT?.user?.login || 'org')}/ setName(e.target.value.toLowerCase().replace(/\s+/g, '-'))} placeholder="adjective-noun" aria-label="Repository name" />
{validation === 'checking' && '○ checking'} {validation === 'valid' && '● available'} {validation === 'invalid' && '✕ taken'}
{validation === 'invalid' ? // name conflicts with existing repo or violates [a-z0-9-] policy : // kebab-case · 3-40 chars · the URL slug}
{/* Tab content */} {tab === 'app' ? (
Services · {selected.length} selected click to toggle
{SERVICES.map(s => { const Glyph = I[s.icon]; const isSel = selected.includes(s.id); return (
toggleService(s.id)}>
{isSel && }
{Glyph && }
{s.name}
{s.desc}
{s.tag}
); })}
) : (
Choose a template
{TEMPLATES.map(t => (
setTemplate(t.id)}>
{t.name}
{template === t.id && (
)}
{t.desc}
{t.stack && t.stack.length > 0 && (
{t.stack.map(s => {s})}
)}
))}
)} {/* Topics toggle */}
Auto-tag with topics
{/* Progress (creating phase) */} {phase === 'creating' && (
creating · {STAGES[stageIdx]?.name || 'init'} {progress}%
{STAGES.map((s, i) => (
{i < stageIdx ? '✓' : i === stageIdx ? '›' : '○'} {s.name}
{i <= stageIdx ? `${i === stageIdx ? progress : s.pct}%` : '—'}
))}
)} {/* Error state */} {createError && (
{createError}
)} {/* Preview README action */}
{/* CTA */}
); } function SuccessCard({ name, tab, selected, template, onCreateAnother, createdData }) { const I = window.I; const userLogin = window.DEVEX_INIT?.user?.login || 'org'; const repoUrl = createdData?.repo_url || `https://github.com/${userLogin}/${name}`; const displayUrl = repoUrl.replace('https://', ''); return (
Repository pushed
{userLogin}/{name} · {tab === 'app' ? `${selected.length} services` : template} · ready
{`
   ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
   ┃   ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░   ┃
   ┃   ░  D E P L O Y M E N T   C O M P L E T E  ░   ┃
   ┃   ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░   ┃
   ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛`}
URL
{displayUrl}
Default branch
main
Type
{tab === 'app' ? 'VPS service' : 'Repo template'}
Topics
devex · {tab === 'app' ? 'vps' : 'starter'}
); } window.Creator = Creator; window.SuccessCard = SuccessCard; window.TERMINAL_LINES = TERMINAL_LINES;