new app
This commit is contained in:
parent
9d22008f32
commit
dd56dc0df6
|
|
@ -1,8 +1,9 @@
|
|||
import { render as renderApp } from 'hono/jsx/dom'
|
||||
import { define, Styles } from '@because/forge'
|
||||
import type { App, AppState } from '../shared/types'
|
||||
import { generateTemplates } from '../shared/templates'
|
||||
import { theme } from './themes'
|
||||
import { Modal, initModal } from './tags/modal'
|
||||
import { closeModal, initModal, Modal, openModal, rerenderModal } from './tags/modal'
|
||||
import { initUpdate } from './update'
|
||||
import { openEmojiPicker } from './tags/emoji-picker'
|
||||
|
||||
|
|
@ -346,6 +347,143 @@ const stateLabels: Record<AppState, string> = {
|
|||
stopping: 'Stopping',
|
||||
}
|
||||
|
||||
// Form styles for modal
|
||||
const Form = define('Form', {
|
||||
base: 'form',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 16,
|
||||
})
|
||||
|
||||
const FormField = define('FormField', {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 6,
|
||||
})
|
||||
|
||||
const FormLabel = define('FormLabel', {
|
||||
base: 'label',
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
color: theme('colors-text'),
|
||||
})
|
||||
|
||||
const FormInput = define('FormInput', {
|
||||
base: 'input',
|
||||
padding: '8px 12px',
|
||||
background: theme('colors-bgSubtle'),
|
||||
border: `1px solid ${theme('colors-border')}`,
|
||||
borderRadius: theme('radius-md'),
|
||||
color: theme('colors-text'),
|
||||
fontSize: 14,
|
||||
selectors: {
|
||||
'&:focus': {
|
||||
outline: 'none',
|
||||
borderColor: theme('colors-primary'),
|
||||
},
|
||||
'&::placeholder': {
|
||||
color: theme('colors-textFaint'),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const FormError = define('FormError', {
|
||||
fontSize: 13,
|
||||
color: theme('colors-error'),
|
||||
})
|
||||
|
||||
const FormActions = define('FormActions', {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 8,
|
||||
marginTop: 8,
|
||||
})
|
||||
|
||||
// New App creation
|
||||
let newAppError = ''
|
||||
let newAppCreating = false
|
||||
|
||||
async function createNewApp(input: HTMLInputElement) {
|
||||
const name = input.value.trim().toLowerCase().replace(/\s+/g, '-')
|
||||
|
||||
if (!name) {
|
||||
newAppError = 'App name is required'
|
||||
rerenderModal()
|
||||
return
|
||||
}
|
||||
|
||||
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
||||
newAppError = 'Name must start with a letter and contain only lowercase letters, numbers, and hyphens'
|
||||
rerenderModal()
|
||||
return
|
||||
}
|
||||
|
||||
if (apps.some(a => a.name === name)) {
|
||||
newAppError = 'An app with this name already exists'
|
||||
rerenderModal()
|
||||
return
|
||||
}
|
||||
|
||||
newAppCreating = true
|
||||
newAppError = ''
|
||||
rerenderModal()
|
||||
|
||||
try {
|
||||
const templates = generateTemplates(name)
|
||||
|
||||
for (const [filename, content] of Object.entries(templates)) {
|
||||
const res = await fetch(`/api/sync/apps/${name}/files/${filename}`, {
|
||||
method: 'PUT',
|
||||
body: content,
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to create ${filename}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Success - close modal and select the new app
|
||||
selectedApp = name
|
||||
localStorage.setItem('selectedApp', name)
|
||||
closeModal()
|
||||
} catch (err) {
|
||||
newAppError = err instanceof Error ? err.message : 'Failed to create app'
|
||||
newAppCreating = false
|
||||
rerenderModal()
|
||||
}
|
||||
}
|
||||
|
||||
function openNewAppModal() {
|
||||
newAppError = ''
|
||||
newAppCreating = false
|
||||
|
||||
openModal('New App', () => (
|
||||
<Form onSubmit={(e: Event) => {
|
||||
e.preventDefault()
|
||||
const input = (e.target as HTMLFormElement).querySelector('input') as HTMLInputElement
|
||||
createNewApp(input)
|
||||
}}>
|
||||
<FormField>
|
||||
<FormLabel for="app-name">App Name</FormLabel>
|
||||
<FormInput
|
||||
id="app-name"
|
||||
type="text"
|
||||
placeholder="my-app"
|
||||
autofocus
|
||||
/>
|
||||
{newAppError && <FormError>{newAppError}</FormError>}
|
||||
</FormField>
|
||||
<FormActions>
|
||||
<Button type="button" onClick={closeModal} disabled={newAppCreating}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" variant="primary" disabled={newAppCreating}>
|
||||
{newAppCreating ? 'Creating...' : 'Create App'}
|
||||
</Button>
|
||||
</FormActions>
|
||||
</Form>
|
||||
))
|
||||
}
|
||||
|
||||
// Actions - call API then let SSE update the state
|
||||
const startApp = (name: string) => fetch(`/api/apps/${name}/start`, { method: 'POST' })
|
||||
const stopApp = (name: string) => fetch(`/api/apps/${name}/stop`, { method: 'POST' })
|
||||
|
|
@ -522,7 +660,7 @@ const Dashboard = () => {
|
|||
</AppList>
|
||||
{!sidebarCollapsed && (
|
||||
<SidebarFooter>
|
||||
<NewAppButton>+ New App</NewAppButton>
|
||||
<NewAppButton onClick={openNewAppModal}>+ New App</NewAppButton>
|
||||
</SidebarFooter>
|
||||
)}
|
||||
</Sidebar>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import type { App as SharedApp, AppState, LogLine } from '@types'
|
||||
import type { Subprocess } from 'bun'
|
||||
import { DEFAULT_EMOJI } from '@types'
|
||||
import { existsSync, readdirSync, readFileSync, statSync, watch, writeFileSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
|
||||
export type { AppState } from '@types'
|
||||
|
||||
export const APPS_DIR = join(process.env.DATA_DIR ?? '.', 'apps')
|
||||
|
||||
const DEFAULT_EMOJI = '🖥️'
|
||||
const MAX_LOGS = 100
|
||||
const _apps = new Map<string, App>()
|
||||
const _listeners = new Set<() => void>()
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { DEFAULT_EMOJI } from './types'
|
||||
|
||||
export interface AppTemplates {
|
||||
'index.tsx': string
|
||||
'package.json': string
|
||||
|
|
@ -41,6 +43,9 @@ export function generateTemplates(appName: string): AppTemplates {
|
|||
start: 'bun toes',
|
||||
dev: 'bun run --hot index.tsx',
|
||||
},
|
||||
toes: {
|
||||
icon: DEFAULT_EMOJI,
|
||||
},
|
||||
devDependencies: {
|
||||
'@types/bun': 'latest',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
export const DEFAULT_EMOJI = '🖥️'
|
||||
|
||||
export type AppState = 'invalid' | 'stopped' | 'starting' | 'running' | 'stopping'
|
||||
|
||||
export type LogLine = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user