import { render } from 'solid-js/web'; import { createSignal, createResource, onMount, Show, For } from 'solid-js'; import { Alert, Spinner } from 'solid-bootstrap'; const BASE_URL = '/private/server/exocore/web'; const fetchTemplates = async () => { const res = await fetch(`${BASE_URL}/templates`, { method: 'POST' }); if (!res.ok) throw new Error('Failed to fetch templates'); return res.json(); }; const checkProjectStatus = async () => { try { const res = await fetch(`${BASE_URL}/project/status`, { method: 'POST' }); const json = await res.json(); if (json.exists) window.location.href = `${BASE_URL}/public/dashboard`; } catch (err) { console.error('Error checking project status:', err); } }; function App() { const [templates] = createResource(fetchTemplates); const [step, setStep] = createSignal(1); // 1 for source, 2 for project name const [projectName, setProjectName] = createSignal(''); const [selectedTemplateId, setSelectedTemplateId] = createSignal(''); const [selectedTemplateGitUrl, setSelectedTemplateGitUrl] = createSignal(''); const [customGitUrl, setCustomGitUrl] = createSignal(''); const [projectSourceType, setProjectSourceType] = createSignal('template'); const [status, setStatus] = createSignal(''); const [loading, setLoading] = createSignal(false); onMount(() => { checkProjectStatus(); document.body.style = ` margin: 0; font-family: 'Inter', sans-serif; background: #0a0a0a; color: #e0e0e0; line-height: 1.6; overflow-x: hidden; `; const link = document.createElement('link'); link.href = 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700&display=swap'; link.rel = 'stylesheet'; document.head.appendChild(link); }); const inputStyle = { background: '#2a2a2a', color: '#fff', border: '1px solid #444', padding: '0.8rem 1rem', 'border-radius': '8px', width: '100%', 'font-family': 'inherit', 'font-size': '1rem', 'box-shadow': 'inset 0 1px 3px rgba(0,0,0,0.3)', 'transition': 'border-color 0.2s ease, box-shadow 0.2s ease', }; const inputFocusStyle = { 'border-color': '#007bff', 'box-shadow': '0 0 0 3px rgba(0, 123, 255, 0.25)', }; const buttonPrimaryColor = '#007bff'; const buttonHoverColor = '#0056b3'; const buttonSecondaryColor = '#555'; const buttonSecondaryHoverColor = '#777'; const customRadioContainerStyle = { 'display': 'flex', 'align-items': 'center', 'cursor': 'pointer', 'user-select': 'none', 'margin-right': '1.5rem', }; const hiddenRadioInputStyle = { 'position': 'absolute', 'opacity': '0', 'width': '0', 'height': '0', }; const customRadioIndicatorStyle = { 'display': 'inline-block', 'width': '20px', 'height': '20px', 'border': '2px solid #666', 'border-radius': '50%', 'margin-right': '0.8rem', 'position': 'relative', 'transition': 'all 0.2s ease-in-out', 'flex-shrink': 0, }; const customRadioIndicatorCheckedStyle = { 'border-color': buttonPrimaryColor, 'background-color': buttonPrimaryColor, 'box-shadow': `0 0 0 4px rgba(0, 123, 255, 0.3)`, }; const customRadioInnerDotStyle = { 'position': 'absolute', 'top': '50%', 'left': '50%', 'transform': 'translate(-50%, -50%)', 'width': '10px', 'height': '10px', 'border-radius': '50%', 'background': '#fff', 'opacity': '0', 'transition': 'opacity 0.2s ease-in-out', }; const customRadioInnerDotCheckedStyle = { 'opacity': '1', }; const templateCardStyle = (isSelected) => ({ padding: '1.2rem', border: `1px solid ${isSelected ? buttonPrimaryColor : '#333'}`, 'border-radius': '10px', cursor: 'pointer', background: isSelected ? '#2a2a3a' : '#1f1f1f', 'transition': 'all 0.2s ease-in-out', 'display': 'flex', 'flex-direction': 'column', 'align-items': 'flex-start', 'gap': '0.8rem', '&:hover': { background: '#252525', 'border-color': isSelected ? buttonPrimaryColor : '#666' }, 'box-shadow': isSelected ? `0 0 10px rgba(0, 123, 255, 0.4)` : 'none' }); const templateImageStyle = { 'width': '70px', 'height': '70px', 'object-fit': 'contain', 'border-radius': '8px', 'background': '#3a3a3a', 'padding': '5px', }; const templateDescriptionStyle = { 'font-size': '0.9rem', 'color': '#bbb', 'line-height': '1.4' }; const isNextDisabledStep1 = () => { if (projectSourceType() === 'template' && !selectedTemplateId()) return true; if (projectSourceType() === 'gitUrl' && !customGitUrl().trim()) return true; return false; }; const isCreateDisabledStep2 = () => { return loading() || !projectName().trim(); }; const handleProceedToNameStep = () => { if (isNextDisabledStep1()) return; setStep(2); }; const handleBackToSourceStep = () => { setStep(1); setStatus(''); }; const handleCreateProject = async () => { setLoading(true); setStatus(''); const finalName = projectName().trim(); if (!finalName) { setStatus('Project name cannot be empty'); setLoading(false); return; } let gitToUse = ''; if (projectSourceType() === 'template') { const chosenTemplate = templates()?.find(t => t.id === selectedTemplateId()); if (chosenTemplate) { gitToUse = chosenTemplate.git; } else { setStatus('Error: Selected template not found.'); setLoading(false); return; } } else { // 'gitUrl' gitToUse = customGitUrl().trim(); if (!gitToUse) { setStatus('Git URL cannot be empty.'); setLoading(false); return; } } try { const payload = { name: finalName, gitUrl: gitToUse, }; const res = await fetch(`${BASE_URL}/project`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); const text = await res.text(); if (res.ok) { setStatus(`Success: ${text}`); setTimeout(() => window.location.href = `${BASE_URL}/public/dashboard`, 1500); } else { setStatus(`Failed: ${text || 'Unknown error'}`); } } catch (err) { setStatus(`Error: ${err.message}`); } finally { setLoading(false); } }; return (

✨ Create New Project ✨

Choose Project Source

Loading templates...

Error loading templates: {templates.error.message}

{tpl => (
{ setSelectedTemplateId(tpl.id); setSelectedTemplateGitUrl(tpl.git); }} > {tpl.name}

{tpl.name}

{tpl.describe}

)}

No templates available.

setCustomGitUrl(e.currentTarget.value)} style={{ ...inputStyle, ...(customGitUrl() && inputFocusStyle) }} onFocus={e => e.currentTarget.style.borderColor = inputFocusStyle['border-color']} onBlur={e => e.currentTarget.style.borderColor = inputStyle.border.split(' ')[2]} disabled={loading()} />

Name Your Project

setProjectName(e.currentTarget.value)} style={{ ...inputStyle, ...(projectName() && inputFocusStyle) }} onFocus={e => e.currentTarget.style.borderColor = inputFocusStyle['border-color']} onBlur={e => e.currentTarget.style.borderColor = inputStyle.border.split(' ')[2]} disabled={loading()} />
{status()}
); } render(() => , document.getElementById('app'));