// 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
{`
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ┃
┃ ░ D E P L O Y M E N T C O M P L E T E ░ ┃
┃ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛`}